Validierung Part II, neue Komponenten

This commit is contained in:
2024-08-01 22:43:32 +02:00
parent 2955c034a0
commit 29f88d610f
18 changed files with 1431 additions and 242 deletions

View File

@@ -9,8 +9,9 @@ import { rimraf } from 'rimraf';
import sharp from 'sharp';
import winston from 'winston';
import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js';
import { emailToDirName, KeyValueStyle } from '../models/main.model.js';
import { createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
import { SelectOptionsService } from '../select-options/select-options.service.js';
import { toDrizzleUser } from '../utils.js';
import * as schema from './schema.js';
const typesOfBusiness: Array<KeyValueStyle> = [
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
@@ -73,7 +74,7 @@ fs.ensureDirSync(`./pictures/property`);
//for (const userData of usersData) {
for (let index = 0; index < usersData.length; index++) {
const userData = usersData[index];
const user: User = { id: undefined, firstname: '', lastname: '', email: '' };
const user: User = createDefaultUser('', '', ''); //{ id: undefined, firstname: '', lastname: '', email: '' };
user.licensedIn = [];
userData.licensedIn.forEach(l => {
console.log(l['value'], l['name']);
@@ -114,7 +115,7 @@ for (let index = 0; index < usersData.length; index++) {
//sleep(200);
const u = await db
.insert(schema.users)
.values(user)
.values(toDrizzleUser(user))
.returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname });
generatedUserData.push(u[0]);
i++;

View File

@@ -19,6 +19,9 @@
// customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
// created?: Date;
// updated?: Date;
import { z } from 'zod';
// }
export interface UserData {
id?: string;
@@ -46,30 +49,108 @@ export type Gender = 'male' | 'female';
export type CustomerType = 'buyer' | 'professional';
export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
export type ListingsCategory = 'commercialProperty' | 'business';
export interface User {
id: string; // UUID as a string
firstname: string;
lastname: string;
email: string;
phoneNumber?: string;
description?: string;
companyName?: string;
companyOverview?: string;
companyWebsite?: string;
companyLocation?: string;
offeredServices?: string;
areasServed?: AreasServed[];
hasProfile?: boolean;
hasCompanyLogo?: boolean;
licensedIn?: LicensedIn[];
gender?: Gender;
customerType?: CustomerType;
customerSubType?: CustomerSubType;
created?: Date;
updated?: Date;
latitude?: number;
longitude?: number;
}
// export interface User {
// id: string; // UUID as a string
// firstname: string;
// lastname: string;
// email: string;
// phoneNumber?: string;
// description?: string;
// companyName?: string;
// companyOverview?: string;
// companyWebsite?: string;
// companyLocation?: string;
// offeredServices?: string;
// areasServed?: AreasServed[];
// hasProfile?: boolean;
// hasCompanyLogo?: boolean;
// licensedIn?: LicensedIn[];
// gender?: Gender;
// customerType?: CustomerType;
// customerSubType?: CustomerSubType;
// created?: Date;
// updated?: Date;
// latitude?: number;
// longitude?: number;
// }
// export interface AreasServed {
// county: string;
// state: string;
// }
// export interface LicensedIn {
// registerNo: string;
// state: string;
// }
// --------------------------------
//
// --------------------------------
export const GenderEnum = z.enum(['male', 'female']);
export const CustomerTypeEnum = z.enum(['buyer', 'professional']);
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);
export const AreasServedSchema = z.object({
county: z.string().nonempty('County is required'),
state: z.string().nonempty('State is required'),
});
export const LicensedInSchema = z.object({
registerNo: z.string().nonempty('Registration number is required'),
state: z.string().nonempty('State is required'),
});
export const UserSchema = z
.object({
id: z.string().uuid('Invalid ID format. Must be a valid UUID').optional(),
firstname: z.string().min(2, 'First name must be at least 2 characters long'),
lastname: z.string().min(2, 'Last name must be at least 2 characters long'),
email: z.string().email('Invalid email address'),
phoneNumber: z.string().optional().nullable(),
description: z.string().min(10, 'Description must be at least 10 characters long').optional().nullable(),
companyName: z.string().optional().nullable(),
companyOverview: z.string().min(10, 'Company overview must be at least 10 characters long').optional().nullable(),
companyWebsite: z.string().url('Invalid company website URL').optional().nullable(),
companyLocation: z.string().optional().nullable(), // Additional validation for US locations could be implemented here
offeredServices: z.string().min(10, 'Offered services must be at least 10 characters long').optional().nullable(),
areasServed: z.array(AreasServedSchema).optional().nullable(),
hasProfile: z.boolean().optional().nullable(),
hasCompanyLogo: z.boolean().optional().nullable(),
licensedIn: z.array(LicensedInSchema).optional().nullable(),
gender: GenderEnum.optional().nullable(),
customerType: CustomerTypeEnum.optional().nullable(),
customerSubType: CustomerSubTypeEnum.optional().nullable(),
created: z.date().optional().nullable(),
updated: z.date().optional().nullable(),
latitude: z.number().optional().nullable(),
longitude: z.number().optional().nullable(),
})
.refine(
data => {
if (data.customerType === 'professional') {
return !!data.customerSubType && !!data.phoneNumber && !!data.companyOverview && !!data.description && !!data.offeredServices && !!data.companyLocation && data.areasServed && data.areasServed.length > 0;
}
return true;
},
{
message: 'For professional customers, additional fields are required: customer subtype, phone number, company overview, description, offered services, company location, and at least one area served',
path: ['customerType'],
},
)
.refine(
data => {
if (data.customerType === 'professional') {
return /\(\d{3}\) \d{3}-\d{4}$/.test(data.phoneNumber || '');
}
return true;
},
{
message: 'Phone number must be in US format: +1 (XXX) XXX-XXXX',
path: ['phoneNumber'],
},
);
export type AreasServed = z.infer<typeof AreasServedSchema>;
export type LicensedIn = z.infer<typeof LicensedInSchema>;
export type User = z.infer<typeof UserSchema>;
export interface BusinessListing {
id: string; // UUID as a string
email: string; // References users.email
@@ -131,11 +212,3 @@ export interface CommercialPropertyListing {
longitude?: number; // double precision
// embedding?: number[]; // Uncomment if needed for vector embedding
}
export interface AreasServed {
county: string;
state: string;
}
export interface LicensedIn {
registerNo: string;
state: string;
}

View File

@@ -281,3 +281,87 @@ export interface ValidationMessage {
field: string;
message: string;
}
export function createDefaultUser(email: string, firstname: string, lastname: string): User {
return {
id: undefined,
email,
firstname,
lastname,
phoneNumber: null,
description: null,
companyName: null,
companyOverview: null,
companyWebsite: null,
companyLocation: null,
offeredServices: null,
areasServed: [],
hasProfile: false,
hasCompanyLogo: false,
licensedIn: [],
gender: null,
customerType: 'buyer',
customerSubType: null,
created: new Date(),
updated: new Date(),
};
}
export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
return {
id: undefined,
serialId: undefined,
email: '',
type: null,
title: '',
description: '',
city: '',
state: '',
price: null,
favoritesForUser: [],
hideImage: false,
draft: false,
zipCode: null,
county: '',
imageOrder: [],
imagePath: '',
created: null,
updated: null,
visits: null,
lastVisit: null,
latitude: null,
longitude: null,
listingsCategory: 'commercialProperty',
};
}
export function createDefaultBusinessListing(): BusinessListing {
return {
id: undefined,
email: '',
type: null,
title: '',
description: '',
city: '',
state: '',
price: null,
favoritesForUser: [],
draft: false,
realEstateIncluded: false,
leasedLocation: false,
franchiseResale: false,
salesRevenue: null,
cashFlow: null,
supportAndTraining: '',
employees: null,
established: null,
internalListingNumber: null,
reasonForSale: '',
brokerLicencing: '',
internals: '',
created: null,
updated: null,
visits: null,
lastVisit: null,
latitude: null,
longitude: null,
listingsCategory: 'business',
};
}

View File

@@ -1,15 +1,16 @@
import { Inject, Injectable } from '@nestjs/common';
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { ZodError } from 'zod';
import * as schema from '../drizzle/schema.js';
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema.js';
import { FileService } from '../file/file.service.js';
import { GeoService } from '../geo/geo.service.js';
import { User } from '../models/db.model.js';
import { emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
import { getDistanceQuery } from '../utils.js';
import { User, UserSchema } from '../models/db.model.js';
import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
import { getDistanceQuery, toDrizzleUser } from '../utils.js';
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
@Injectable()
@@ -111,9 +112,8 @@ export class UserService {
.from(schema.users)
.where(sql`email = ${email}`)) as User[];
if (users.length === 0) {
const user: User = { id: undefined, email, firstname: jwtuser.firstname, lastname: jwtuser.lastname, customerType: 'buyer' };
this.saveUser(user);
return user;
const user: User = { id: undefined, customerType: 'buyer', ...createDefaultUser(email, jwtuser.firstname, jwtuser.lastname) };
return await this.saveUser(user);
} else {
const user = users[0];
user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email));
@@ -132,17 +132,33 @@ export class UserService {
user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email));
return user;
}
async saveUser(user: any): Promise<User> {
if (user.id) {
user.created = new Date(user.created);
async saveUser(user: User): Promise<User> {
try {
user.updated = new Date();
const [updateUser] = await this.conn.update(schema.users).set(user).where(eq(schema.users.id, user.id)).returning();
return updateUser as User;
} else {
user.created = new Date();
user.updated = new Date();
const [newUser] = await this.conn.insert(schema.users).values(user).returning();
return newUser as User;
if (user.id) {
user.created = new Date(user.created);
} else {
user.created = new Date();
}
const validatedUser = UserSchema.parse(user);
const drizzleUser = toDrizzleUser(validatedUser);
if (user.id) {
const [updateUser] = await this.conn.update(schema.users).set(drizzleUser).where(eq(schema.users.id, user.id)).returning();
return updateUser as User;
} else {
const drizzleUser = toDrizzleUser(user);
const [newUser] = await this.conn.insert(schema.users).values(drizzleUser).returning();
return newUser as User;
}
} catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
}));
throw new BadRequestException(formattedErrors);
}
throw error;
}
}
// async findUser(criteria: UserListingCriteria) {

