Start Umbau zu postgres

This commit is contained in:
Andreas Knuth 2024-04-14 22:52:19 +02:00
parent e784b424b0
commit 7d10080069
21 changed files with 5441 additions and 272 deletions

View File

@ -29,6 +29,7 @@
"@nestjs/passport": "^10.0.3", "@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0", "@nestjs/platform-express": "^10.0.0",
"@nestjs/serve-static": "^4.0.1", "@nestjs/serve-static": "^4.0.1",
"drizzle-orm": "^0.30.8",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"ky": "^1.2.0", "ky": "^1.2.0",
"nest-winston": "^1.9.4", "nest-winston": "^1.9.4",
@ -38,6 +39,7 @@
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pg": "^8.11.5",
"redis": "^4.6.13", "redis": "^4.6.13",
"redis-om": "^0.4.3", "redis-om": "^0.4.3",
"reflect-metadata": "^0.2.0", "reflect-metadata": "^0.2.0",
@ -58,6 +60,7 @@
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/pg": "^8.11.5",
"@types/supertest": "^6.0.0", "@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0", "@typescript-eslint/parser": "^6.0.0",

View File

@ -1,4 +1,4 @@
import { Module } from '@nestjs/common'; import { MiddlewareConsumer, Module } from '@nestjs/common';
import { AppController } from './app.controller.js'; import { AppController } from './app.controller.js';
import { AppService } from './app.service.js'; import { AppService } from './app.service.js';
import { FileService } from './file/file.service.js'; import { FileService } from './file/file.service.js';
@ -23,11 +23,13 @@ import { ListingsModule } from './listings/listings.module.js';
import { SelectOptionsModule } from './select-options/select-options.module.js'; import { SelectOptionsModule } from './select-options/select-options.module.js';
import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js'; import { CommercialPropertyListingsController } from './listings/commercial-property-listings.controller.js';
import { ImageModule } from './image/image.module.js'; import { ImageModule } from './image/image.module.js';
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@Module({ @Module({
imports: [ConfigModule.forRoot(), MailModule, AuthModule, imports: [ConfigModule.forRoot({isGlobal: true}), MailModule, AuthModule,
ServeStaticModule.forRoot({ ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'pictures'), // `public` ist das Verzeichnis, wo Ihre statischen Dateien liegen rootPath: join(__dirname, '..', 'pictures'), // `public` ist das Verzeichnis, wo Ihre statischen Dateien liegen
}), }),
@ -52,9 +54,13 @@ const __dirname = path.dirname(__filename);
ListingsModule, ListingsModule,
SelectOptionsModule, SelectOptionsModule,
RedisModule, RedisModule,
ImageModule ImageModule,
], ],
controllers: [AppController, SubscriptionsController], controllers: [AppController, SubscriptionsController],
providers: [AppService, FileService], providers: [AppService, FileService],
}) })
export class AppModule {} export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestDurationMiddleware).forRoutes('*');
}
}

View File

@ -0,0 +1,12 @@
import { jsonb, varchar } from 'drizzle-orm/pg-core';
import { pgTable, text, primaryKey } from 'drizzle-orm/pg-core';
export const businesses_json = pgTable('businesses_json', {
id: varchar('id', { length: 255 }).primaryKey(),
data: jsonb('data'),
});
export type BusinessesJson = {
id: string;
data: Record<string, any>;
};

View File

@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { drizzle } from 'drizzle-orm/node-postgres';
import pkg from 'pg';
const { Pool } = pkg;
import * as schema from './businesses_json.model.js';
import { ConfigService } from '@nestjs/config';
import { jsonb, varchar } from 'drizzle-orm/pg-core';
import { PG_CONNECTION } from './schema.js';
@Module({
providers: [
{
provide: PG_CONNECTION,
inject: [ConfigService],
useFactory: async (configService: ConfigService) => {
const connectionString = configService.get<string>('DATABASE_URL');
const pool = new Pool({
connectionString,
// ssl: true,
});
return drizzle(pool, { schema });
},
},
],
exports: [PG_CONNECTION],
})
export class DrizzleModule {}

View File

