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, 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() export class UserService { constructor( @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(PG_CONNECTION) private conn: NodePgDatabase, private fileService: FileService, private geoService: GeoService, ) {} private getWhereConditions(criteria: UserListingCriteria): SQL[] { const whereConditions: SQL[] = []; whereConditions.push(eq(schema.users.customerType, 'professional')); if (criteria.city && criteria.searchType === 'exact') { whereConditions.push(ilike(schema.users.companyLocation, `%${criteria.city}%`)); } if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city); whereConditions.push(sql`${getDistanceQuery(schema.users, parseFloat(cityGeo.latitude), parseFloat(cityGeo.longitude))} <= ${criteria.radius}`); } if (criteria.types && criteria.types.length > 0) { // whereConditions.push(inArray(schema.users.customerSubType, criteria.types)); whereConditions.push(inArray(schema.users.customerSubType, criteria.types as CustomerSubType[])); } if (criteria.firstname) { whereConditions.push(ilike(schema.users.firstname, `%${criteria.firstname}%`)); } if (criteria.lastname) { whereConditions.push(ilike(schema.users.lastname, `%${criteria.lastname}%`)); } if (criteria.companyName) { whereConditions.push(ilike(schema.users.companyName, `%${criteria.companyName}%`)); } if (criteria.counties && criteria.counties.length > 0) { whereConditions.push(or(...criteria.counties.map(county => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'county' ILIKE ${`%${county}%`})`))); } if (criteria.state) { whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${criteria.state})`); } return whereConditions; } async searchUserListings(criteria: UserListingCriteria) { const start = criteria.start ? criteria.start : 0; const length = criteria.length ? criteria.length : 12; const query = this.conn.select().from(schema.users); const whereConditions = this.getWhereConditions(criteria); if (whereConditions.length > 0) { const whereClause = and(...whereConditions); query.where(whereClause); } // Paginierung query.limit(length).offset(start); const results = await query; const totalCount = await this.getUserListingsCount(criteria); return { results, totalCount, }; } async getUserListingsCount(criteria: UserListingCriteria): Promise { const countQuery = this.conn.select({ value: count() }).from(schema.users); const whereConditions = this.getWhereConditions(criteria); if (whereConditions.length > 0) { const whereClause = and(...whereConditions); countQuery.where(whereClause); } const [{ value: totalCount }] = await countQuery; return totalCount; } async getUserByMail(email: string, jwtuser?: JwtUser) { const users = (await this.conn .select() .from(schema.users) .where(sql`email = ${email}`)) as User[]; if (users.length === 0) { 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)); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email)); return user; } } async getUserById(id: string) { const users = (await this.conn .select() .from(schema.users) .where(sql`id = ${id}`)) as User[]; const user = users[0]; user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email)); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email)); return user; } async saveUser(user: User): Promise { try { user.updated = new Date(); 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 getStates(): Promise { const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`; const result = await this.conn.execute(query); return result.rows; } }