View File

@@ -1,5 +1,7 @@
import { sql } from 'drizzle-orm';
import { z } from 'zod';
import { businesses, commercials, users } from './drizzle/schema.js';
import { AreasServedSchema, CustomerSubTypeEnum, CustomerTypeEnum, GenderEnum, LicensedInSchema, User } from './models/db.model.js';
export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern
export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen
@@ -28,3 +30,33 @@ export const getDistanceQuery = (schema: typeof businesses | typeof commercials
))
`;
};
export function toDrizzleUser(user: User): {
email: string;
firstname: string;
lastname: string;
phoneNumber?: string;
description?: string;
companyName?: string;
companyOverview?: string;
companyWebsite?: string;
companyLocation?: string;
offeredServices?: string;
areasServed?: (typeof AreasServedSchema._type)[];
hasProfile?: boolean;
hasCompanyLogo?: boolean;
licensedIn?: (typeof LicensedInSchema._type)[];
gender?: z.infer<typeof GenderEnum>;
customerType?: z.infer<typeof CustomerTypeEnum>;
customerSubType?: z.infer<typeof CustomerSubTypeEnum>;
latitude?: number;
longitude?: number;
} {
const { id, created, updated, ...drizzleUser } = user;
return {
...drizzleUser,
email: drizzleUser.email,
firstname: drizzleUser.firstname,
lastname: drizzleUser.lastname,
};
}