@ -0,0 +1,15 @@
import { integer, serial, text, pgTable } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
import { jsonb, varchar } from 'drizzle-orm/pg-core';
export const PG_CONNECTION = 'PG_CONNECTION';
export const businesses_json = pgTable('businesses_json', {
id: varchar('id', { length: 255 }).primaryKey(),
data: jsonb('data'),
});
export type BusinessesJson = {
id: string;
data: Record<string, any>;
};

View File

@ -21,7 +21,7 @@ export class ImageController {
@UseInterceptors(FileInterceptor('file'),) @UseInterceptors(FileInterceptor('file'),)
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) { async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
const imagename = await this.fileService.storePropertyPicture(file,id); const imagename = await this.fileService.storePropertyPicture(file,id);
await this.listingService.addImage(id,imagename); // await this.listingService.addImage(id,imagename);
} }
@Post('uploadProfile/:id') @Post('uploadProfile/:id')
@ -38,16 +38,16 @@ export class ImageController {
@Get(':id') @Get(':id')
async getPropertyImagesById(@Param('id') id:string): Promise<any> { async getPropertyImagesById(@Param('id') id:string): Promise<any> {
const result = await this.listingService.getCommercialPropertyListingById(id); // const result = await this.listingService.getCommercialPropertyListingById(id);
const listing = result as CommercialPropertyListing; // const listing = result as CommercialPropertyListing;
if (listing.imageOrder){ // if (listing.imageOrder){
return listing.imageOrder // return listing.imageOrder
} else { // } else {
const imageOrder = await this.fileService.getPropertyImages(id); // const imageOrder = await this.fileService.getPropertyImages(id);
listing.imageOrder=imageOrder; // listing.imageOrder=imageOrder;
this.listingService.saveListing(listing); // this.listingService.saveListing(listing);
return imageOrder; // return imageOrder;
} // }
} }
@Get('profileImages/:userids') @Get('profileImages/:userids')
async getProfileImagesForUsers(@Param('userids') userids:string): Promise<any> { async getProfileImagesForUsers(@Param('userids') userids:string): Promise<any> {
@ -61,7 +61,7 @@ export class ImageController {
@Delete('propertyPicture/:listingid/:imagename') @Delete('propertyPicture/:listingid/:imagename')
async deletePropertyImagesById(@Param('listingid') listingid:string,@Param('imagename') imagename:string): Promise<any> { async deletePropertyImagesById(@Param('listingid') listingid:string,@Param('imagename') imagename:string): Promise<any> {
this.fileService.deleteImage(`pictures/property/${listingid}/${imagename}`); this.fileService.deleteImage(`pictures/property/${listingid}/${imagename}`);
await this.listingService.deleteImage(listingid,imagename); // await this.listingService.deleteImage(listingid,imagename);
} }
@Delete('logo/:userid/') @Delete('logo/:userid/')
async deleteLogoImagesById(@Param('id') id:string): Promise<any> { async deleteLogoImagesById(@Param('id') id:string): Promise<any> {

View File

@ -4,51 +4,52 @@ import { convertStringToNullUndefined } from '../utils.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { ListingCriteria } from 'src/models/main.model.js';
@Controller('listings/business') @Controller('listings/business')
export class BusinessListingsController { export class BusinessListingsController {
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { constructor(private readonly listingsService:ListingsService,
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
} }
@Get() @Get()
findAll(): any { findAll(): any {
return this.listingsService.getAllBusinessListings(); this.logger.info(`start findAll Listing`);
return this.listingsService.findListings();
} }
@Get(':id') @Get(':id')
findById(@Param('id') id:string): any { findById(@Param('id') id:string): any {
return this.listingsService.getBusinessListingById(id); return this.listingsService.findById(id);
} }
@Get('user/:userid') @Get('user/:userid')
findByUserId(@Param('userid') userid:string): any { findByUserId(@Param('userid') userid:string): any {
return this.listingsService.getBusinessListingByUserId(userid); return this.listingsService.findByUserId(userid);
} }
@Post('search') @Post('search')
find(@Body() criteria: any): any { find(@Body() criteria: ListingCriteria): any {
return this.listingsService.findBusinessListings(criteria); return this.listingsService.findByState(criteria.state);
} }
/**
* @param listing creates a new listing
*/
@Post() @Post()
save(@Body() listing: any){ create(@Body() listing: any){
this.logger.info(`Save Listing`); this.logger.info(`Save Listing`);
this.listingsService.saveListing(listing) this.listingsService.createListing(listing)
}
@Put()
update(@Body() listing: any){
this.logger.info(`Save Listing`);
this.listingsService.updateListing(listing.id,listing)
} }
/**
* @param id deletes a listing
*/
@Delete(':id') @Delete(':id')
deleteById(@Param('id') id:string){ deleteById(@Param('id') id:string){
this.listingsService.deleteBusinessListing(id) this.listingsService.deleteListing(id)
} }
@Delete('deleteAll') // @Delete('deleteAll')
deleteAll(){ // deleteAll(){
this.listingsService.deleteAllBusinessListings() // this.listingsService.deleteAllBusinessListings()
} // }
} }

View File

@ -13,39 +13,39 @@ export class CommercialPropertyListingsController {
} }
@Get(':id') // @Get(':id')
findById(@Param('id') id:string): any { // findById(@Param('id') id:string): any {
return this.listingsService.getCommercialPropertyListingById(id); // return this.listingsService.getCommercialPropertyListingById(id);
} // }
@Post('search') // @Post('search')
find(@Body() criteria: any): any { // find(@Body() criteria: any): any {
return this.listingsService.findCommercialPropertyListings(criteria); // return this.listingsService.findCommercialPropertyListings(criteria);
} // }
@Put('imageOrder/:id') // @Put('imageOrder/:id')
async changeImageOrder(@Param('id') id:string,@Body() imageOrder: ImageProperty[]) { // async changeImageOrder(@Param('id') id:string,@Body() imageOrder: ImageProperty[]) {
this.listingsService.updateImageOrder(id, imageOrder) // this.listingsService.updateImageOrder(id, imageOrder)
} // }
/** // /**
* @param listing creates a new listing // * @param listing creates a new listing
*/ // */
@Post() // @Post()
save(@Body() listing: any){ // save(@Body() listing: any){
this.logger.info(`Save Listing`); // this.logger.info(`Save Listing`);
this.listingsService.saveListing(listing) // this.listingsService.saveListing(listing)
} // }
/** // /**
* @param id deletes a listing // * @param id deletes a listing
*/ // */
@Delete(':id') // @Delete(':id')
deleteById(@Param('id') id:string){ // deleteById(@Param('id') id:string){
this.listingsService.deleteCommercialPropertyListing(id) // this.listingsService.deleteCommercialPropertyListing(id)
} // }
@Delete('deleteAll') // @Delete('deleteAll')
deleteAll(){ // deleteAll(){
this.listingsService.deleteAllcommercialListings() // this.listingsService.deleteAllcommercialListings()
} // }
} }

View File

@ -1,4 +1,4 @@
import { Module } from '@nestjs/common'; import { Injectable, Module, OnModuleInit } from '@nestjs/common';
import { BusinessListingsController } from './business-listings.controller.js'; import { BusinessListingsController } from './business-listings.controller.js';
import { ListingsService } from './listings.service.js'; import { ListingsService } from './listings.service.js';
import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js'; import { CommercialPropertyListingsController } from './commercial-property-listings.controller.js';
@ -8,9 +8,15 @@ import { UnknownListingsController } from './unknown-listings.controller.js';
import { UserModule } from '../user/user.module.js'; import { UserModule } from '../user/user.module.js';
import { BrokerListingsController } from './broker-listings.controller.js'; import { BrokerListingsController } from './broker-listings.controller.js';
import { UserService } from '../user/user.service.js'; import { UserService } from '../user/user.service.js';
import { Client, Connection } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import { businesses_json } from '../drizzle/businesses_json.model.js';
import { DrizzleModule } from '../drizzle/drizzle.module.js';
@Module({ @Module({
imports: [RedisModule], imports: [RedisModule,DrizzleModule
],
controllers: [BusinessListingsController, CommercialPropertyListingsController,UnknownListingsController,BrokerListingsController], controllers: [BusinessListingsController, CommercialPropertyListingsController,UnknownListingsController,BrokerListingsController],
providers: [ListingsService,FileService,UserService], providers: [ListingsService,FileService,UserService],
exports: [ListingsService], exports: [ListingsService],

View File

@ -10,177 +10,186 @@ import {
import { convertStringToNullUndefined } from '../utils.js'; import { convertStringToNullUndefined } from '../utils.js';
import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { EntityData, EntityId, Repository, Schema, SchemaDefinition } from 'redis-om'; import { EntityData, EntityId, Schema, SchemaDefinition } from 'redis-om';
import { REDIS_CLIENT } from '../redis/redis.module.js'; import { eq, ilike, sql } from 'drizzle-orm';
import { BusinessesJson, PG_CONNECTION, businesses_json } from '../drizzle/schema.js';
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
import * as schema from '../drizzle/schema.js';
@Injectable() @Injectable()
export class ListingsService { export class ListingsService {
schemaNameBusiness:ListingCategory={name:'business'}
schemaNameCommercial:ListingCategory={name:'commercialProperty'} constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
businessListingRepository:Repository; @Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,){
commercialPropertyListingRepository:Repository; // this.businessListingRepository = new Repository(this.businessListingSchema, redis);
baseListingSchemaDef : SchemaDefinition = { // this.commercialPropertyListingRepository = new Repository(this.commercialPropertyListingSchema, redis)
id: { type: 'string' }, // this.businessListingRepository.createIndex();
userId: { type: 'string' }, // this.commercialPropertyListingRepository.createIndex();
listingsCategory: { type: 'string' },
title: { type: 'string' },
description: { type: 'string' },
country: { type: 'string' },
state:{ type: 'string' },
city:{ type: 'string' },
zipCode: { type: 'number' },
type: { type: 'string' },
price: { type: 'number' },
favoritesForUser:{ type: 'string[]' },
hideImage:{ type: 'boolean' },
draft:{ type: 'boolean' },
created:{ type: 'date' },
updated:{ type: 'date' }
} }
businessListingSchemaDef : SchemaDefinition = {
...this.baseListingSchemaDef, // ##############################################################
salesRevenue: { type: 'number' }, // ##############################################################
cashFlow: { type: 'number' }, async createListing(newListing: { id: string; data: BusinessesJson }): Promise<BusinessesJson> {
employees: { type: 'number' }, const [createdListing] = await this.conn.insert(businesses_json).values(newListing).returning();
established: { type: 'number' }, return createdListing as BusinessesJson;
internalListingNumber: { type: 'number' },
realEstateIncluded:{ type: 'boolean' },
leasedLocation:{ type: 'boolean' },
franchiseResale:{ type: 'boolean' },
supportAndTraining: { type: 'string' },
reasonForSale: { type: 'string' },
brokerLicencing: { type: 'string' },
internals: { type: 'string' },
} }
commercialPropertyListingSchemaDef : SchemaDefinition = {
...this.baseListingSchemaDef, async updateListing(id: string, data: BusinessListing): Promise<BusinessesJson> {
imageNames:{ type: 'string[]' }, const [updateListing] = await this.conn.update(businesses_json).set(data).where(eq(businesses_json.id, id)).returning();
return updateListing as BusinessesJson;
} }
businessListingSchema = new Schema(this.schemaNameBusiness.name,this.businessListingSchemaDef, {
dataStructure: 'JSON' async deleteListing(id: string): Promise<void> {
}) await this.conn.delete(businesses_json).where(eq(businesses_json.id, id));
commercialPropertyListingSchema = new Schema(this.schemaNameCommercial.name,this.commercialPropertyListingSchemaDef, {
dataStructure: 'JSON'
})
constructor(@Inject(REDIS_CLIENT) private readonly redis: any, @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger){
this.businessListingRepository = new Repository(this.businessListingSchema, redis);
this.commercialPropertyListingRepository = new Repository(this.commercialPropertyListingSchema, redis)
this.businessListingRepository.createIndex();
this.commercialPropertyListingRepository.createIndex();
} }
async saveListing(listing: BusinessListing | CommercialPropertyListing) {
const repo=listing.listingsCategory==='business'?this.businessListingRepository:this.commercialPropertyListingRepository; async findByPriceRange(minPrice: number, maxPrice: number): Promise<BusinessesJson[]> {
let result return this.conn.select().from(businesses_json).where(sql`${businesses_json.data}->>'price' BETWEEN ${minPrice} AND ${maxPrice}`);
if (listing.id){
result = await repo.save(listing.id,listing as any)
} else {
result = await repo.save(listing as any)
listing.id=result[EntityId];
result = await repo.save(listing.id,listing as any)
} }
return result;
async findByState(state: string): Promise<BusinessesJson[]> {
return this.conn.select().from(businesses_json).where(sql`${businesses_json.data}->>'state' = ${state}`);
} }
async getCommercialPropertyListingById(id: string): Promise<CommercialPropertyListing>{ async findById(id: string): Promise<BusinessesJson[]> {
return await this.commercialPropertyListingRepository.fetch(id) as unknown as CommercialPropertyListing; return this.conn.select().from(businesses_json).where(sql`${businesses_json.id} = ${id}`);
} }
async getBusinessListingById(id: string) { async findByUserId(userId: string): Promise<BusinessesJson[]> {
return await this.businessListingRepository.fetch(id) return this.conn.select().from(businesses_json).where(sql`${businesses_json.data}->>'userId' = ${userId}`);
} }
async getBusinessListingByUserId(userid:string){ // async findByTitleContains(title: string): Promise<BusinessesJson[]> {
return await this.businessListingRepository.search().where('userId').equals(userid).return.all() // return this.conn.select().from(businesses_json).where(ilike(sql`${businesses_json.data}->>'title'`, `%${title}%`));
// }
async findByTitleContains(title: string): Promise<BusinessesJson[]> {
return this.conn.select().from(businesses_json).where(sql`${businesses_json.data}->>'title' ILIKE '%' || ${title} || '%'`);
} }
async deleteBusinessListing(id: string){ async findListings(start = 0, size = 48): Promise<{ data: Record<string, any>[]; total: number }> {
return await this.businessListingRepository.remove(id); // return this.conn.select({ data: businesses_json.data }).from(businesses_json).offset(start).limit(size);
const [data, total] = await Promise.all([
this.conn.select({ data: businesses_json.data }).from(businesses_json).offset(start).limit(size),
this.conn.select({ count: sql`count(*)` }).from(businesses_json).then((result) => Number(result[0].count)),
]);
return { data, total };
} }
async deleteCommercialPropertyListing(id: string){ // ##############################################################
return await this.commercialPropertyListingRepository.remove(id); // ##############################################################
} // async saveListing(listing: BusinessListing | CommercialPropertyListing) {
async getAllBusinessListings(start?: number, end?: number) { // const repo=listing.listingsCategory==='business'?this.businessListingRepository:this.commercialPropertyListingRepository;
return await this.businessListingRepository.search().return.all() // let result
} // if (listing.id){
async getAllCommercialListings(start?: number, end?: number) { // result = await repo.save(listing.id,listing as any)
return await this.commercialPropertyListingRepository.search().return.all() // } else {
} // result = await repo.save(listing as any)
async findBusinessListings(criteria:ListingCriteria): Promise<any> { // listing.id=result[EntityId];
// let listings = await this.getAllBusinessListings(); // result = await repo.save(listing.id,listing as any)
// }
// return result;
// }
// async getCommercialPropertyListingById(id: string): Promise<CommercialPropertyListing>{
// 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<any> {
// // 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<any> {
// let listings = await this.getAllCommercialListings();
// return this.find(criteria,listings); // return this.find(criteria,listings);
this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`); // }
const result = await this.redis.ft.search('business:index','*',{LIMIT:{from:0,size:50}}); // async deleteAllBusinessListings(){
this.logger.info(`start findBusinessListings: ${JSON.stringify(criteria)}`); // const ids = await this.getIdsForRepo(this.schemaNameBusiness.name);
return result.documents; // this.businessListingRepository.remove(ids);
} // }
async findCommercialPropertyListings(criteria:ListingCriteria): Promise<any> { // async deleteAllcommercialListings(){
let listings = await this.getAllCommercialListings(); // const ids = await this.getIdsForRepo(this.schemaNameCommercial.name);
return this.find(criteria,listings); // this.commercialPropertyListingRepository.remove(ids);
} // }
async deleteAllBusinessListings(){ // async getIdsForRepo(repoName:string, maxcount=100000){
const ids = await this.getIdsForRepo(this.schemaNameBusiness.name); // let cursor = 0;
this.businessListingRepository.remove(ids); // let ids = [];
} // do {
async deleteAllcommercialListings(){ // const reply = await this.redis.scan(cursor, {
const ids = await this.getIdsForRepo(this.schemaNameCommercial.name); // MATCH: `${repoName}:*`,
this.commercialPropertyListingRepository.remove(ids); // COUNT: maxcount
} // });
async getIdsForRepo(repoName:string, maxcount=100000){ // cursor = reply.cursor;
let cursor = 0; // // Extrahiere die ID aus jedem Schlüssel und füge sie zur Liste hinzu
let ids = []; // ids = ids.concat(reply.keys.map(key => key.split(':')[1]).filter(id=>id!='index'));
do { // } while (cursor !== 0);
const reply = await this.redis.scan(cursor, { // return ids;
MATCH: `${repoName}:*`, // }
COUNT: maxcount
}); // async find(criteria:ListingCriteria, listings: any[]): Promise<any> {
cursor = reply.cursor; // listings=listings.filter(l=>l.listingsCategory===criteria.listingsCategory);
// Extrahiere die ID aus jedem Schlüssel und füge sie zur Liste hinzu // if (convertStringToNullUndefined(criteria.type)){
ids = ids.concat(reply.keys.map(key => key.split(':')[1]).filter(id=>id!='index')); // console.log(criteria.type);
} while (cursor !== 0); // listings=listings.filter(l=>l.type===criteria.type);
return ids; // }
} // 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
// }
// async updateImageOrder(id:string,imageOrder: ImageProperty[]){
// const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing
// listing.imageOrder=imageOrder;
// this.saveListing(listing);
// }
// async deleteImage(listingid:string,name:string,){
// const listing = await this.getCommercialPropertyListingById(listingid) as unknown as CommercialPropertyListing
// const index = listing.imageOrder.findIndex(im=>im.name===name);
// if (index>-1){
// listing.imageOrder.splice(index,1);
// this.saveListing(listing);
// }
// }
// async addImage(id:string,imagename: string){
// const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing
// listing.imageOrder.push({name:imagename,code:'',id:''});
// this.saveListing(listing);
// }
async find(criteria:ListingCriteria, listings: any[]): Promise<any> {
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
}
async updateImageOrder(id:string,imageOrder: ImageProperty[]){
const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing
listing.imageOrder=imageOrder;
this.saveListing(listing);
}
async deleteImage(listingid:string,name:string,){
const listing = await this.getCommercialPropertyListingById(listingid) as unknown as CommercialPropertyListing
const index = listing.imageOrder.findIndex(im=>im.name===name);
if (index>-1){
listing.imageOrder.splice(index,1);
this.saveListing(listing);
}
}
async addImage(id:string,imagename: string){
const listing = await this.getCommercialPropertyListingById(id) as unknown as CommercialPropertyListing
listing.imageOrder.push({name:imagename,code:'',id:''});
this.saveListing(listing);
}
} }

View File

@ -12,18 +12,18 @@ export class UnknownListingsController {
} }
@Get(':id') // @Get(':id')
async findById(@Param('id') id:string): Promise<any> { // async findById(@Param('id') id:string): Promise<any> {
const result = await this.listingsService.getBusinessListingById(id); // const result = await this.listingsService.getBusinessListingById(id);
if (result.id){ // if (result.id){
return result // return result
} else { // } else {
return await this.listingsService.getCommercialPropertyListingById(id); // return await this.listingsService.getCommercialPropertyListingById(id);
} // }
} // }
@Get('repo/:repo') // @Get('repo/:repo')
async getAllByRepo(@Param('repo') repo:string): Promise<any> { // async getAllByRepo(@Param('repo') repo:string): Promise<any> {
return await this.listingsService.getIdsForRepo(repo); // return await this.listingsService.getIdsForRepo(repo);
} // }
} }

View File

@ -3,22 +3,23 @@ import { Module } from '@nestjs/common';
@Module({ @Module({
providers: [ providers: [
{ // {
provide: 'REDIS_OPTIONS', // provide: 'REDIS_OPTIONS',
useValue: { // useValue: {
url: 'redis://localhost:6379' // url: 'redis://localhost:6379'
} // }
}, // },
{ // {
inject: ['REDIS_OPTIONS'], // inject: ['REDIS_OPTIONS'],
provide: 'REDIS_CLIENT', // provide: 'REDIS_CLIENT',
useFactory: async (options: { url: string }) => { // useFactory: async (options: { url: string }) => {
const client = createClient(options); // const client = createClient(options);
await client.connect(); // await client.connect();
return client; // return client;
} // }
}], // }
exports:['REDIS_CLIENT'] ],
// exports:['REDIS_CLIENT']
}) })
export class RedisModule {} export class RedisModule {}
export const REDIS_CLIENT = "REDIS_CLIENT"; export const REDIS_CLIENT = "REDIS_CLIENT";

View File

@ -0,0 +1,16 @@
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RequestDurationMiddleware implements NestMiddleware {
private readonly logger = new Logger(RequestDurationMiddleware.name);
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
this.logger.log(`${req.method} ${req.url} - ${duration}ms`);
});
next();
}
}

View File

@ -25,9 +25,9 @@ export class UserService {
}, { }, {
dataStructure: 'JSON' dataStructure: 'JSON'
}) })
constructor(@Inject(REDIS_CLIENT) private readonly redis: any,private fileService:FileService){ constructor(private fileService:FileService){
this.userRepository = new Repository(this.userSchema, redis) // this.userRepository = new Repository(this.userSchema, redis)
this.userRepository.createIndex(); // this.userRepository.createIndex();
} }
async getUserById( id:string){ async getUserById( id:string){
const user = await this.userRepository.fetch(id) as UserEntity; const user = await this.userRepository.fetch(id) as UserEntity;

View File

@ -56,7 +56,7 @@
<div class="surface-200 h-full"> <div class="surface-200 h-full">
<div class="wrapper"> <div class="wrapper">
<div class="grid"> <div class="grid">
@for (listing of filteredListings; track listing.id) { @for (listing of listings; track listing.id) {
<div *ngIf="listing.listingsCategory==='business'" class="col-12 lg:col-3 p-3"> <div *ngIf="listing.listingsCategory==='business'" class="col-12 lg:col-3 p-3">
<div class="shadow-2 border-round surface-card mb-3 h-full flex-column justify-content-between flex"> <div class="shadow-2 border-round surface-card mb-3 h-full flex-column justify-content-between flex">
<div class="p-4 h-full flex flex-column"> <div class="p-4 h-full flex flex-column">

View File

@ -73,10 +73,11 @@ export class ListingsComponent {
if (this.category==='business' || this.category==='commercialProperty'){ if (this.category==='business' || this.category==='commercialProperty'){
this.users=[] this.users=[]
this.listings=await this.listingsService.getListings(this.criteria); this.listings=await this.listingsService.getListings(this.criteria);
this.setStates(); this.setStates();
this.filteredListings=[...this.listings]; //this.filteredListings=[...this.listings];
this.totalRecords=this.listings.length this.totalRecords=this.listings.length
this.filteredListings=[...this.listings].splice(this.first,this.rows); //this.filteredListings=[...this.listings].splice(this.first,this.rows);
this.cdRef.markForCheck(); this.cdRef.markForCheck();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} else { } else {
@ -112,9 +113,13 @@ export class ListingsComponent {
this.cdRef.detectChanges(); this.cdRef.detectChanges();
} }
onPageChange(event: any) { onPageChange(event: any) {
this.first = event.first; //this.first = event.first;
this.rows = event.rows; //this.rows = event.rows;
this.filteredListings=[...this.listings].splice(this.first,this.rows); //this.filteredListings=[...this.listings].splice(this.first,this.rows);
this.criteria.start=event.first;
this.criteria.length=event.rows;
this.criteria.page=event.page;
this.criteria.pageCount=event.pageCount;
} }
imageErrorHandler(listing: ListingType) { imageErrorHandler(listing: ListingType) {
listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann listing.hideImage = true; // Bild ausblenden, wenn es nicht geladen werden kann

View File

@ -68,6 +68,10 @@ export type ListingType =
| CommercialPropertyListing; | CommercialPropertyListing;
export interface ListingCriteria { export interface ListingCriteria {
start:number,
length:number,
page:number,
pageCount:number,
type:string, type:string,
state:string, state:string,
minPrice:string, minPrice:string,

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
[]

View File

@ -11,17 +11,20 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"typescript": "^5.2.2" "typescript": "^5.4.5"
}, },
"dependencies": { "dependencies": {
"@types/node": "^20.12.7",
"currency.js": "^2.0.4", "currency.js": "^2.0.4",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"inquirer": "^9.2.17", "inquirer": "^9.2.17",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"pg": "^8.11.5",
"puppeteer": "^22.1.0", "puppeteer": "^22.1.0",
"redis": "^4.6.13", "redis": "^4.6.13",
"redis-om": "^0.4.3", "redis-om": "^0.4.3",
"uuidv4": "^6.2.13",
"winston": "^3.13.0", "winston": "^3.13.0",
"yargs": "^17.7.2" "yargs": "^17.7.2"
} }

View File

@ -0,0 +1,85 @@
import pkg from 'pg';
const { Pool } = pkg;
import fsextra from 'fs-extra';
const { fstat, readFileSync, writeJsonSync } = fsextra;
import { v4 as uuidv4 } from 'uuid';
// PostgreSQL Verbindungskonfiguration
const pool = new Pool({
user: 'bizmatch',
host: 'localhost',
database: 'bizmatch',
password: 'xieng7Seih',
port: 5432,
});
// Typdefinition für das JSON-Objekt
interface BusinessListing {
userId?: string;
listingsCategory: string;
title: string;
description: string;
type: string;
state: string;
city: string;
id: string;
price: number;
salesRevenue: number;
leasedLocation: boolean;
established: number;
employees: number;
reasonForSale: string;
supportAndTraining: string;
cashFlow: number;
brokerLicencing: string;
internalListingNumber: number;
realEstateIncluded: boolean;
franchiseResale: boolean;
draft: boolean;
internals: string;
created: Date;
}
// Funktion zum Einlesen und Importieren von JSON-Daten
async function importJsonData(filePath: string): Promise<void> {
try {
const data: string = readFileSync(filePath, 'utf8');
const jsonData: BusinessListing[] = JSON.parse(data); // Erwartet ein Array von Objekten
const out: BusinessListing[] =[]
// Daten für jedes Listing in die Datenbank einfügen
for (const listing of jsonData) {
// const uuid = uuidv4();
// listing.id=uuid;
const values = [
listing.userId, listing.listingsCategory, listing.title, listing.description,
listing.type, listing.state, listing.city, listing.id, listing.price, listing.salesRevenue,
listing.leasedLocation, listing.established, listing.employees,
listing.reasonForSale, listing.supportAndTraining, listing.cashFlow, listing.brokerLicencing,
listing.internalListingNumber, listing.realEstateIncluded, listing.franchiseResale,
listing.draft, listing.internals, listing.created, new Date(), 0, null
];
const json_values = [
listing.id, listing
]
await pool.query(`INSERT INTO businesses
(user_id, listings_category, title, description, type, state, city, id, price, sales_revenue, leased_location,
established, employees, reason_for_sale, support_and_training, cash_flow, broker_licencing, internal_listing_number,
real_estate_included, franchise_resale, draft, internals, created, updated, visits, last_visit)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26)`, values);
await pool.query('INSERT INTO businesses_json (id, data) VALUES ($1,$2)', json_values);
// out.push(listing);
}
writeJsonSync('./data/businesses_.json',out);
console.log('All data imported successfully.');
} catch (err) {
console.error('Error importing data:', err.message);
} finally {
// Schließen der Verbindung zum Pool
await pool.end();
}
}
// Passen Sie den Dateipfad an Ihre spezifischen Bedürfnisse an
importJsonData('./data/businesses_.json');