import { Inject, Injectable } from '@nestjs/common'; import { BusinessListing, CommercialPropertyListing, ListingCriteria, ListingType, ImageProperty, ListingCategory, ResponseBusinessListing } from '../models/main.model.js'; import { convertStringToNullUndefined } from '../utils.js'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; import { EntityData, EntityId, Schema, SchemaDefinition } from 'redis-om'; import { SQL, eq, ilike, sql } from 'drizzle-orm'; import { BusinessesJson, PG_CONNECTION, businesses_json, commercials_json } from '../drizzle/schema.js'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import * as schema from '../drizzle/schema.js'; import { PgTableFn, PgTableWithColumns, QueryBuilder } from 'drizzle-orm/pg-core'; @Injectable() export class ListingsService { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(PG_CONNECTION) private conn: NodePgDatabase,) { } private buildWhereClause(criteria: ListingCriteria):SQL { const finalSql = sql`1=1`; finalSql.append(criteria.type?sql` AND data->>'type' = ${criteria.type}` : sql``) finalSql.append(criteria.state ? sql` AND data->>'state' = ${criteria.state}` : sql``) finalSql.append(criteria.minPrice ? sql` AND CAST(data->>'price' AS NUMERIC) >= ${parseFloat(criteria.minPrice)}` : sql``) finalSql.append(criteria.maxPrice ? sql` AND CAST(data->>'price' AS NUMERIC) < ${parseFloat(criteria.maxPrice)}` : sql``) finalSql.append(criteria.realEstateChecked !== undefined ? sql` AND CAST(data->>'realEstateIncluded' AS BOOLEAN) = ${criteria.realEstateChecked}` : sql``) finalSql.append(criteria.title ? sql` AND LOWER(data->>'title') LIKE LOWER('%' || ${criteria.title} || '%')` : sql``) return finalSql } // ############################################################## // Listings general // ############################################################## private async findListings(table: typeof businesses_json | typeof commercials_json, criteria: ListingCriteria ,start = 0, length = 12): Promise { const whereClause = this.buildWhereClause(criteria) const [data, total] = await Promise.all([ (await this.conn.select({ id:table.id, data: table.data }).from(table).where(whereClause).offset(start).limit(length)).map(e=>{ const ret = e.data as any ret.id = e.id return ret }), this.conn.select({ count: sql`count(*)` }).from(table).where(whereClause).then((result) => Number(result[0].count)), ]); return { total, data }; } async findById(id: string, table: typeof businesses_json | typeof commercials_json): Promise { const result = await this.conn.select({ data: table.data }).from(table).where(sql`${table.id} = ${id}`) return result[0].data as BusinessListing } async findListingsByCriteria(criteria: ListingCriteria, table: typeof businesses_json | typeof commercials_json): Promise<{ data: Record[]; total: number }> { const start = criteria.start ? criteria.start : 0; const length = criteria.length ? criteria.length : 12; return await this.findListings(table, criteria, start, length) } async findByPriceRange(minPrice: number, maxPrice: number, table: typeof businesses_json | typeof commercials_json): Promise { return this.conn.select().from(table).where(sql`${table.data}->>'price' BETWEEN ${minPrice} AND ${maxPrice}`); } async findByState(state: string, table: typeof businesses_json | typeof commercials_json): Promise { return this.conn.select().from(table).where(sql`${table.data}->>'state' = ${state}`); } async findByUserId(userId: string, table: typeof businesses_json | typeof commercials_json): Promise { return this.conn.select().from(table).where(sql`${table.data}->>'userId' = ${userId}`); } async findByTitleContains(title: string, table: typeof businesses_json | typeof commercials_json): Promise { return this.conn.select().from(table).where(sql`${table.data}->>'title' ILIKE '%' || ${title} || '%'`); } async createListing(data: BusinessListing, table: typeof businesses_json | typeof commercials_json): Promise { const newListing = { data, created: data.created, updated: data.updated, visits: 0, last_visit: null } const [createdListing] = await this.conn.insert(table).values(newListing).returning(); return createdListing as BusinessesJson; } async updateListing(id: string, data: BusinessListing | CommercialPropertyListing, table: typeof businesses_json | typeof commercials_json): Promise { const [updateListing] = await this.conn.update(table).set(data).where(eq(table.id, id)).returning(); return updateListing as BusinessesJson; } async deleteListing(id: string, table: typeof businesses_json | typeof commercials_json): Promise { await this.conn.delete(table).where(eq(table.id, id)); } // ############################################################## // Images for commercial Properties // ############################################################## async updateImageOrder(id: string, imageOrder: ImageProperty[]) { const listing = await this.findById(id, commercials_json) as unknown as CommercialPropertyListing listing.imageOrder = imageOrder; await this.updateListing(listing.id, listing, commercials_json) } async deleteImage(id: string, name: string,) { const listing = await this.findById(id, commercials_json) as unknown as CommercialPropertyListing const index = listing.imageOrder.findIndex(im => im.name === name); if (index > -1) { listing.imageOrder.splice(index, 1); await this.updateListing(listing.id, listing, commercials_json) } } async addImage(id: string, imagename: string) { const listing = await this.findById(id, commercials_json) as unknown as CommercialPropertyListing listing.imageOrder.push({ name: imagename, code: '', id: '' }); await this.updateListing(listing.id, listing, commercials_json) } // async getCommercialPropertyListingById(id: string): Promise{ // return await this.commercialPropertyListingRepository.fetch(id) as unknown as CommercialPropertyListing; // } // async getBusinessListingById(id: string) { // return await this.businessListingRepository.fetch(id) // } // async getBusinessListingByUserId(userid:string){ // return await this.businessListingRepository.search().where('userId').equals(userid).return.all() // } // async deleteBusinessListing(id: string){ // return await this.businessListingRepository.remove(id); // } // async deleteCommercialPropertyListing(id: string){ // return await this.commercialPropertyListingRepository.remove(id); // } // async getAllBusinessListings(start?: number, end?: number) { // return await this.businessListingRepository.search().return.all() // } // async getAllCommercialListings(start?: number, end?: number) { // return await this.commercialPropertyListingRepository.search().return.all() // } // async findBusinessListings(criteria:ListingCriteria): Promise { // // let listings = await this.getAllBusinessListings(); // // return this.find(criteria,listings); // const from=criteria.start?criteria.start:0 // const size=criteria.length?criteria.length:24 // this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`); // const result = await this.redis.ft.search('business:index','*',{LIMIT:{from,size}}); // this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`); // return result // } // async findCommercialPropertyListings(criteria:ListingCriteria): Promise { // let listings = await this.getAllCommercialListings(); // return this.find(criteria,listings); // } // async deleteAllBusinessListings(){ // const ids = await this.getIdsForRepo(this.schemaNameBusiness.name); // this.businessListingRepository.remove(ids); // } // async deleteAllcommercialListings(){ // const ids = await this.getIdsForRepo(this.schemaNameCommercial.name); // this.commercialPropertyListingRepository.remove(ids); // } // async getIdsForRepo(repoName:string, maxcount=100000){ // let cursor = 0; // let ids = []; // do { // const reply = await this.redis.scan(cursor, { // MATCH: `${repoName}:*`, // COUNT: maxcount // }); // cursor = reply.cursor; // // Extrahiere die ID aus jedem Schlüssel und füge sie zur Liste hinzu // ids = ids.concat(reply.keys.map(key => key.split(':')[1]).filter(id=>id!='index')); // } while (cursor !== 0); // return ids; // } // async find(criteria:ListingCriteria, listings: any[]): Promise { // listings=listings.filter(l=>l.listingsCategory===criteria.listingsCategory); // if (convertStringToNullUndefined(criteria.type)){ // console.log(criteria.type); // listings=listings.filter(l=>l.type===criteria.type); // } // if (convertStringToNullUndefined(criteria.state)){ // console.log(criteria.state); // listings=listings.filter(l=>l.state===criteria.state); // } // if (convertStringToNullUndefined(criteria.minPrice)){ // console.log(criteria.minPrice); // listings=listings.filter(l=>l.price>=Number(criteria.minPrice)); // } // if (convertStringToNullUndefined(criteria.maxPrice)){ // console.log(criteria.maxPrice); // listings=listings.filter(l=>l.price<=Number(criteria.maxPrice)); // } // if (convertStringToNullUndefined(criteria.realEstateChecked)){ // console.log(criteria.realEstateChecked); // listings=listings.filter(l=>l.realEstateIncluded); // } // if (convertStringToNullUndefined(criteria.category)){ // console.log(criteria.category); // listings=listings.filter(l=>l.category===criteria.category); // } // return listings // } }