umstellung auf json Tabellen ...

This commit is contained in:
2025-08-03 09:12:57 -05:00
parent 9c88143c04
commit 738f1d929b
6 changed files with 236 additions and 308 deletions

View File

@@ -1,12 +1,11 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
import { and, arrayContains, asc, count, desc, eq, gte, ilike, inArray, lte, ne, or, SQL, sql } from 'drizzle-orm';
import { and, arrayContains, asc, count, desc, eq, gte, inArray, lte, or, SQL, sql } from 'drizzle-orm';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston';
import { ZodError } from 'zod';
import * as schema from '../drizzle/schema';
import { businesses, PG_CONNECTION } from '../drizzle/schema';
import { FileService } from '../file/file.service';
import { businesses_json, PG_CONNECTION, users_json } from '../drizzle/schema';
import { GeoService } from '../geo/geo.service';
import { BusinessListing, BusinessListingSchema } from '../models/db.model';
import { BusinessListingCriteria, JwtUser } from '../models/main.model';
@@ -17,7 +16,6 @@ export class BusinessListingService {
constructor(
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
private fileService?: FileService,
private geoService?: GeoService,
) {}
@@ -25,104 +23,105 @@ export class BusinessListingService {
const whereConditions: SQL[] = [];
if (criteria.city && criteria.searchType === 'exact') {
whereConditions.push(sql`${businesses.location}->>'name' ilike ${criteria.city.name}`);
//whereConditions.push(ilike(businesses.location-->'city', `%${criteria.city.name}%`));
whereConditions.push(sql`(${businesses_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`);
}
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name);
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
whereConditions.push(sql`${getDistanceQuery(businesses_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
}
if (criteria.types && criteria.types.length > 0) {
whereConditions.push(inArray(businesses.type, criteria.types));
whereConditions.push(inArray(sql`${businesses_json.data}->>'type'`, criteria.types));
}
if (criteria.state) {
whereConditions.push(sql`${businesses.location}->>'state' = ${criteria.state}`);
whereConditions.push(sql`(${businesses_json.data}->'location'->>'state') = ${criteria.state}`);
}
if (criteria.minPrice) {
whereConditions.push(gte(businesses.price, criteria.minPrice));
whereConditions.push(gte(sql`(${businesses_json.data}->>'price')::double precision`, criteria.minPrice));
}
if (criteria.maxPrice) {
whereConditions.push(lte(businesses.price, criteria.maxPrice));
whereConditions.push(lte(sql`(${businesses_json.data}->>'price')::double precision`, criteria.maxPrice));
}
if (criteria.minRevenue) {
whereConditions.push(gte(businesses.salesRevenue, criteria.minRevenue));
whereConditions.push(gte(sql`(${businesses_json.data}->>'salesRevenue')::double precision`, criteria.minRevenue));
}
if (criteria.maxRevenue) {
whereConditions.push(lte(businesses.salesRevenue, criteria.maxRevenue));
whereConditions.push(lte(sql`(${businesses_json.data}->>'salesRevenue')::double precision`, criteria.maxRevenue));
}
if (criteria.minCashFlow) {
whereConditions.push(gte(businesses.cashFlow, criteria.minCashFlow));
whereConditions.push(gte(sql`(${businesses_json.data}->>'cashFlow')::double precision`, criteria.minCashFlow));
}
if (criteria.maxCashFlow) {
whereConditions.push(lte(businesses.cashFlow, criteria.maxCashFlow));
whereConditions.push(lte(sql`(${businesses_json.data}->>'cashFlow')::double precision`, criteria.maxCashFlow));
}
if (criteria.minNumberEmployees) {
whereConditions.push(gte(businesses.employees, criteria.minNumberEmployees));
whereConditions.push(gte(sql`(${businesses_json.data}->>'employees')::integer`, criteria.minNumberEmployees));
}
if (criteria.maxNumberEmployees) {
whereConditions.push(lte(businesses.employees, criteria.maxNumberEmployees));
whereConditions.push(lte(sql`(${businesses_json.data}->>'employees')::integer`, criteria.maxNumberEmployees));
}
if (criteria.establishedSince) {
whereConditions.push(gte(businesses.established, criteria.establishedSince));
whereConditions.push(gte(sql`(${businesses_json.data}->>'established')::integer`, criteria.establishedSince));
}
if (criteria.establishedUntil) {
whereConditions.push(lte(businesses.established, criteria.establishedUntil));
whereConditions.push(lte(sql`(${businesses_json.data}->>'established')::integer`, criteria.establishedUntil));
}
if (criteria.realEstateChecked) {
whereConditions.push(eq(businesses.realEstateIncluded, criteria.realEstateChecked));
whereConditions.push(eq(sql`(${businesses_json.data}->>'realEstateIncluded')::boolean`, criteria.realEstateChecked));
}
if (criteria.leasedLocation) {
whereConditions.push(eq(businesses.leasedLocation, criteria.leasedLocation));
whereConditions.push(eq(sql`(${businesses_json.data}->>'leasedLocation')::boolean`, criteria.leasedLocation));
}
if (criteria.franchiseResale) {
whereConditions.push(eq(businesses.franchiseResale, criteria.franchiseResale));
whereConditions.push(eq(sql`(${businesses_json.data}->>'franchiseResale')::boolean`, criteria.franchiseResale));
}
if (criteria.title) {
whereConditions.push(or(ilike(businesses.title, `%${criteria.title}%`), ilike(businesses.description, `%${criteria.title}%`)));
whereConditions.push(sql`(${businesses_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${businesses_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`);
}
if (criteria.brokerName) {
const { firstname, lastname } = splitName(criteria.brokerName);
if (firstname === lastname) {
whereConditions.push(or(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
whereConditions.push(sql`(${users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`);
} else {
whereConditions.push(and(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`)));
whereConditions.push(sql`(${users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`);
}
}
if (criteria.email) {
whereConditions.push(eq(schema.users.email, criteria.email));
whereConditions.push(eq(users_json.email, criteria.email));
}
if (user?.role !== 'admin') {
whereConditions.push(or(eq(businesses.email, user?.email), ne(businesses.draft, true)));
whereConditions.push(or(eq(businesses_json.email, user?.email), sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`));
}
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
whereConditions.push(and(sql`(${users_json.data}->>'customerType') = 'professional'`, sql`(${users_json.data}->>'customerSubType') = 'broker'`));
return whereConditions;
}
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
const start = criteria.start ? criteria.start : 0;
const length = criteria.length ? criteria.length : 12;
const query = this.conn
.select({
business: businesses,
brokerFirstName: schema.users.firstname,
brokerLastName: schema.users.lastname,
business: businesses_json,
brokerFirstName: sql`${users_json.data}->>'firstname'`.as('brokerFirstName'),
brokerLastName: sql`${users_json.data}->>'lastname'`.as('brokerLastName'),
})
.from(businesses)
.leftJoin(schema.users, eq(businesses.email, schema.users.email));
.from(businesses_json)
.leftJoin(users_json, eq(businesses_json.email, users_json.email));
const whereConditions = this.getWhereConditions(criteria, user);
@@ -134,28 +133,28 @@ export class BusinessListingService {
// Sortierung
switch (criteria.sortBy) {
case 'priceAsc':
query.orderBy(asc(businesses.price));
query.orderBy(asc(sql`(${businesses_json.data}->>'price')::double precision`));
break;
case 'priceDesc':
query.orderBy(desc(businesses.price));
query.orderBy(desc(sql`(${businesses_json.data}->>'price')::double precision`));
break;
case 'srAsc':
query.orderBy(asc(businesses.salesRevenue));
query.orderBy(asc(sql`(${businesses_json.data}->>'salesRevenue')::double precision`));
break;
case 'srDesc':
query.orderBy(desc(businesses.salesRevenue));
query.orderBy(desc(sql`(${businesses_json.data}->>'salesRevenue')::double precision`));
break;
case 'cfAsc':
query.orderBy(asc(businesses.cashFlow));
query.orderBy(asc(sql`(${businesses_json.data}->>'cashFlow')::double precision`));
break;
case 'cfDesc':
query.orderBy(desc(businesses.cashFlow));
query.orderBy(desc(sql`(${businesses_json.data}->>'cashFlow')::double precision`));
break;
case 'creationDateFirst':
query.orderBy(asc(businesses.created));
query.orderBy(asc(sql`${businesses_json.data}->>'created'`));
break;
case 'creationDateLast':
query.orderBy(desc(businesses.created));
query.orderBy(desc(sql`${businesses_json.data}->>'created'`));
break;
default:
// Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden
@@ -166,7 +165,13 @@ export class BusinessListingService {
const data = await query;
const totalCount = await this.getBusinessListingsCount(criteria, user);
const results = data.map(r => r.business);
const results = data.map(r => ({
id: r.business.id,
email: r.business.email,
...(r.business.data as BusinessListing),
brokerFirstName: r.brokerFirstName,
brokerLastName: r.brokerLastName,
}));
return {
results,
totalCount,
@@ -174,7 +179,7 @@ export class BusinessListingService {
}
async getBusinessListingsCount(criteria: BusinessListingCriteria, user: JwtUser): Promise<number> {
const countQuery = this.conn.select({ value: count() }).from(businesses).leftJoin(schema.users, eq(businesses.email, schema.users.email));
const countQuery = this.conn.select({ value: count() }).from(businesses_json).leftJoin(users_json, eq(businesses_json.email, users_json.email));
const whereConditions = this.getWhereConditions(criteria, user);
@@ -190,15 +195,15 @@ export class BusinessListingService {
async findBusinessesById(id: string, user: JwtUser): Promise<BusinessListing> {
const conditions = [];
if (user?.role !== 'admin') {
conditions.push(or(eq(businesses.email, user?.email), ne(businesses.draft, true)));
conditions.push(or(eq(businesses_json.email, user?.email), sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`));
}
conditions.push(sql`${businesses.id} = ${id}`);
conditions.push(eq(businesses_json.id, id));
const result = await this.conn
.select()
.from(businesses)
.from(businesses_json)
.where(and(...conditions));
if (result.length > 0) {
return result[0] as BusinessListing;
return { id: result[0].id, email: result[0].email, ...(result[0].data as BusinessListing) } as BusinessListing;
} else {
throw new BadRequestException(`No entry available for ${id}`);
}
@@ -206,35 +211,34 @@ export class BusinessListingService {
async findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
const conditions = [];
conditions.push(eq(businesses.email, email));
conditions.push(eq(businesses_json.email, email));
if (email !== user?.email && user?.role !== 'admin') {
conditions.push(ne(businesses.draft, true));
conditions.push(sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`);
}
const listings = (await this.conn
const listings = await this.conn
.select()
.from(businesses)
.where(and(...conditions))) as BusinessListing[];
return listings;
.from(businesses_json)
.where(and(...conditions));
return listings.map(l => ({ id: l.id, email: l.email, ...(l.data as BusinessListing) }) as BusinessListing);
}
// #### Find Favorites ########################################
async findFavoriteListings(user: JwtUser): Promise<BusinessListing[]> {
const userFavorites = await this.conn
.select()
.from(businesses)
.where(arrayContains(businesses.favoritesForUser, [user.email]));
return userFavorites;
.from(businesses_json)
.where(arrayContains(sql`${businesses_json.data}->>'favoritesForUser'`, [user.email]));
return userFavorites.map(l => ({ id: l.id, email: l.email, ...(l.data as BusinessListing) }) as BusinessListing);
}
// #### CREATE ########################################
async createListing(data: BusinessListing): Promise<BusinessListing> {
try {
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
data.updated = new Date();
BusinessListingSchema.parse(data);
const convertedBusinessListing = data;
delete convertedBusinessListing.id;
const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
return createdListing;
const { id, email, ...rest } = data;
const convertedBusinessListing = { email, data: rest };
const [createdListing] = await this.conn.insert(businesses_json).values(convertedBusinessListing).returning();
return { id: createdListing.id, email: createdListing.email, ...(createdListing.data as BusinessListing) };
} catch (error) {
if (error instanceof ZodError) {
const filteredErrors = error.errors
@@ -248,10 +252,10 @@ export class BusinessListingService {
throw error;
}
}
// #### UPDATE Business ########################################
async updateBusinessListing(id: string, data: BusinessListing, user: JwtUser): Promise<BusinessListing> {
try {
const [existingListing] = await this.conn.select().from(businesses).where(eq(businesses.id, id));
const [existingListing] = await this.conn.select().from(businesses_json).where(eq(businesses_json.id, id));
if (!existingListing) {
throw new NotFoundException(`Business listing with id ${id} not found`);
@@ -259,12 +263,13 @@ export class BusinessListingService {
data.updated = new Date();
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
if (existingListing.email === user?.email) {
data.favoritesForUser = existingListing.favoritesForUser;
data.favoritesForUser = (<BusinessListing>existingListing.data).favoritesForUser || [];
}
BusinessListingSchema.parse(data);
const convertedBusinessListing = data;
const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
return updateListing;
const { id: _, email, ...rest } = data;
const convertedBusinessListing = { email, data: rest };
const [updateListing] = await this.conn.update(businesses_json).set(convertedBusinessListing).where(eq(businesses_json.id, id)).returning();
return { id: updateListing.id, email: updateListing.email, ...(updateListing.data as BusinessListing) };
} catch (error) {
if (error instanceof ZodError) {
const filteredErrors = error.errors
@@ -278,17 +283,17 @@ export class BusinessListingService {
throw error;
}
}
// #### DELETE ########################################
async deleteListing(id: string): Promise<void> {
await this.conn.delete(businesses).where(eq(businesses.id, id));
await this.conn.delete(businesses_json).where(eq(businesses_json.id, id));
}
// #### DELETE Favorite ###################################
async deleteFavorite(id: string, user: JwtUser): Promise<void> {
await this.conn
.update(businesses)
.update(businesses_json)
.set({
favoritesForUser: sql`array_remove(${businesses.favoritesForUser}, ${user.email})`,
data: sql`jsonb_set(${businesses_json.data}, '{favoritesForUser}', array_remove((${businesses_json.data}->>'favoritesForUser')::jsonb, ${user.email}))`,
})
.where(sql`${businesses.id} = ${id}`);
.where(eq(businesses_json.id, id));
}
}