import { Inject, Injectable } from '@nestjs/common'; import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { BusinessListing, CommercialPropertyListing } from 'src/models/db.model.js'; import { Logger } from 'winston'; import * as schema from '../drizzle/schema.js'; import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js'; import { FileService } from '../file/file.service.js'; import { JwtUser, ListingCriteria, emailToDirName } from '../models/main.model.js'; @Injectable() export class ListingsService { constructor( @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(PG_CONNECTION) private conn: NodePgDatabase, private fileService: FileService, ) {} private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] { const conditions = []; if (criteria.type) { conditions.push(eq(table.type, criteria.type)); } if (criteria.state) { conditions.push(eq(table.state, criteria.state)); } if (criteria.minPrice) { conditions.push(gte(table.price, criteria.minPrice)); } if (criteria.maxPrice) { conditions.push(lte(table.price, criteria.maxPrice)); } if (criteria.realEstateChecked) { conditions.push(eq(businesses.realEstateIncluded, true)); } if (criteria.title) { conditions.push(ilike(table.title, `%${criteria.title}%`)); } return conditions; } // ############################################################## // Listings general // ############################################################## async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise { const start = criteria.start ? criteria.start : 0; const length = criteria.length ? criteria.length : 12; const conditions = this.getConditions(criteria, commercials, user); if (!user || (!user?.roles?.includes('ADMIN') ?? false)) { conditions.push(or(eq(commercials.draft, false), eq(commercials.imagePath, emailToDirName(user?.username)))); } const [data, total] = await Promise.all([ this.conn .select() .from(commercials) .where(and(...conditions)) .offset(start) .limit(length), this.conn .select({ count: sql`count(*)` }) .from(commercials) .where(and(...conditions)) .then(result => Number(result[0].count)), ]); return { total, data }; } async findBusinessListings(criteria: ListingCriteria, user: JwtUser): Promise { const start = criteria.start ? criteria.start : 0; const length = criteria.length ? criteria.length : 12; const conditions = this.getConditions(criteria, businesses, user); if (!user || (!user?.roles?.includes('ADMIN') ?? false)) { conditions.push(or(eq(businesses.draft, false), eq(businesses.imageName, emailToDirName(user?.username)))); } const [data, total] = await Promise.all([ this.conn .select() .from(businesses) .where(and(...conditions)) .offset(start) .limit(length), this.conn .select({ count: sql`count(*)` }) .from(businesses) .where(and(...conditions)) .then(result => Number(result[0].count)), ]); return { total, data }; } async findCommercialPropertiesById(id: string, user: JwtUser): Promise { let result = await this.conn .select() .from(commercials) .where(and(sql`${commercials.id} = ${id}`)); result = result.filter(r => !r.draft || r.imagePath === emailToDirName(user?.username) || user?.roles.includes('ADMIN')); return result[0] as CommercialPropertyListing; } async findBusinessesById(id: string, user: JwtUser): Promise { let result = await this.conn .select() .from(businesses) .where(and(sql`${businesses.id} = ${id}`)); result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN')); return result[0] as BusinessListing; } async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise { const conditions = []; conditions.push(eq(commercials.imagePath, emailToDirName(email))); if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) { conditions.push(ne(commercials.draft, true)); } return (await this.conn .select() .from(commercials) .where(and(...conditions))) as CommercialPropertyListing[]; } async findBusinessesByEmail(email: string, user: JwtUser): Promise { const conditions = []; conditions.push(eq(businesses.imageName, emailToDirName(email))); if (email !== user?.username && (!user?.roles?.includes('ADMIN') ?? false)) { conditions.push(ne(businesses.draft, true)); } return (await this.conn .select() .from(businesses) .where(and(...conditions))) as CommercialPropertyListing[]; } 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; } async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise { data.created = new Date(); data.updated = new Date(); const [createdListing] = await this.conn.insert(table).values(data).returning(); return createdListing as BusinessListing | CommercialPropertyListing; } async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise { data.updated = new Date(); data.created = new Date(data.created); const imageOrder = await this.fileService.getPropertyImages(data.imagePath, String(data.serialId)); let difference = imageOrder.filter(x => !data.imageOrder.includes(x)).concat(data.imageOrder.filter(x => !imageOrder.includes(x))); if (difference.length > 0) { this.logger.warn(`changes between image directory and imageOrder in listing ${data.serialId}: ${difference.join(',')}`); data.imageOrder = imageOrder; } const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning(); return updateListing as BusinessListing | CommercialPropertyListing; } async updateBusinessListing(id: string, data: BusinessListing): Promise { data.updated = new Date(); data.created = new Date(data.created); const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning(); return updateListing as BusinessListing | CommercialPropertyListing; } async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise { await this.conn.delete(table).where(eq(table.id, id)); } async getStates(table: typeof businesses | typeof commercials): Promise { return await this.conn .select({ state: table.state, count: sql`count(${table.id})`.mapWith(Number) }) .from(table) .groupBy(sql`${table.state}`) .orderBy(sql`count desc`); } // ############################################################## // Images for commercial Properties // ############################################################## async deleteImage(imagePath: string, serial: string, name: string) { const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing; const index = listing.imageOrder.findIndex(im => im === name); if (index > -1) { listing.imageOrder.splice(index, 1); await this.updateCommercialPropertyListing(listing.id, listing); } } async addImage(imagePath: string, serial: string, imagename: string) { const listing = (await this.findByImagePath(imagePath, serial)) as unknown as CommercialPropertyListing; listing.imageOrder.push(imagename); await this.updateCommercialPropertyListing(listing.id, listing); } }