From 738f1d929b5fda304351215574a9401fbee58631 Mon Sep 17 00:00:00 2001 From: Andreas Knuth Date: Sun, 3 Aug 2025 09:12:57 -0500 Subject: [PATCH] umstellung auf json Tabellen ... --- bizmatch-server/src/drizzle/schema.ts | 78 +++++++-- bizmatch-server/src/event/event.service.ts | 9 +- .../src/listings/business-listing.service.ts | 155 +++++++++--------- .../listings/commercial-property.service.ts | 109 ++++++------ bizmatch-server/src/user/user.service.ts | 61 ++++--- bizmatch-server/src/utils.ts | 132 +-------------- 6 files changed, 236 insertions(+), 308 deletions(-) diff --git a/bizmatch-server/src/drizzle/schema.ts b/bizmatch-server/src/drizzle/schema.ts index 54f877f..3df4188 100644 --- a/bizmatch-server/src/drizzle/schema.ts +++ b/bizmatch-server/src/drizzle/schema.ts @@ -8,6 +8,56 @@ export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', ' export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']); export const subscriptionTypeEnum = pgEnum('subscriptionType', ['free', 'professional', 'broker']); +// Neue JSONB-basierte Tabellen +export const users_json = pgTable( + 'users_json', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + email: varchar('email', { length: 255 }).notNull().unique(), + data: jsonb('data'), + }, + table => ({ + emailIdx: index('idx_users_json_email').on(table.email), + }), +); + +export const businesses_json = pgTable( + 'businesses_json', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + email: varchar('email', { length: 255 }).references(() => users_json.email), + data: jsonb('data'), + }, + table => ({ + emailIdx: index('idx_businesses_json_email').on(table.email), + }), +); + +export const commercials_json = pgTable( + 'commercials_json', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + email: varchar('email', { length: 255 }).references(() => users_json.email), + data: jsonb('data'), + }, + table => ({ + emailIdx: index('idx_commercials_json_email').on(table.email), + }), +); + +export const listing_events_json = pgTable( + 'listing_events_json', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + email: varchar('email', { length: 255 }), + data: jsonb('data'), + }, + table => ({ + emailIdx: index('idx_listing_events_json_email').on(table.email), + }), +); + +// Bestehende Tabellen bleiben unverändert export const users = pgTable( 'users', { @@ -41,6 +91,7 @@ export const users = pgTable( ), }), ); + export const businesses = pgTable( 'businesses', { @@ -52,7 +103,7 @@ export const businesses = pgTable( price: doublePrecision('price'), favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(), draft: boolean('draft'), - listingsCategory: listingsCategoryEnum('listingsCategory'), //varchar('listingsCategory', { length: 255 }), + listingsCategory: listingsCategoryEnum('listingsCategory'), realEstateIncluded: boolean('realEstateIncluded'), leasedLocation: boolean('leasedLocation'), franchiseResale: boolean('franchiseResale'), @@ -76,6 +127,7 @@ export const businesses = pgTable( ), }), ); + export const commercials = pgTable( 'commercials', { @@ -87,7 +139,7 @@ export const commercials = pgTable( description: text('description'), price: doublePrecision('price'), favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(), - listingsCategory: listingsCategoryEnum('listingsCategory'), //listingsCategory: varchar('listingsCategory', { length: 255 }), + listingsCategory: listingsCategoryEnum('listingsCategory'), draft: boolean('draft'), imageOrder: varchar('imageOrder', { length: 200 }).array(), imagePath: varchar('imagePath', { length: 200 }), @@ -102,18 +154,18 @@ export const commercials = pgTable( }), ); -export const listingEvents = pgTable('listing_events', { +export const listing_events = pgTable('listing_events', { id: uuid('id').primaryKey().defaultRandom().notNull(), - listingId: varchar('listing_id', { length: 255 }), // Assuming listings are referenced by UUID, adjust as necessary + listingId: varchar('listing_id', { length: 255 }), email: varchar('email', { length: 255 }), - eventType: varchar('event_type', { length: 50 }), // 'view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact' + eventType: varchar('event_type', { length: 50 }), eventTimestamp: timestamp('event_timestamp').defaultNow(), - userIp: varchar('user_ip', { length: 45 }), // Optional if you choose to track IP in frontend or backend - userAgent: varchar('user_agent', { length: 255 }), // Store User-Agent as string - locationCountry: varchar('location_country', { length: 100 }), // Country from IP - locationCity: varchar('location_city', { length: 100 }), // City from IP - locationLat: varchar('location_lat', { length: 20 }), // Latitude from IP, stored as varchar - locationLng: varchar('location_lng', { length: 20 }), // Longitude from IP, stored as varchar - referrer: varchar('referrer', { length: 255 }), // Referrer URL if applicable - additionalData: jsonb('additional_data'), // JSON for any other optional data (like email, social shares etc.) + userIp: varchar('user_ip', { length: 45 }), + userAgent: varchar('user_agent', { length: 255 }), + locationCountry: varchar('location_country', { length: 100 }), + locationCity: varchar('location_city', { length: 100 }), + locationLat: varchar('location_lat', { length: 20 }), + locationLng: varchar('location_lng', { length: 20 }), + referrer: varchar('referrer', { length: 255 }), + additionalData: jsonb('additional_data'), }); diff --git a/bizmatch-server/src/event/event.service.ts b/bizmatch-server/src/event/event.service.ts index c344db6..0a725bf 100644 --- a/bizmatch-server/src/event/event.service.ts +++ b/bizmatch-server/src/event/event.service.ts @@ -2,17 +2,22 @@ import { Inject, Injectable } from '@nestjs/common'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { ListingEvent } from 'src/models/db.model'; +import { Logger } from 'winston'; import * as schema from '../drizzle/schema'; -import { listingEvents, PG_CONNECTION } from '../drizzle/schema'; +import { listing_events_json, PG_CONNECTION } from '../drizzle/schema'; + @Injectable() export class EventService { constructor( @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(PG_CONNECTION) private conn: NodePgDatabase, ) {} + async createEvent(event: ListingEvent) { // Speichere das Event in der Datenbank event.eventTimestamp = new Date(); - await this.conn.insert(listingEvents).values(event).execute(); + const { id, email, ...rest } = event; + const convertedEvent = { email, data: rest }; + await this.conn.insert(listing_events_json).values(convertedEvent).execute(); } } diff --git a/bizmatch-server/src/listings/business-listing.service.ts b/bizmatch-server/src/listings/business-listing.service.ts index fb7ffb5..a17f2c6 100644 --- a/bizmatch-server/src/listings/business-listing.service.ts +++ b/bizmatch-server/src/listings/business-listing.service.ts @@ -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, - 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 { - 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 { 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 { 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 { 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 { 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 { 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 = (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 { - 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 { 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)); } } diff --git a/bizmatch-server/src/listings/commercial-property.service.ts b/bizmatch-server/src/listings/commercial-property.service.ts index d3f816e..aab76cb 100644 --- a/bizmatch-server/src/listings/commercial-property.service.ts +++ b/bizmatch-server/src/listings/commercial-property.service.ts @@ -1,11 +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 { commercials, PG_CONNECTION } from '../drizzle/schema'; +import { commercials_json, PG_CONNECTION } from '../drizzle/schema'; import { FileService } from '../file/file.service'; import { GeoService } from '../geo/geo.service'; import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model'; @@ -24,33 +24,33 @@ export class CommercialPropertyService { const whereConditions: SQL[] = []; if (criteria.city && criteria.searchType === 'exact') { - whereConditions.push(sql`${commercials.location}->>'name' ilike ${criteria.city.name}`); + whereConditions.push(sql`(${commercials_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(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); + whereConditions.push(sql`${getDistanceQuery(commercials_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); } if (criteria.types && criteria.types.length > 0) { - whereConditions.push(inArray(schema.commercials.type, criteria.types)); + whereConditions.push(inArray(sql`${commercials_json.data}->>'type'`, criteria.types)); } if (criteria.state) { - whereConditions.push(sql`${schema.commercials.location}->>'state' = ${criteria.state}`); + whereConditions.push(sql`(${commercials_json.data}->'location'->>'state') = ${criteria.state}`); } if (criteria.minPrice) { - whereConditions.push(gte(schema.commercials.price, criteria.minPrice)); + whereConditions.push(gte(sql`(${commercials_json.data}->>'price')::double precision`, criteria.minPrice)); } if (criteria.maxPrice) { - whereConditions.push(lte(schema.commercials.price, criteria.maxPrice)); + whereConditions.push(lte(sql`(${commercials_json.data}->>'price')::double precision`, criteria.maxPrice)); } if (criteria.title) { - whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`))); + whereConditions.push(sql`(${commercials_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${commercials_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`); } if (user?.role !== 'admin') { - whereConditions.push(or(eq(commercials.email, user?.email), ne(commercials.draft, true))); + whereConditions.push(or(eq(commercials_json.email, user?.email), sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`)); } // whereConditions.push(and(eq(schema.users.customerType, 'professional'))); return whereConditions; @@ -59,7 +59,7 @@ export class CommercialPropertyService { async searchCommercialProperties(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise { const start = criteria.start ? criteria.start : 0; const length = criteria.length ? criteria.length : 12; - const query = this.conn.select({ commercial: commercials }).from(commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email)); + const query = this.conn.select({ commercial: commercials_json }).from(commercials_json).leftJoin(schema.users_json, eq(commercials_json.email, schema.users_json.email)); const whereConditions = this.getWhereConditions(criteria, user); if (whereConditions.length > 0) { @@ -69,16 +69,16 @@ export class CommercialPropertyService { // Sortierung switch (criteria.sortBy) { case 'priceAsc': - query.orderBy(asc(commercials.price)); + query.orderBy(asc(sql`(${commercials_json.data}->>'price')::double precision`)); break; case 'priceDesc': - query.orderBy(desc(commercials.price)); + query.orderBy(desc(sql`(${commercials_json.data}->>'price')::double precision`)); break; case 'creationDateFirst': - query.orderBy(asc(commercials.created)); + query.orderBy(asc(sql`${commercials_json.data}->>'created'`)); break; case 'creationDateLast': - query.orderBy(desc(commercials.created)); + query.orderBy(desc(sql`${commercials_json.data}->>'created'`)); break; default: // Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden @@ -89,7 +89,7 @@ export class CommercialPropertyService { query.limit(length).offset(start); const data = await query; - const results = data.map(r => r.commercial); + const results = data.map(r => ({ id: r.commercial.id, email: r.commercial.email, ...(r.commercial.data as CommercialPropertyListing) })); const totalCount = await this.getCommercialPropertiesCount(criteria, user); return { @@ -98,7 +98,7 @@ export class CommercialPropertyService { }; } async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise { - const countQuery = this.conn.select({ value: count() }).from(schema.commercials).leftJoin(schema.users, eq(commercials.email, schema.users.email)); + const countQuery = this.conn.select({ value: count() }).from(commercials_json).leftJoin(schema.users_json, eq(commercials_json.email, schema.users_json.email)); const whereConditions = this.getWhereConditions(criteria, user); if (whereConditions.length > 0) { @@ -114,15 +114,15 @@ export class CommercialPropertyService { async findCommercialPropertiesById(id: string, user: JwtUser): Promise { const conditions = []; if (user?.role !== 'admin') { - conditions.push(or(eq(commercials.email, user?.email), ne(commercials.draft, true))); + conditions.push(or(eq(commercials_json.email, user?.email), sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`)); } - conditions.push(sql`${commercials.id} = ${id}`); + conditions.push(eq(commercials_json.id, id)); const result = await this.conn .select() - .from(commercials) + .from(commercials_json) .where(and(...conditions)); if (result.length > 0) { - return result[0] as CommercialPropertyListing; + return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing; } else { throw new BadRequestException(`No entry available for ${id}`); } @@ -131,31 +131,33 @@ export class CommercialPropertyService { // #### Find by User EMail ######################################## async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise { const conditions = []; - conditions.push(eq(commercials.email, email)); + conditions.push(eq(commercials_json.email, email)); if (email !== user?.email && user?.role !== 'admin') { - conditions.push(ne(commercials.draft, true)); + conditions.push(sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`); } - const listings = (await this.conn + const listings = await this.conn .select() - .from(commercials) - .where(and(...conditions))) as CommercialPropertyListing[]; - return listings as CommercialPropertyListing[]; + .from(commercials_json) + .where(and(...conditions)); + return listings.map(l => ({ id: l.id, email: l.email, ...(l.data as CommercialPropertyListing) }) as CommercialPropertyListing); } // #### Find Favorites ######################################## async findFavoriteListings(user: JwtUser): Promise { const userFavorites = await this.conn .select() - .from(commercials) - .where(arrayContains(commercials.favoritesForUser, [user.email])); - return userFavorites; + .from(commercials_json) + .where(arrayContains(sql`${commercials_json.data}->>'favoritesForUser'`, [user.email])); + return userFavorites.map(l => ({ id: l.id, email: l.email, ...(l.data as CommercialPropertyListing) }) as CommercialPropertyListing); } // #### Find by imagePath ######################################## async findByImagePath(imagePath: string, serial: string): Promise { const result = await this.conn .select() - .from(commercials) - .where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`)); - return result[0] as CommercialPropertyListing; + .from(commercials_json) + .where(and(sql`(${commercials_json.data}->>'imagePath') = ${imagePath}`, sql`(${commercials_json.data}->>'serialId')::integer = ${serial}`)); + if (result.length > 0) { + return { id: result[0].id, email: result[0].email, ...(result[0].data as CommercialPropertyListing) } as CommercialPropertyListing; + } } // #### CREATE ######################################## async createListing(data: CommercialPropertyListing): Promise { @@ -163,10 +165,10 @@ export class CommercialPropertyService { data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date(); data.updated = new Date(); CommercialPropertyListingSchema.parse(data); - const convertedCommercialPropertyListing = data; - delete convertedCommercialPropertyListing.id; - const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning(); - return createdListing; + const { id, email, ...rest } = data; + const convertedCommercialPropertyListing = { email, data: rest }; + const [createdListing] = await this.conn.insert(commercials_json).values(convertedCommercialPropertyListing).returning(); + return { id: createdListing.id, email: createdListing.email, ...(createdListing.data as CommercialPropertyListing) }; } catch (error) { if (error instanceof ZodError) { const filteredErrors = error.errors @@ -183,7 +185,7 @@ export class CommercialPropertyService { // #### UPDATE CommercialProps ######################################## async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing, user: JwtUser): Promise { try { - const [existingListing] = await this.conn.select().from(commercials).where(eq(commercials.id, id)); + const [existingListing] = await this.conn.select().from(commercials_json).where(eq(commercials_json.id, id)); if (!existingListing) { throw new NotFoundException(`Business listing with id ${id} not found`); @@ -191,7 +193,7 @@ export class CommercialPropertyService { 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 || !user) { - data.favoritesForUser = existingListing.favoritesForUser; + data.favoritesForUser = (existingListing.data).favoritesForUser || []; } CommercialPropertyListingSchema.parse(data); const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId)); @@ -200,9 +202,10 @@ export class CommercialPropertyService { this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`); data.imageOrder = imageOrder; } - const convertedCommercialPropertyListing = data; - const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning(); - return updateListing; + const { id: _, email, ...rest } = data; + const convertedCommercialPropertyListing = { email, data: rest }; + const [updateListing] = await this.conn.update(commercials_json).set(convertedCommercialPropertyListing).where(eq(commercials_json.id, id)).returning(); + return { id: updateListing.id, email: updateListing.email, ...(updateListing.data as CommercialPropertyListing) }; } catch (error) { if (error instanceof ZodError) { const filteredErrors = error.errors @@ -220,7 +223,7 @@ export class CommercialPropertyService { // Images for commercial Properties // ############################################################## async deleteImage(imagePath: string, serial: string, name: string) { - const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing; + const listing = await this.findByImagePath(imagePath, serial); const index = listing.imageOrder.findIndex(im => im === name); if (index > -1) { listing.imageOrder.splice(index, 1); @@ -228,31 +231,21 @@ export class CommercialPropertyService { } } async addImage(imagePath: string, serial: string, imagename: string) { - const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing; + const listing = await this.findByImagePath(imagePath, serial); listing.imageOrder.push(imagename); await this.updateCommercialPropertyListing(listing.id, listing, null); } // #### DELETE ######################################## async deleteListing(id: string): Promise { - await this.conn.delete(commercials).where(eq(commercials.id, id)); + await this.conn.delete(commercials_json).where(eq(commercials_json.id, id)); } // #### DELETE Favorite ################################### async deleteFavorite(id: string, user: JwtUser): Promise { await this.conn - .update(commercials) + .update(commercials_json) .set({ - favoritesForUser: sql`array_remove(${commercials.favoritesForUser}, ${user.email})`, + data: sql`jsonb_set(${commercials_json.data}, '{favoritesForUser}', array_remove((${commercials_json.data}->>'favoritesForUser')::jsonb, ${user.email}))`, }) - .where(sql`${commercials.id} = ${id}`); + .where(eq(commercials_json.id, id)); } - // ############################################################## - // States - // ############################################################## - // async getStates(): Promise { - // return await this.conn - // .select({ state: commercials.state, count: sql`count(${commercials.id})`.mapWith(Number) }) - // .from(commercials) - // .groupBy(sql`${commercials.state}`) - // .orderBy(sql`count desc`); - // } } diff --git a/bizmatch-server/src/user/user.service.ts b/bizmatch-server/src/user/user.service.ts index 124bb69..4869e61 100644 --- a/bizmatch-server/src/user/user.service.ts +++ b/bizmatch-server/src/user/user.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { and, asc, count, desc, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm'; +import { and, asc, count, desc, eq, inArray, or, SQL, sql } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; @@ -9,7 +9,7 @@ import { FileService } from '../file/file.service'; import { GeoService } from '../geo/geo.service'; import { User, UserSchema } from '../models/db.model'; import { createDefaultUser, emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model'; -import { DrizzleUser, getDistanceQuery, splitName } from '../utils'; +import { getDistanceQuery, splitName } from '../utils'; type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number]; @Injectable() @@ -23,45 +23,45 @@ export class UserService { private getWhereConditions(criteria: UserListingCriteria): SQL[] { const whereConditions: SQL[] = []; - whereConditions.push(eq(schema.users.customerType, 'professional')); + whereConditions.push(sql`(${schema.users_json.data}->>'customerType') = 'professional'`); if (criteria.city && criteria.searchType === 'exact') { - whereConditions.push(sql`${schema.users.location}->>'name' ilike ${criteria.city.name}`); + whereConditions.push(sql`(${schema.users_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(schema.users, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); + whereConditions.push(sql`${getDistanceQuery(schema.users_json, cityGeo.latitude, 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[])); + whereConditions.push(inArray(sql`${schema.users_json.data}->>'customerSubType'`, criteria.types as CustomerSubType[])); } if (criteria.brokerName) { const { firstname, lastname } = splitName(criteria.brokerName); - whereConditions.push(or(ilike(schema.users.firstname, `%${firstname}%`), ilike(schema.users.lastname, `%${lastname}%`))); + whereConditions.push(sql`(${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`); } if (criteria.companyName) { - whereConditions.push(ilike(schema.users.companyName, `%${criteria.companyName}%`)); + whereConditions.push(sql`(${schema.users_json.data}->>'companyName') ILIKE ${`%${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}%`})`))); + whereConditions.push(or(...criteria.counties.map(county => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users_json.data}->'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})`); + whereConditions.push(sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users_json.data}->'areasServed') AS area WHERE area->>'state' = ${criteria.state})`); } //never show user which denied - whereConditions.push(eq(schema.users.showInDirectory, true)); + whereConditions.push(sql`(${schema.users_json.data}->>'showInDirectory')::boolean IS TRUE`); return whereConditions; } async searchUserListings(criteria: UserListingCriteria): Promise<{ results: User[]; totalCount: number }> { const start = criteria.start ? criteria.start : 0; const length = criteria.length ? criteria.length : 12; - const query = this.conn.select().from(schema.users); + const query = this.conn.select().from(schema.users_json); const whereConditions = this.getWhereConditions(criteria); if (whereConditions.length > 0) { @@ -71,10 +71,10 @@ export class UserService { // Sortierung switch (criteria.sortBy) { case 'nameAsc': - query.orderBy(asc(schema.users.lastname)); + query.orderBy(asc(sql`${schema.users_json.data}->>'lastname'`)); break; case 'nameDesc': - query.orderBy(desc(schema.users.lastname)); + query.orderBy(desc(sql`${schema.users_json.data}->>'lastname'`)); break; default: // Keine spezifische Sortierung, Standardverhalten kann hier eingefügt werden @@ -84,7 +84,7 @@ export class UserService { query.limit(length).offset(start); const data = await query; - const results = data; + const results = data.map(u => ({ id: u.id, email: u.email, ...(u.data as User) }) as User); const totalCount = await this.getUserListingsCount(criteria); return { @@ -93,7 +93,7 @@ export class UserService { }; } async getUserListingsCount(criteria: UserListingCriteria): Promise { - const countQuery = this.conn.select({ value: count() }).from(schema.users); + const countQuery = this.conn.select({ value: count() }).from(schema.users_json); const whereConditions = this.getWhereConditions(criteria); if (whereConditions.length > 0) { @@ -105,35 +105,29 @@ export class UserService { return totalCount; } async getUserByMail(email: string, jwtuser?: JwtUser) { - const users = (await this.conn - .select() - .from(schema.users) - .where(sql`email = ${email}`)) as User[]; + const users = await this.conn.select().from(schema.users_json).where(eq(schema.users_json.email, email)); if (users.length === 0) { const user: User = { id: undefined, customerType: 'professional', ...createDefaultUser(email, '', '', null) }; const u = await this.saveUser(user, false); return u; } else { - const user = users[0]; + const user = { id: users[0].id, email: users[0].email, ...(users[0].data as User) } as User; 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 users = await this.conn.select().from(schema.users_json).where(eq(schema.users_json.id, id)); - const user = users[0]; + const user = { id: users[0].id, email: users[0].email, ...(users[0].data as User) } as User; user.hasCompanyLogo = this.fileService.hasCompanyLogo(emailToDirName(user.email)); user.hasProfile = this.fileService.hasProfile(emailToDirName(user.email)); return user; } async getAllUser() { - const users = await this.conn.select().from(schema.users); - return users; + const users = await this.conn.select().from(schema.users_json); + return users.map(u => ({ id: u.id, email: u.email, ...(u.data as User) }) as User); } async saveUser(user: User, processValidation = true): Promise { try { @@ -148,13 +142,14 @@ export class UserService { validatedUser = UserSchema.parse(user); } //const drizzleUser = convertUserToDrizzleUser(validatedUser); - const drizzleUser = validatedUser as DrizzleUser; + const { id: _, ...rest } = validatedUser; + const drizzleUser = { email: user.email, data: rest }; 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; + const [updateUser] = await this.conn.update(schema.users_json).set(drizzleUser).where(eq(schema.users_json.id, user.id)).returning(); + return { id: updateUser.id, email: updateUser.email, ...(updateUser.data as User) } as User; } else { - const [newUser] = await this.conn.insert(schema.users).values(drizzleUser).returning(); - return newUser as User; + const [newUser] = await this.conn.insert(schema.users_json).values(drizzleUser).returning(); + return { id: newUser.id, email: newUser.email, ...(newUser.data as User) } as User; } } catch (error) { throw error; diff --git a/bizmatch-server/src/utils.ts b/bizmatch-server/src/utils.ts index 2b576dd..eb7cf52 100644 --- a/bizmatch-server/src/utils.ts +++ b/bizmatch-server/src/utils.ts @@ -1,5 +1,5 @@ import { sql } from 'drizzle-orm'; -import { businesses, commercials, users } from './drizzle/schema'; +import { businesses, businesses_json, commercials, commercials_json, users, users_json } from './drizzle/schema'; export const EARTH_RADIUS_KM = 6371; // Erdradius in Kilometern export const EARTH_RADIUS_MILES = 3959; // Erdradius in Meilen export function convertStringToNullUndefined(value) { @@ -16,21 +16,13 @@ export function convertStringToNullUndefined(value) { return value; } -export const getDistanceQuery = (schema: typeof businesses | typeof commercials | typeof users, lat: number, lon: number, unit: 'km' | 'miles' = 'miles') => { +export const getDistanceQuery = (schema: typeof businesses_json | typeof commercials_json | typeof users_json, lat: number, lon: number, unit: 'km' | 'miles' = 'miles') => { const radius = unit === 'km' ? EARTH_RADIUS_KM : EARTH_RADIUS_MILES; - - // return sql` - // ${radius} * 2 * ASIN(SQRT( - // POWER(SIN((${lat} - ${schema.latitude}) * PI() / 180 / 2), 2) + - // COS(${lat} * PI() / 180) * COS(${schema.latitude} * PI() / 180) * - // POWER(SIN((${lon} - ${schema.longitude}) * PI() / 180 / 2), 2) - // )) - // `; return sql` ${radius} * 2 * ASIN(SQRT( - POWER(SIN((${lat} - (${schema.location}->>'latitude')::float) * PI() / 180 / 2), 2) + - COS(${lat} * PI() / 180) * COS((${schema.location}->>'latitude')::float * PI() / 180) * - POWER(SIN((${lon} - (${schema.location}->>'longitude')::float) * PI() / 180 / 2), 2) + POWER(SIN((${lat} - (${schema.data}->'location'->>'latitude')::float) * PI() / 180 / 2), 2) + + COS(${lat} * PI() / 180) * COS((${schema.data}->'location'->>'latitude')::float * PI() / 180) * + POWER(SIN((${lon} - (${schema.data}->'location'->>'longitude')::float) * PI() / 180 / 2), 2) )) `; }; @@ -38,121 +30,7 @@ export const getDistanceQuery = (schema: typeof businesses | typeof commercials export type DrizzleUser = typeof users.$inferSelect; export type DrizzleBusinessListing = typeof businesses.$inferSelect; export type DrizzleCommercialPropertyListing = typeof commercials.$inferSelect; -// export function convertBusinessToDrizzleBusiness(businessListing: Partial): DrizzleBusinessListing { -// const drizzleBusinessListing = flattenObject(businessListing); -// drizzleBusinessListing.city = drizzleBusinessListing.name; -// delete drizzleBusinessListing.name; -// return drizzleBusinessListing; -// } -// export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial): BusinessListing { -// const o = { -// location: drizzleBusinessListing.city ? undefined : null, -// location_name: drizzleBusinessListing.city ? drizzleBusinessListing.city : undefined, -// location_state: drizzleBusinessListing.state ? drizzleBusinessListing.state : undefined, -// location_latitude: drizzleBusinessListing.latitude ? drizzleBusinessListing.latitude : undefined, -// location_longitude: drizzleBusinessListing.longitude ? drizzleBusinessListing.longitude : undefined, -// ...drizzleBusinessListing, -// }; -// Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); -// delete o.city; -// delete o.state; -// delete o.latitude; -// delete o.longitude; -// return unflattenObject(o); -// } -// export function convertCommercialToDrizzleCommercial(commercialPropertyListing: Partial): DrizzleCommercialPropertyListing { -// const drizzleCommercialPropertyListing = flattenObject(commercialPropertyListing); -// drizzleCommercialPropertyListing.city = drizzleCommercialPropertyListing.name; -// delete drizzleCommercialPropertyListing.name; -// return drizzleCommercialPropertyListing; -// } -// export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial): CommercialPropertyListing { -// const o = { -// location: drizzleCommercialPropertyListing.city ? undefined : null, -// location_name: drizzleCommercialPropertyListing.city ? drizzleCommercialPropertyListing.city : undefined, -// location_state: drizzleCommercialPropertyListing.state ? drizzleCommercialPropertyListing.state : undefined, -// location_street: drizzleCommercialPropertyListing.street ? drizzleCommercialPropertyListing.street : undefined, -// location_housenumber: drizzleCommercialPropertyListing.housenumber ? drizzleCommercialPropertyListing.housenumber : undefined, -// location_county: drizzleCommercialPropertyListing.county ? drizzleCommercialPropertyListing.county : undefined, -// location_zipCode: drizzleCommercialPropertyListing.zipCode ? drizzleCommercialPropertyListing.zipCode : undefined, -// location_latitude: drizzleCommercialPropertyListing.latitude ? drizzleCommercialPropertyListing.latitude : undefined, -// location_longitude: drizzleCommercialPropertyListing.longitude ? drizzleCommercialPropertyListing.longitude : undefined, -// ...drizzleCommercialPropertyListing, -// }; -// Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); -// delete o.city; -// delete o.state; -// delete o.street; -// delete o.housenumber; -// delete o.county; -// delete o.zipCode; -// delete o.latitude; -// delete o.longitude; -// return unflattenObject(o); -// } -// export function convertUserToDrizzleUser(user: Partial): DrizzleUser { -// const drizzleUser = flattenObject(user); -// drizzleUser.city = drizzleUser.name; -// delete drizzleUser.name; -// return drizzleUser; -// } -// export function convertDrizzleUserToUser(drizzleUser: Partial): User { -// const o: any = { -// companyLocation: drizzleUser.city ? undefined : null, -// companyLocation_name: drizzleUser.city ? drizzleUser.city : undefined, -// companyLocation_state: drizzleUser.state ? drizzleUser.state : undefined, -// companyLocation_latitude: drizzleUser.latitude ? drizzleUser.latitude : undefined, -// companyLocation_longitude: drizzleUser.longitude ? drizzleUser.longitude : undefined, -// ...drizzleUser, -// }; -// Object.keys(o).forEach(key => (o[key] === undefined ? delete o[key] : {})); -// delete o.city; -// delete o.state; -// delete o.latitude; -// delete o.longitude; - -// return unflattenObject(o); -// } -// function flattenObject(obj: any, res: any = {}): any { -// for (const key in obj) { -// if (obj.hasOwnProperty(key)) { -// const value = obj[key]; - -// if (typeof value === 'object' && value !== null && !Array.isArray(value)) { -// if (value instanceof Date) { -// res[key] = value; -// } else { -// flattenObject(value, res); -// } -// } else { -// res[key] = value; -// } -// } -// } -// return res; -// } -// function unflattenObject(obj: any, separator: string = '_'): any { -// const result: any = {}; - -// for (const key in obj) { -// if (obj.hasOwnProperty(key)) { -// const keys = key.split(separator); -// keys.reduce((acc, curr, idx) => { -// if (idx === keys.length - 1) { -// acc[curr] = obj[key]; -// } else { -// if (!acc[curr]) { -// acc[curr] = {}; -// } -// } -// return acc[curr]; -// }, result); -// } -// } - -// return result; -// } export function splitName(fullName: string): { firstname: string; lastname: string } { const parts = fullName.trim().split(/\s+/); // Teile den Namen am Leerzeichen auf