Paginator & SQL Querries where clauses & city search
This commit is contained in:
@@ -7,10 +7,10 @@ import { join } from 'path';
|
||||
import pkg from 'pg';
|
||||
import { rimraf } from 'rimraf';
|
||||
import sharp from 'sharp';
|
||||
import { SelectOptionsService } from 'src/select-options/select-options.service.js';
|
||||
import winston from 'winston';
|
||||
import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js';
|
||||
import { emailToDirName, KeyValueStyle } from '../models/main.model.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import * as schema from './schema.js';
|
||||
const typesOfBusiness: Array<KeyValueStyle> = [
|
||||
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||
|
||||
@@ -1,40 +1,41 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
import { City, Geo, State } from 'src/models/server.model.js';
|
||||
import { GeoResult } from 'src/models/main.model.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { City, Geo, State } from '../models/server.model.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class GeoService {
|
||||
geo:Geo;
|
||||
constructor() {
|
||||
this.loadGeo();
|
||||
}
|
||||
private loadGeo(): void {
|
||||
const filePath = join(__dirname,'../..', 'assets', 'geo.json');
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
this.geo = JSON.parse(rawData);
|
||||
}
|
||||
|
||||
findCitiesStartingWith( prefix: string, state?:string): { city: string; state: string; state_code: string }[] {
|
||||
const result: { city: string; state: string; state_code: string }[] = [];
|
||||
|
||||
this.geo.states.forEach((state: State) => {
|
||||
state.cities.forEach((city: City) => {
|
||||
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
result.push({
|
||||
city: city.name,
|
||||
state: state.name,
|
||||
state_code: state.state_code
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return state ? result.filter(e=>e.state_code.toLowerCase()===state.toLowerCase()) :result;
|
||||
}
|
||||
}
|
||||
geo: Geo;
|
||||
constructor() {
|
||||
this.loadGeo();
|
||||
}
|
||||
private loadGeo(): void {
|
||||
const filePath = join(__dirname, '../..', 'assets', 'geo.json');
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
this.geo = JSON.parse(rawData);
|
||||
}
|
||||
|
||||
findCitiesStartingWith(prefix: string, state?: string): { city: string; state: string; state_code: string }[] {
|
||||
const result: GeoResult[] = [];
|
||||
|
||||
this.geo.states.forEach((state: State) => {
|
||||
state.cities.forEach((city: City) => {
|
||||
if (city.name.toLowerCase().startsWith(prefix.toLowerCase())) {
|
||||
result.push({
|
||||
id: city.id,
|
||||
city: city.name,
|
||||
state: state.name,
|
||||
state_code: state.state_code,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return state ? result.filter(e => e.state_code.toLowerCase() === state.toLowerCase()) : result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { ListingsService } from '../listings/listings.service.js';
|
||||
import { CommercialPropertyService } from '../listings/commercial-property.service.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
|
||||
@Controller('image')
|
||||
export class ImageController {
|
||||
constructor(
|
||||
private fileService: FileService,
|
||||
private listingService: ListingsService,
|
||||
private listingService: CommercialPropertyService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
private selectOptions: SelectOptionsService,
|
||||
) {}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { ListingsModule } from '../listings/listings.module.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { ImageController } from './image.controller.js';
|
||||
import { ImageService } from './image.service.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { ListingsService } from '../listings/listings.service.js';
|
||||
import { ListingsModule } from '../listings/listings.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [ListingsModule],
|
||||
controllers: [ImageController],
|
||||
providers: [ImageService,FileService,SelectOptionsService]
|
||||
providers: [ImageService, FileService, SelectOptionsService],
|
||||
})
|
||||
export class ImageModule {}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put } from '@nestjs/common';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { convertStringToNullUndefined } from '../utils.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
import { Body, Controller, Inject, Post } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { UserListingCriteria } from 'src/models/main.model.js';
|
||||
import { Logger } from 'winston';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
|
||||
@Controller('listings/professionals_brokers')
|
||||
export class BrokerListingsController {
|
||||
|
||||
constructor(private readonly userService:UserService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
}
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@Post('search')
|
||||
find(@Body() criteria: any): any {
|
||||
return this.userService.findUser(criteria);
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
return this.userService.searchUserListings(criteria);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
192
bizmatch-server/src/listings/business-listing.service.ts
Normal file
192
bizmatch-server/src/listings/business-listing.service.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, eq, gte, ilike, inArray, lte, ne, 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 * as schema from '../drizzle/schema.js';
|
||||
import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
|
||||
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
|
||||
@Injectable()
|
||||
export class BusinessListingService {
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private fileService: FileService,
|
||||
) {}
|
||||
|
||||
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city) {
|
||||
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
|
||||
}
|
||||
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
whereConditions.push(inArray(businesses.type, criteria.types));
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(eq(businesses.state, criteria.state));
|
||||
}
|
||||
|
||||
if (criteria.county) {
|
||||
whereConditions.push(ilike(businesses.city, `%${criteria.county}%`)); // Assuming county is part of city, adjust if necessary
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
whereConditions.push(gte(businesses.price, criteria.minPrice));
|
||||
}
|
||||
|
||||
if (criteria.maxPrice) {
|
||||
whereConditions.push(lte(businesses.price, criteria.maxPrice));
|
||||
}
|
||||
|
||||
if (criteria.minRevenue) {
|
||||
whereConditions.push(gte(businesses.salesRevenue, criteria.minRevenue));
|
||||
}
|
||||
|
||||
if (criteria.maxRevenue) {
|
||||
whereConditions.push(lte(businesses.salesRevenue, criteria.maxRevenue));
|
||||
}
|
||||
|
||||
if (criteria.minCashFlow) {
|
||||
whereConditions.push(gte(businesses.cashFlow, criteria.minCashFlow));
|
||||
}
|
||||
|
||||
if (criteria.maxCashFlow) {
|
||||
whereConditions.push(lte(businesses.cashFlow, criteria.maxCashFlow));
|
||||
}
|
||||
|
||||
if (criteria.minNumberEmployees) {
|
||||
whereConditions.push(gte(businesses.employees, criteria.minNumberEmployees));
|
||||
}
|
||||
|
||||
if (criteria.maxNumberEmployees) {
|
||||
whereConditions.push(lte(businesses.employees, criteria.maxNumberEmployees));
|
||||
}
|
||||
|
||||
if (criteria.establishedSince) {
|
||||
whereConditions.push(gte(businesses.established, criteria.establishedSince));
|
||||
}
|
||||
|
||||
if (criteria.establishedUntil) {
|
||||
whereConditions.push(lte(businesses.established, criteria.establishedUntil));
|
||||
}
|
||||
|
||||
if (criteria.realEstateChecked) {
|
||||
whereConditions.push(eq(businesses.realEstateIncluded, criteria.realEstateChecked));
|
||||
}
|
||||
|
||||
if (criteria.leasedLocation) {
|
||||
whereConditions.push(eq(businesses.leasedLocation, criteria.leasedLocation));
|
||||
}
|
||||
|
||||
if (criteria.franchiseResale) {
|
||||
whereConditions.push(eq(businesses.franchiseResale, criteria.franchiseResale));
|
||||
}
|
||||
|
||||
if (criteria.title) {
|
||||
whereConditions.push(or(ilike(businesses.title, `%${criteria.title}%`), ilike(businesses.description, `%${criteria.title}%`)));
|
||||
}
|
||||
|
||||
if (criteria.brokerName) {
|
||||
whereConditions.push(or(ilike(schema.users.firstname, `%${criteria.brokerName}%`), ilike(schema.users.lastname, `%${criteria.brokerName}%`)));
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
.from(businesses)
|
||||
.leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
||||
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
|
||||
// Paginierung
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const data = await query;
|
||||
const totalCount = await this.getBusinessListingsCount(criteria);
|
||||
const results = data.map(r => r.business);
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
|
||||
async getBusinessListingsCount(criteria: BusinessListingCriteria): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(businesses).leftJoin(schema.users, eq(businesses.email, schema.users.email));
|
||||
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
countQuery.where(whereClause);
|
||||
}
|
||||
|
||||
const [{ value: totalCount }] = await countQuery;
|
||||
return totalCount;
|
||||
}
|
||||
async findBusinessesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
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 findBusinessesByEmail(email: string, user: JwtUser): Promise<BusinessListing[]> {
|
||||
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[];
|
||||
}
|
||||
// #### CREATE ########################################
|
||||
async createListing(data: BusinessListing): Promise<BusinessListing> {
|
||||
data.created = new Date();
|
||||
data.updated = new Date();
|
||||
const [createdListing] = await this.conn.insert(businesses).values(data).returning();
|
||||
return createdListing as BusinessListing;
|
||||
}
|
||||
// #### UPDATE Business ########################################
|
||||
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||
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;
|
||||
}
|
||||
// #### DELETE ########################################
|
||||
async deleteListing(id: string): Promise<void> {
|
||||
await this.conn.delete(businesses).where(eq(businesses.id, id));
|
||||
}
|
||||
// ##############################################################
|
||||
// States
|
||||
// ##############################################################
|
||||
async getStates(): Promise<any[]> {
|
||||
return await this.conn
|
||||
.select({ state: businesses.state, count: sql<number>`count(${businesses.id})`.mapWith(Number) })
|
||||
.from(businesses)
|
||||
.groupBy(sql`${businesses.state}`)
|
||||
.orderBy(sql`count desc`);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { businesses } from '../drizzle/schema.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
import { BusinessListingService } from './business-listing.service.js';
|
||||
|
||||
@Controller('listings/business')
|
||||
export class BusinessListingsController {
|
||||
constructor(
|
||||
private readonly listingsService: ListingsService,
|
||||
private readonly listingsService: BusinessListingService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@@ -28,19 +27,19 @@ export class BusinessListingsController {
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('find')
|
||||
find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
|
||||
return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('search')
|
||||
search(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
return this.listingsService.searchBusinessListings(criteria.prompt);
|
||||
}
|
||||
// @UseGuards(OptionalJwtAuthGuard)
|
||||
// @Post('search')
|
||||
// search(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
// return this.listingsService.searchBusinessListings(criteria.prompt);
|
||||
// }
|
||||
|
||||
@Post()
|
||||
create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return this.listingsService.createListing(listing, businesses);
|
||||
return this.listingsService.createListing(listing);
|
||||
}
|
||||
@Put()
|
||||
update(@Body() listing: any) {
|
||||
@@ -49,10 +48,10 @@ export class BusinessListingsController {
|
||||
}
|
||||
@Delete(':id')
|
||||
deleteById(@Param('id') id: string) {
|
||||
this.listingsService.deleteListing(id, businesses);
|
||||
this.listingsService.deleteListing(id);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates(businesses);
|
||||
return this.listingsService.getStates();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { commercials } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { CommercialPropertyListing } from '../models/db.model';
|
||||
import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
import { CommercialPropertyService } from './commercial-property.service.js';
|
||||
|
||||
@Controller('listings/commercialProperty')
|
||||
export class CommercialPropertyListingsController {
|
||||
constructor(
|
||||
private readonly listingsService: ListingsService,
|
||||
private readonly listingsService: CommercialPropertyService,
|
||||
private fileService: FileService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
@@ -30,16 +29,16 @@ export class CommercialPropertyListingsController {
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('find')
|
||||
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
|
||||
return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
|
||||
return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates(commercials);
|
||||
return this.listingsService.getStates();
|
||||
}
|
||||
@Post()
|
||||
async create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return await this.listingsService.createListing(listing, commercials);
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
@Put()
|
||||
async update(@Body() listing: any) {
|
||||
@@ -48,7 +47,7 @@ export class CommercialPropertyListingsController {
|
||||
}
|
||||
@Delete(':id/:imagePath')
|
||||
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||
this.listingsService.deleteListing(id, commercials);
|
||||
this.listingsService.deleteListing(id);
|
||||
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
168
bizmatch-server/src/listings/commercial-property.service.ts
Normal file
168
bizmatch-server/src/listings/commercial-property.service.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, count, eq, gte, ilike, inArray, lte, ne, 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 * as schema from '../drizzle/schema.js';
|
||||
import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
|
||||
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
|
||||
@Injectable()
|
||||
export class CommercialPropertyService {
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private fileService: FileService,
|
||||
) {}
|
||||
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city) {
|
||||
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
|
||||
}
|
||||
|
||||
if (criteria.types && criteria.types.length > 0) {
|
||||
whereConditions.push(inArray(schema.commercials.type, criteria.types));
|
||||
}
|
||||
|
||||
if (criteria.state) {
|
||||
whereConditions.push(eq(schema.commercials.state, criteria.state));
|
||||
}
|
||||
|
||||
if (criteria.county) {
|
||||
whereConditions.push(ilike(schema.commercials.county, `%${criteria.county}%`));
|
||||
}
|
||||
|
||||
if (criteria.minPrice) {
|
||||
whereConditions.push(gte(schema.commercials.price, criteria.minPrice));
|
||||
}
|
||||
|
||||
if (criteria.maxPrice) {
|
||||
whereConditions.push(lte(schema.commercials.price, criteria.maxPrice));
|
||||
}
|
||||
|
||||
if (criteria.title) {
|
||||
whereConditions.push(or(ilike(schema.commercials.title, `%${criteria.title}%`), ilike(schema.commercials.description, `%${criteria.title}%`)));
|
||||
}
|
||||
|
||||
return whereConditions;
|
||||
}
|
||||
// #### Find by criteria ########################################
|
||||
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().from(schema.commercials);
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
|
||||
// Paginierung
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const results = await query;
|
||||
const totalCount = await this.getCommercialPropertiesCount(criteria);
|
||||
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
async getCommercialPropertiesCount(criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(schema.commercials);
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
countQuery.where(whereClause);
|
||||
}
|
||||
|
||||
const [{ value: totalCount }] = await countQuery;
|
||||
return totalCount;
|
||||
}
|
||||
|
||||
// #### Find by ID ########################################
|
||||
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
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;
|
||||
}
|
||||
|
||||
// #### Find by User EMail ########################################
|
||||
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
||||
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[];
|
||||
}
|
||||
// #### 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;
|
||||
}
|
||||
// #### CREATE ########################################
|
||||
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
|
||||
data.created = new Date();
|
||||
data.updated = new Date();
|
||||
const [createdListing] = await this.conn.insert(commercials).values(data).returning();
|
||||
return createdListing as CommercialPropertyListing;
|
||||
}
|
||||
// #### UPDATE CommercialProps ########################################
|
||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
|
||||
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;
|
||||
}
|
||||
// ##############################################################
|
||||
// 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);
|
||||
}
|
||||
// #### DELETE ########################################
|
||||
async deleteListing(id: string): Promise<void> {
|
||||
await this.conn.delete(commercials).where(eq(commercials.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`);
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,15 @@ import { UserService } from '../user/user.service.js';
|
||||
import { BrokerListingsController } from './broker-listings.controller.js';
|
||||
import { BusinessListingsController } from './business-listings.controller.js';
|
||||
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
|
||||
import { BusinessListingService } from './business-listing.service.js';
|
||||
import { CommercialPropertyService } from './commercial-property.service.js';
|
||||
import { UnknownListingsController } from './unknown-listings.controller.js';
|
||||
|
||||
@Module({
|
||||
imports: [DrizzleModule, AuthModule],
|
||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||
providers: [ListingsService, FileService, UserService],
|
||||
exports: [ListingsService],
|
||||
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService],
|
||||
exports: [BusinessListingService, CommercialPropertyService],
|
||||
})
|
||||
export class ListingsModule {}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, eq, gte, ilike, inArray, lte, ne, or, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import OpenAI from 'openai';
|
||||
import { Logger } from 'winston';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { businesses, commercials, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { BusinessListing, CommercialPropertyListing } from '../models/db.model';
|
||||
import { BusinessListingCriteria, CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
|
||||
@Injectable()
|
||||
export class ListingsService {
|
||||
openai: OpenAI;
|
||||
constructor(
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private fileService: FileService,
|
||||
) {
|
||||
this.openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen
|
||||
});
|
||||
}
|
||||
private getConditions(criteria: BusinessListingCriteria | CommercialPropertyListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
|
||||
const conditions = [];
|
||||
if (criteria.types?.length > 0) {
|
||||
conditions.push(inArray(table.type, criteria.types));
|
||||
}
|
||||
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.title) {
|
||||
conditions.push(ilike(table.title, `%${criteria.title}%`));
|
||||
}
|
||||
return conditions;
|
||||
}
|
||||
// ##############################################################
|
||||
// Listings general
|
||||
// ##############################################################
|
||||
|
||||
// #### Find by embeddng ########################################
|
||||
async searchBusinessListings(query: string, limit: number = 20): Promise<BusinessListing[]> {
|
||||
const queryEmbedding = await this.createEmbedding(query);
|
||||
|
||||
const results = await this.conn
|
||||
.select()
|
||||
.from(businesses)
|
||||
.orderBy(sql`embedding <-> ${JSON.stringify(queryEmbedding)}`)
|
||||
.limit(limit);
|
||||
|
||||
return results as BusinessListing[];
|
||||
}
|
||||
// #### Find by criteria ########################################
|
||||
async findCommercialPropertyListings(criteria: CommercialPropertyListingCriteria, user: JwtUser): Promise<any> {
|
||||
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: BusinessListingCriteria, user: JwtUser): Promise<any> {
|
||||
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 };
|
||||
}
|
||||
|
||||
// #### Find by ID ########################################
|
||||
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||
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<CommercialPropertyListing> {
|
||||
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;
|
||||
}
|
||||
|
||||
// #### Find by User EMail ########################################
|
||||
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
||||
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<BusinessListing[]> {
|
||||
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[];
|
||||
}
|
||||
|
||||
// #### 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;
|
||||
}
|
||||
|
||||
// #### CREATE ########################################
|
||||
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
||||
data.created = new Date();
|
||||
data.updated = new Date();
|
||||
const [createdListing] = await this.conn.insert(table).values(data).returning();
|
||||
return createdListing as BusinessListing | CommercialPropertyListing;
|
||||
}
|
||||
// #### UPDATE CommercialProps ########################################
|
||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||
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;
|
||||
}
|
||||
// #### UPDATE Business ########################################
|
||||
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||
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;
|
||||
}
|
||||
// #### DELETE ########################################
|
||||
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
|
||||
await this.conn.delete(table).where(eq(table.id, id));
|
||||
}
|
||||
|
||||
// ##############################################################
|
||||
// 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);
|
||||
}
|
||||
// ##############################################################
|
||||
// States
|
||||
// ##############################################################
|
||||
async getStates(table: typeof businesses | typeof commercials): Promise<any[]> {
|
||||
return await this.conn
|
||||
.select({ state: table.state, count: sql<number>`count(${table.id})`.mapWith(Number) })
|
||||
.from(table)
|
||||
.groupBy(sql`${table.state}`)
|
||||
.orderBy(sql`count desc`);
|
||||
}
|
||||
// ##############################################################
|
||||
// Embedding
|
||||
// ##############################################################
|
||||
async createEmbedding(text: string): Promise<number[]> {
|
||||
const response = await this.openai.embeddings.create({
|
||||
model: 'text-embedding-3-small',
|
||||
input: text,
|
||||
});
|
||||
return response.data[0].embedding;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Controller, Inject } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
|
||||
@Controller('listings/undefined')
|
||||
export class UnknownListingsController {
|
||||
constructor(
|
||||
private readonly listingsService: ListingsService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
|
||||
// @Get(':id')
|
||||
// async findById(@Param('id') id: string): Promise<any> {
|
||||
|
||||
@@ -36,32 +36,32 @@ export type ListingCategory = {
|
||||
export type ListingType = BusinessListing | CommercialPropertyListing;
|
||||
|
||||
export type ResponseBusinessListingArray = {
|
||||
data: BusinessListing[];
|
||||
total: number;
|
||||
results: BusinessListing[];
|
||||
totalCount: number;
|
||||
};
|
||||
export type ResponseBusinessListing = {
|
||||
data: BusinessListing;
|
||||
};
|
||||
export type ResponseCommercialPropertyListingArray = {
|
||||
data: CommercialPropertyListing[];
|
||||
total: number;
|
||||
results: CommercialPropertyListing[];
|
||||
totalCount: number;
|
||||
};
|
||||
export type ResponseCommercialPropertyListing = {
|
||||
data: CommercialPropertyListing;
|
||||
};
|
||||
export type ResponseUsersArray = {
|
||||
data: User[];
|
||||
total: number;
|
||||
results: User[];
|
||||
totalCount: number;
|
||||
};
|
||||
export interface ListCriteria {
|
||||
start: number;
|
||||
length: number;
|
||||
page: number;
|
||||
pageCount: number;
|
||||
city: string;
|
||||
types: string[];
|
||||
city: string;
|
||||
prompt: string;
|
||||
criteriaType: 'business' | 'commercialProperty' | 'user';
|
||||
criteriaType: 'business' | 'commercialProperty' | 'broker';
|
||||
}
|
||||
export interface BusinessListingCriteria extends ListCriteria {
|
||||
state: string;
|
||||
@@ -97,7 +97,7 @@ export interface UserListingCriteria extends ListCriteria {
|
||||
companyName: string;
|
||||
counties: string[];
|
||||
states: string[];
|
||||
criteriaType: 'user';
|
||||
criteriaType: 'broker';
|
||||
}
|
||||
|
||||
export interface KeycloakUser {
|
||||
@@ -224,6 +224,12 @@ export interface UploadParams {
|
||||
imagePath: string;
|
||||
serialId?: number;
|
||||
}
|
||||
export interface GeoResult {
|
||||
id: number;
|
||||
city: string;
|
||||
state: string;
|
||||
state_code: string;
|
||||
}
|
||||
export function isEmpty(value: any): boolean {
|
||||
// Check for undefined or null
|
||||
if (value === undefined || value === null) {
|
||||
@@ -258,3 +264,4 @@ export function emailToDirName(email: string): string {
|
||||
|
||||
return normalizedEmail;
|
||||
}
|
||||
export const LISTINGS_PER_PAGE = 12;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Logger } from 'winston';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { User } from '../models/db.model';
|
||||
import { JwtUser, Subscription } from '../models/main.model.js';
|
||||
import { JwtUser, Subscription, UserListingCriteria } from '../models/main.model.js';
|
||||
import { UserService } from './user.service.js';
|
||||
|
||||
@Controller('user')
|
||||
@@ -39,9 +39,9 @@ export class UserController {
|
||||
}
|
||||
|
||||
@Post('search')
|
||||
find(@Body() criteria: any): any {
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
this.logger.info(`Searching for users with criteria: ${JSON.stringify(criteria)}`);
|
||||
const foundUsers = this.userService.findUser(criteria);
|
||||
const foundUsers = this.userService.searchUserListings(criteria);
|
||||
this.logger.info(`Found users: ${JSON.stringify(foundUsers)}`);
|
||||
return foundUsers;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, eq, ilike, or, sql } from 'drizzle-orm';
|
||||
import { and, count, eq, ilike, inArray, or, SQL, sql } from 'drizzle-orm';
|
||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres/driver.js';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { customerSubTypeEnum, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { User } from '../models/db.model.js';
|
||||
import { JwtUser, UserListingCriteria, emailToDirName } from '../models/main.model.js';
|
||||
import { emailToDirName, JwtUser, UserListingCriteria } from '../models/main.model.js';
|
||||
|
||||
type CustomerSubType = (typeof customerSubTypeEnum.enumValues)[number];
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
@@ -16,17 +17,85 @@ export class UserService {
|
||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||
private fileService: FileService,
|
||||
) {}
|
||||
private getConditions(criteria: UserListingCriteria): any[] {
|
||||
const conditions = [];
|
||||
if (criteria.states?.length > 0) {
|
||||
criteria.states.forEach(state => {
|
||||
conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: state }])}`);
|
||||
});
|
||||
// private getConditions(criteria: UserListingCriteria): any[] {
|
||||
// const conditions = [];
|
||||
// if (criteria.states?.length > 0) {
|
||||
// criteria.states.forEach(state => {
|
||||
// conditions.push(sql`${schema.users.areasServed} @> ${JSON.stringify([{ state: state }])}`);
|
||||
// });
|
||||
// }
|
||||
// if (criteria.firstname || criteria.lastname) {
|
||||
// conditions.push(or(ilike(schema.users.firstname, `%${criteria.lastname}%`), ilike(schema.users.lastname, `%${criteria.lastname}%`)));
|
||||
// }
|
||||
// return conditions;
|
||||
// }
|
||||
private getWhereConditions(criteria: UserListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city) {
|
||||
whereConditions.push(ilike(schema.users.companyLocation, `%${criteria.city}%`));
|
||||
}
|
||||
if (criteria.firstname || criteria.lastname) {
|
||||
conditions.push(or(ilike(schema.users.firstname, `%${criteria.lastname}%`), ilike(schema.users.lastname, `%${criteria.lastname}%`)));
|
||||
|
||||
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[]));
|
||||
}
|
||||
return conditions;
|
||||
|
||||
if (criteria.firstname) {
|
||||
whereConditions.push(ilike(schema.users.firstname, `%${criteria.firstname}%`));
|
||||
}
|
||||
|
||||
if (criteria.lastname) {
|
||||
whereConditions.push(ilike(schema.users.lastname, `%${criteria.lastname}%`));
|
||||
}
|
||||
|
||||
if (criteria.companyName) {
|
||||
whereConditions.push(ilike(schema.users.companyName, `%${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}%`})`)));
|
||||
}
|
||||
|
||||
if (criteria.states && criteria.states.length > 0) {
|
||||
whereConditions.push(or(...criteria.states.map(state => sql`EXISTS (SELECT 1 FROM jsonb_array_elements(${schema.users.areasServed}) AS area WHERE area->>'state' = ${state})`)));
|
||||
}
|
||||
|
||||
return whereConditions;
|
||||
}
|
||||
async searchUserListings(criteria: UserListingCriteria) {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn.select().from(schema.users);
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
query.where(whereClause);
|
||||
}
|
||||
|
||||
// Paginierung
|
||||
query.limit(length).offset(start);
|
||||
|
||||
const results = await query;
|
||||
const totalCount = await this.getUserListingsCount(criteria);
|
||||
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
};
|
||||
}
|
||||
async getUserListingsCount(criteria: UserListingCriteria): Promise<number> {
|
||||
const countQuery = this.conn.select({ value: count() }).from(schema.users);
|
||||
const whereConditions = this.getWhereConditions(criteria);
|
||||
|
||||
if (whereConditions.length > 0) {
|
||||
const whereClause = and(...whereConditions);
|
||||
countQuery.where(whereClause);
|
||||
}
|
||||
|
||||
const [{ value: totalCount }] = await countQuery;
|
||||
return totalCount;
|
||||
}
|
||||
async getUserByMail(email: string, jwtuser?: JwtUser) {
|
||||
const users = (await this.conn
|
||||
@@ -68,25 +137,25 @@ export class UserService {
|
||||
return newUser as User;
|
||||
}
|
||||
}
|
||||
async findUser(criteria: UserListingCriteria) {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const conditions = this.getConditions(criteria);
|
||||
const [data, total] = await Promise.all([
|
||||
this.conn
|
||||
.select()
|
||||
.from(schema.users)
|
||||
.where(and(...conditions))
|
||||
.offset(start)
|
||||
.limit(length),
|
||||
this.conn
|
||||
.select({ count: sql`count(*)` })
|
||||
.from(schema.users)
|
||||
.where(and(...conditions))
|
||||
.then(result => Number(result[0].count)),
|
||||
]);
|
||||
return { total, data };
|
||||
}
|
||||
// async findUser(criteria: UserListingCriteria) {
|
||||
// const start = criteria.start ? criteria.start : 0;
|
||||
// const length = criteria.length ? criteria.length : 12;
|
||||
// const conditions = this.getConditions(criteria);
|
||||
// const [data, total] = await Promise.all([
|
||||
// this.conn
|
||||
// .select()
|
||||
// .from(schema.users)
|
||||
// .where(and(...conditions))
|
||||
// .offset(start)
|
||||
// .limit(length),
|
||||
// this.conn
|
||||
// .select({ count: sql`count(*)` })
|
||||
// .from(schema.users)
|
||||
// .where(and(...conditions))
|
||||
// .then(result => Number(result[0].count)),
|
||||
// ]);
|
||||
// return { total, data };
|
||||
// }
|
||||
async getStates(): Promise<any[]> {
|
||||
const query = sql`SELECT jsonb_array_elements(${schema.users.areasServed}) ->> 'state' AS state, COUNT(DISTINCT ${schema.users.id}) AS count FROM ${schema.users} GROUP BY state ORDER BY count DESC`;
|
||||
const result = await this.conn.execute(query);
|
||||
|
||||
Reference in New Issue
Block a user