umstellung auf json Tabellen ...

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

View File

@ -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'),
});

View File

@ -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<typeof schema>,
) {}
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();
}
}

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));
}
}

View File

@ -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<any> {
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<number> {
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<CommercialPropertyListing> {
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<CommercialPropertyListing[]> {
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<CommercialPropertyListing[]> {
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<CommercialPropertyListing> {
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<CommercialPropertyListing> {
@ -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<CommercialPropertyListing> {
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 = (<CommercialPropertyListing>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<void> {
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<void> {
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<any[]> {
// return await this.conn
// .select({ state: commercials.state, count: sql<number>`count(${commercials.id})`.mapWith(Number) })
// .from(commercials)
// .groupBy(sql`${commercials.state}`)
// .orderBy(sql`count desc`);
// }
}

View File

@ -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<number> {
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<User> {
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;

View File

@ -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<BusinessListing>): DrizzleBusinessListing {
// const drizzleBusinessListing = flattenObject(businessListing);
// drizzleBusinessListing.city = drizzleBusinessListing.name;
// delete drizzleBusinessListing.name;
// return drizzleBusinessListing;
// }
// export function convertDrizzleBusinessToBusiness(drizzleBusinessListing: Partial<DrizzleBusinessListing>): 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<CommercialPropertyListing>): DrizzleCommercialPropertyListing {
// const drizzleCommercialPropertyListing = flattenObject(commercialPropertyListing);
// drizzleCommercialPropertyListing.city = drizzleCommercialPropertyListing.name;
// delete drizzleCommercialPropertyListing.name;
// return drizzleCommercialPropertyListing;
// }
// export function convertDrizzleCommercialToCommercial(drizzleCommercialPropertyListing: Partial<DrizzleCommercialPropertyListing>): 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<User>): DrizzleUser {
// const drizzleUser = flattenObject(user);
// drizzleUser.city = drizzleUser.name;
// delete drizzleUser.name;
// return drizzleUser;
// }
// export function convertDrizzleUserToUser(drizzleUser: Partial<DrizzleUser>): 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