Compare commits
113 Commits
99bcc77abf
...
tailwind
| Author | SHA1 | Date | |
|---|---|---|---|
| 69b0a83b1e | |||
| 7df5d32cc4 | |||
| 1a77656d8a | |||
| b1f26fbf48 | |||
| 3795a5a30c | |||
| 8698aa3e66 | |||
| 398f8d29ca | |||
| 8b71b073be | |||
| 4c1b1fbc87 | |||
| f58448679d | |||
| c9305749d2 | |||
| 29f88d610f | |||
| 2955c034a0 | |||
| 55e800009e | |||
| 6348af8862 | |||
| a6ae643458 | |||
| 38e943c18e | |||
| acec14d372 | |||
| 9db23c2177 | |||
| abcde3991d | |||
| f88eebe8d3 | |||
| bdafb03165 | |||
| af982d19d8 | |||
| b7b34dacab | |||
| bf4bd69337 | |||
| b4644ea295 | |||
| 7bd5e1aaf8 | |||
| 08c179fa09 | |||
| 7f67b81242 | |||
| e0dbebb61c | |||
| 677b95c21c | |||
| 1534c14a68 | |||
| 5fa2dd60fa | |||
| 9228cbebbe | |||
| 1ccd1d174c | |||
| 958f0afd9b | |||
| 3d5b7e3f39 | |||
| d488f90f48 | |||
| 044f8efa0f | |||
| 24a3d210f0 | |||
| 2465b8966b | |||
| e87222d3c1 | |||
| 902ab9caed | |||
| 44acbcd4d0 | |||
| b4cf17b8ea | |||
| 226d2ebc1e | |||
| 0473f74241 | |||
| c9d94e973a | |||
| f9d9c6ad9e | |||
| 5dc893da38 | |||
| c471629c6d | |||
| 13fb3cd4b8 | |||
| d6768b3da9 | |||
| 7fdc87fb0b | |||
| 0b7e33612a | |||
| 8fba3aa832 | |||
| 214327031c | |||
| dc9adb151d | |||
| 747435bfba | |||
| 782c254a33 | |||
| df4e2b00e2 | |||
| 0684b9534f | |||
| e0ecea5af2 | |||
| 327aef0f21 | |||
| cb73daf863 | |||
| 08c53e2eb2 | |||
| 492c03c2be | |||
| f51a298227 | |||
| 474d7c63d5 | |||
| d2e5562602 | |||
| aff55c5433 | |||
| 5230ef1230 | |||
| d508415de4 | |||
| 6b61c19bd7 | |||
| bb5a408cdc | |||
| 9121ca1a69 | |||
| 4230867608 | |||
| 9e03620be7 | |||
| 7f0f21b598 | |||
| c90d6b72b7 | |||
| c4cdcf4505 | |||
| 7d10080069 | |||
| e784b424b0 | |||
| 4a9798c85e | |||
| 5c6dfa1e0d | |||
| 8aea819496 | |||
| fb69d2e4b3 | |||
| e5b7779492 | |||
| a437851f6d | |||
| 89bb85a512 | |||
| 840d7a63b1 | |||
| 73ab12a694 | |||
| a2c613c38f | |||
| d5210d3df4 | |||
| fd91adda57 | |||
| 2b27ab8ba5 | |||
| 25b201a9c6 | |||
| b8cabf4f0c | |||
| f3868da8f8 | |||
| be146fdc6a | |||
| 6ad40b6dca | |||
| 06f349d1c3 | |||
| c7fccd46d9 | |||
| 3b0196bd66 | |||
| 244fbe78ed | |||
| 56648b47a2 | |||
| f3b29a362e | |||
| fa3b9e104a | |||
| 798b84aa88 | |||
|
|
0e01b6ba09 | ||
|
|
3f9ffc1616 | ||
| 29a2eacd0f | |||
| 2eb34adc7c |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -48,6 +48,9 @@ public
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
.env.prod
|
||||
.env.dev
|
||||
.env.test
|
||||
|
||||
# temp directory
|
||||
.temp
|
||||
@@ -62,10 +65,7 @@ pids
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
*.js
|
||||
*.map
|
||||
package-lock.json
|
||||
|
||||
*.jar
|
||||
gitea
|
||||
auth
|
||||
|
||||
16
bizmatch-server/.editorconfig
Normal file
16
bizmatch-server/.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
25
bizmatch-server/.eslintrc.js
Normal file
25
bizmatch-server/.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
45
bizmatch-server/.eslintrc.json
Normal file
45
bizmatch-server/.eslintrc.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"env": {
|
||||
"es2021": true,
|
||||
"browser": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base",
|
||||
"airbnb-typescript",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"eslint-config-prettier",
|
||||
"plugin:cypress/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module",
|
||||
"project": ["./tsconfig.json"]
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"import/no-unresolved": ["off"],
|
||||
"import/prefer-default-export": ["off"],
|
||||
"no-useless-constructor": "off",
|
||||
"@typescript-eslint/no-useless-constructor": ["error"],
|
||||
"@typescript-eslint/lines-between-class-members": ["off"],
|
||||
"no-param-reassign": ["off"],
|
||||
"max-classes-per-file": ["off"],
|
||||
"no-shadow": ["off"],
|
||||
"class-methods-use-this": ["off"],
|
||||
"react/jsx-filename-extension": ["off"],
|
||||
"import/no-cycle": ["off"],
|
||||
"radix": ["off"],
|
||||
"no-promise-executor-return": ["off"],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "enumMember",
|
||||
"format": ["UPPER_CASE", "PascalCase"]
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
|
||||
"spaced-comment": ["off"],
|
||||
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }]
|
||||
}
|
||||
}
|
||||
6
bizmatch-server/.gitignore
vendored
6
bizmatch-server/.gitignore
vendored
@@ -55,4 +55,8 @@ pids
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
public
|
||||
pictures
|
||||
pictures_base
|
||||
|
||||
src/*.js
|
||||
bun.lockb
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 220,
|
||||
"proseWrap": "always",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
54
bizmatch-server/.vscode/launch.json
vendored
54
bizmatch-server/.vscode/launch.json
vendored
@@ -6,17 +6,59 @@
|
||||
"request": "launch",
|
||||
"name": "Debug Nest Framework",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"start:debug",
|
||||
"--",
|
||||
"--inspect-brk"
|
||||
],
|
||||
"runtimeArgs": ["run", "start:debug", "--", "--inspect-brk"],
|
||||
"autoAttachChildProcesses": true,
|
||||
"restart": true,
|
||||
"sourceMaps": true,
|
||||
"stopOnEntry": false,
|
||||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"HOST_NAME": "localhost"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch TypeScript file with tsx",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["tsx", "--inspect"],
|
||||
"args": ["${workspaceFolder}/src/drizzle/import.ts"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"],
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": ["${workspaceFolder}/src/**/*.ts", "!**/node_modules/**"],
|
||||
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**/*.js"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Launch TypeScript file",
|
||||
"runtimeArgs": ["-r", "ts-node/register", "-r", "tsconfig-paths/register"],
|
||||
"args": ["${workspaceFolder}/src/drizzle/import.ts"],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"protocol": "inspector",
|
||||
"outFiles": ["${workspaceFolder}/**/*.js"],
|
||||
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**/*.js"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "generateDefs",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/dist/src/drizzle/generateDefs.js",
|
||||
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||
"sourceMaps": true,
|
||||
"smartStep": true
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "generateTypes",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"program": "${workspaceFolder}/dist/src/drizzle/generateTypes.js",
|
||||
"outFiles": ["${workspaceFolder}/dist/src/drizzle/**/*.js"],
|
||||
"sourceMaps": true,
|
||||
"smartStep": true
|
||||
}
|
||||
]
|
||||
}
|
||||
28
bizmatch-server/.vscode/settings.json
vendored
Normal file
28
bizmatch-server/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"editor.suggestSelection": "first",
|
||||
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
|
||||
"explorer.confirmDelete": false,
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
},
|
||||
"[scss]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"prettier.printWidth": 240,
|
||||
"git.autofetch": false,
|
||||
"git.autorefresh": true
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
FT.CREATE listingsIndex ON JSON PREFIX 1 listings: SCHEMA $.location AS location TAG SORTABLE $.price AS price NUMERIC SORTABLE $.listingsCategory AS listingsCategory TAG SORTABLE $.type AS type TAG SORTABLE
|
||||
1326
bizmatch-server/broker.json
Normal file
1326
bizmatch-server/broker.json
Normal file
File diff suppressed because it is too large
Load Diff
1371
bizmatch-server/data/broker.json
Normal file
1371
bizmatch-server/data/broker.json
Normal file
File diff suppressed because it is too large
Load Diff
12493
bizmatch-server/data/businesses.json
Normal file
12493
bizmatch-server/data/businesses.json
Normal file
File diff suppressed because it is too large
Load Diff
2858
bizmatch-server/data/commercials.json
Normal file
2858
bizmatch-server/data/commercials.json
Normal file
File diff suppressed because it is too large
Load Diff
239
bizmatch-server/dbschema.ts
Normal file
239
bizmatch-server/dbschema.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
|
||||
/**
|
||||
* AUTO-GENERATED FILE - DO NOT EDIT!
|
||||
*
|
||||
* This file was automatically generated by pg-to-ts v.4.1.1
|
||||
* $ pg-to-ts generate -c postgresql://username:password@localhost:5432/bizmatch -t businesses -t commercials -t users -s public
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export type Json = unknown;
|
||||
export type customerSubType = 'appraiser' | 'attorney' | 'broker' | 'cpa' | 'surveyor' | 'titleCompany';
|
||||
export type customerType = 'buyer' | 'professional';
|
||||
export type gender = 'female' | 'male';
|
||||
export type listingsCategory = 'business' | 'commercialProperty';
|
||||
|
||||
// Table businesses
|
||||
export interface Businesses {
|
||||
id: string;
|
||||
email: string | null;
|
||||
type: string | null;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
zipCode: number | null;
|
||||
county: string | null;
|
||||
price: number | null;
|
||||
favoritesForUser: string[] | null;
|
||||
draft: boolean | null;
|
||||
listingsCategory: listingsCategory | null;
|
||||
realEstateIncluded: boolean | null;
|
||||
leasedLocation: boolean | null;
|
||||
franchiseResale: boolean | null;
|
||||
salesRevenue: number | null;
|
||||
cashFlow: number | null;
|
||||
supportAndTraining: string | null;
|
||||
employees: number | null;
|
||||
established: number | null;
|
||||
internalListingNumber: number | null;
|
||||
reasonForSale: string | null;
|
||||
brokerLicencing: string | null;
|
||||
internals: string | null;
|
||||
imageName: string | null;
|
||||
created: Date | null;
|
||||
updated: Date | null;
|
||||
visits: number | null;
|
||||
lastVisit: Date | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
}
|
||||
export interface BusinessesInput {
|
||||
id?: string;
|
||||
email?: string | null;
|
||||
type?: string | null;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
zipCode?: number | null;
|
||||
county?: string | null;
|
||||
price?: number | null;
|
||||
favoritesForUser?: string[] | null;
|
||||
draft?: boolean | null;
|
||||
listingsCategory?: listingsCategory | null;
|
||||
realEstateIncluded?: boolean | null;
|
||||
leasedLocation?: boolean | null;
|
||||
franchiseResale?: boolean | null;
|
||||
salesRevenue?: number | null;
|
||||
cashFlow?: number | null;
|
||||
supportAndTraining?: string | null;
|
||||
employees?: number | null;
|
||||
established?: number | null;
|
||||
internalListingNumber?: number | null;
|
||||
reasonForSale?: string | null;
|
||||
brokerLicencing?: string | null;
|
||||
internals?: string | null;
|
||||
imageName?: string | null;
|
||||
created?: Date | null;
|
||||
updated?: Date | null;
|
||||
visits?: number | null;
|
||||
lastVisit?: Date | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
}
|
||||
const businesses = {
|
||||
tableName: 'businesses',
|
||||
columns: ['id', 'email', 'type', 'title', 'description', 'city', 'state', 'zipCode', 'county', 'price', 'favoritesForUser', 'draft', 'listingsCategory', 'realEstateIncluded', 'leasedLocation', 'franchiseResale', 'salesRevenue', 'cashFlow', 'supportAndTraining', 'employees', 'established', 'internalListingNumber', 'reasonForSale', 'brokerLicencing', 'internals', 'imageName', 'created', 'updated', 'visits', 'lastVisit', 'latitude', 'longitude'],
|
||||
requiredForInsert: [],
|
||||
primaryKey: 'id',
|
||||
foreignKeys: { email: { table: 'users', column: 'email', $type: null as unknown as Users }, },
|
||||
$type: null as unknown as Businesses,
|
||||
$input: null as unknown as BusinessesInput
|
||||
} as const;
|
||||
|
||||
// Table commercials
|
||||
export interface Commercials {
|
||||
id: string;
|
||||
serialId: number;
|
||||
email: string | null;
|
||||
type: string | null;
|
||||
title: string | null;
|
||||
description: string | null;
|
||||
city: string | null;
|
||||
state: string | null;
|
||||
price: number | null;
|
||||
favoritesForUser: string[] | null;
|
||||
listingsCategory: listingsCategory | null;
|
||||
hideImage: boolean | null;
|
||||
draft: boolean | null;
|
||||
zipCode: number | null;
|
||||
county: string | null;
|
||||
imageOrder: string[] | null;
|
||||
imagePath: string | null;
|
||||
created: Date | null;
|
||||
updated: Date | null;
|
||||
visits: number | null;
|
||||
lastVisit: Date | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
}
|
||||
export interface CommercialsInput {
|
||||
id?: string;
|
||||
serialId?: number;
|
||||
email?: string | null;
|
||||
type?: string | null;
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
price?: number | null;
|
||||
favoritesForUser?: string[] | null;
|
||||
listingsCategory?: listingsCategory | null;
|
||||
hideImage?: boolean | null;
|
||||
draft?: boolean | null;
|
||||
zipCode?: number | null;
|
||||
county?: string | null;
|
||||
imageOrder?: string[] | null;
|
||||
imagePath?: string | null;
|
||||
created?: Date | null;
|
||||
updated?: Date | null;
|
||||
visits?: number | null;
|
||||
lastVisit?: Date | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
}
|
||||
const commercials = {
|
||||
tableName: 'commercials',
|
||||
columns: ['id', 'serialId', 'email', 'type', 'title', 'description', 'city', 'state', 'price', 'favoritesForUser', 'listingsCategory', 'hideImage', 'draft', 'zipCode', 'county', 'imageOrder', 'imagePath', 'created', 'updated', 'visits', 'lastVisit', 'latitude', 'longitude'],
|
||||
requiredForInsert: [],
|
||||
primaryKey: 'id',
|
||||
foreignKeys: { email: { table: 'users', column: 'email', $type: null as unknown as Users }, },
|
||||
$type: null as unknown as Commercials,
|
||||
$input: null as unknown as CommercialsInput
|
||||
} as const;
|
||||
|
||||
// Table users
|
||||
export interface Users {
|
||||
id: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
phoneNumber: string | null;
|
||||
description: string | null;
|
||||
companyName: string | null;
|
||||
companyOverview: string | null;
|
||||
companyWebsite: string | null;
|
||||
companyLocation: string | null;
|
||||
offeredServices: string | null;
|
||||
areasServed: Json | null;
|
||||
hasProfile: boolean | null;
|
||||
hasCompanyLogo: boolean | null;
|
||||
licensedIn: Json | null;
|
||||
gender: gender | null;
|
||||
customerType: customerType | null;
|
||||
customerSubType: customerSubType | null;
|
||||
created: Date | null;
|
||||
updated: Date | null;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
}
|
||||
export interface UsersInput {
|
||||
id?: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
phoneNumber?: string | null;
|
||||
description?: string | null;
|
||||
companyName?: string | null;
|
||||
companyOverview?: string | null;
|
||||
companyWebsite?: string | null;
|
||||
companyLocation?: string | null;
|
||||
offeredServices?: string | null;
|
||||
areasServed?: Json | null;
|
||||
hasProfile?: boolean | null;
|
||||
hasCompanyLogo?: boolean | null;
|
||||
licensedIn?: Json | null;
|
||||
gender?: gender | null;
|
||||
customerType?: customerType | null;
|
||||
customerSubType?: customerSubType | null;
|
||||
created?: Date | null;
|
||||
updated?: Date | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
}
|
||||
const users = {
|
||||
tableName: 'users',
|
||||
columns: ['id', 'firstname', 'lastname', 'email', 'phoneNumber', 'description', 'companyName', 'companyOverview', 'companyWebsite', 'companyLocation', 'offeredServices', 'areasServed', 'hasProfile', 'hasCompanyLogo', 'licensedIn', 'gender', 'customerType', 'customerSubType', 'created', 'updated', 'latitude', 'longitude'],
|
||||
requiredForInsert: ['firstname', 'lastname', 'email'],
|
||||
primaryKey: 'id',
|
||||
foreignKeys: {},
|
||||
$type: null as unknown as Users,
|
||||
$input: null as unknown as UsersInput
|
||||
} as const;
|
||||
|
||||
|
||||
export interface TableTypes {
|
||||
businesses: {
|
||||
select: Businesses;
|
||||
input: BusinessesInput;
|
||||
};
|
||||
commercials: {
|
||||
select: Commercials;
|
||||
input: CommercialsInput;
|
||||
};
|
||||
users: {
|
||||
select: Users;
|
||||
input: UsersInput;
|
||||
};
|
||||
}
|
||||
|
||||
export const tables = {
|
||||
businesses,
|
||||
commercials,
|
||||
users,
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
/// <reference types="multer" />
|
||||
import { FileService } from '../file/file.service.js';
|
||||
export declare class AccountController {
|
||||
private fileService;
|
||||
constructor(fileService: FileService);
|
||||
uploadFile(file: Express.Multer.File, id: string): void;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export declare class AccountService {
|
||||
}
|
||||
6
bizmatch-server/dist/app.controller.d.ts
vendored
6
bizmatch-server/dist/app.controller.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
import { AppService } from './app.service.js';
|
||||
export declare class AppController {
|
||||
private readonly appService;
|
||||
constructor(appService: AppService);
|
||||
getHello(): string;
|
||||
}
|
||||
2
bizmatch-server/dist/app.module.d.ts
vendored
2
bizmatch-server/dist/app.module.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export declare class AppModule {
|
||||
}
|
||||
3
bizmatch-server/dist/app.service.d.ts
vendored
3
bizmatch-server/dist/app.service.d.ts
vendored
@@ -1,3 +0,0 @@
|
||||
export declare class AppService {
|
||||
getHello(): string;
|
||||
}
|
||||
66
bizmatch-server/dist/assets/listings.json
vendored
66
bizmatch-server/dist/assets/listings.json
vendored
@@ -1,66 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id":"1",
|
||||
"userId":"1",
|
||||
"listingsCategory": "business",
|
||||
"title": "Industrial Service Company In Corpus Christi For Sale - 1954",
|
||||
"summary": ["Asking price: $5,500,000","Sales revenue: $1,200,000","Net profit: $650,000"],
|
||||
"description": ["This company services a wide variety of industries. Asking price includes Business and the Real Estate and is approx 30,000 sq ft with room for expansion including approx 5 acres. Absentee run business."],
|
||||
"type": "2",
|
||||
"location": "Texas",
|
||||
"price":5500000,
|
||||
"salesRevenue":1200000,
|
||||
"cashFlow":650000,
|
||||
"brokerLicencing":"TREC Broker #516788",
|
||||
"established":1954,
|
||||
"realEstateIncluded":true
|
||||
},
|
||||
{
|
||||
"id":"2",
|
||||
"userId":"1",
|
||||
"listingsCategory": "business",
|
||||
"title": "Coastal Bend Manufacturing Business Plastic Injection For Sale - 1950",
|
||||
"summary": ["Asking price: $165,000","Sales revenue: Undisclosed","Net profit: Undisclosed"],
|
||||
"description": [""],
|
||||
"type": "12",
|
||||
"location": "Texas",
|
||||
"price":165000,
|
||||
"salesRevenue":null,
|
||||
"cashFlow":null,
|
||||
"brokerLicencing":"TREC Broker #516788",
|
||||
"established":1950,
|
||||
"realEstateIncluded":false
|
||||
},
|
||||
{
|
||||
"id":"3",
|
||||
"userId":"1",
|
||||
"listingsCategory": "business",
|
||||
"title": "Corner Property On Everhart South-side Corpus Christi For Sale - 1944",
|
||||
"summary": ["Asking price: $830,000","Sales revenue: Undisclosed","Net profit: Undisclosed"],
|
||||
"description": [""],
|
||||
"type": "3",
|
||||
"location": "Texas",
|
||||
"price":830000,
|
||||
"salesRevenue":null,
|
||||
"cashFlow":null,
|
||||
"brokerLicencing":"TREC Broker #516788",
|
||||
"established":1944,
|
||||
"realEstateIncluded":false
|
||||
},
|
||||
{
|
||||
"id":"4",
|
||||
"userId":"1",
|
||||
"listingsCategory": "business",
|
||||
"title": "Corpus Christi Dessert Business For Sale - 1941",
|
||||
"summary": ["Asking price: $124,900","Sales revenue: $225,000","Net profit: $50,000"],
|
||||
"description": [""],
|
||||
"type": "13",
|
||||
"location": "Texas",
|
||||
"price":830000,
|
||||
"salesRevenue":225000,
|
||||
"cashFlow":50000,
|
||||
"brokerLicencing":"TREC Broker #516788",
|
||||
"established":1941,
|
||||
"realEstateIncluded":false
|
||||
}
|
||||
]
|
||||
14
bizmatch-server/dist/assets/subscriptions.json
vendored
14
bizmatch-server/dist/assets/subscriptions.json
vendored
@@ -1,14 +0,0 @@
|
||||
[{
|
||||
"id":"1",
|
||||
"userId":"e0811669-c7eb-4e5e-a699-e8334d5c5b01",
|
||||
"level":"Business Broker",
|
||||
"start":"2024-02-12T21:54:20.603Z",
|
||||
"modified":"2024-02-12T21:54:20.603Z",
|
||||
"end":"9999-02-12T21:54:20.603Z",
|
||||
"status":"active",
|
||||
"invoices":[{
|
||||
"date":"2024-02-12T21:54:20.603Z",
|
||||
"id":"C991853B99",
|
||||
"price":0
|
||||
}]
|
||||
}]
|
||||
8
bizmatch-server/dist/file/file.service.d.ts
vendored
8
bizmatch-server/dist/file/file.service.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
/// <reference types="multer" />
|
||||
export declare class FileService {
|
||||
private subscriptions;
|
||||
constructor();
|
||||
private loadSubscriptions;
|
||||
getSubscriptions(): any;
|
||||
storeFile(file: Express.Multer.File, id: string): Promise<void>;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ListingsService } from './listings.service.js';
|
||||
import { Logger } from 'winston';
|
||||
export declare class ListingsController {
|
||||
private readonly listingsService;
|
||||
private readonly logger;
|
||||
constructor(listingsService: ListingsService, logger: Logger);
|
||||
findAll(): any;
|
||||
findById(id: string): any;
|
||||
find(criteria: any): any;
|
||||
updateById(id: string, listing: any): void;
|
||||
create(listing: any): void;
|
||||
deleteById(id: string): void;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BusinessListing, InvestmentsListing, ListingCriteria, ProfessionalsBrokersListing } from '../models/main.model.js';
|
||||
import { RedisService } from '../redis/redis.service.js';
|
||||
import { Logger } from 'winston';
|
||||
export declare class ListingsService {
|
||||
private redisService;
|
||||
private readonly logger;
|
||||
constructor(redisService: RedisService, logger: Logger);
|
||||
setListing(value: BusinessListing | ProfessionalsBrokersListing | InvestmentsListing, id?: string): Promise<void>;
|
||||
getListingById(id: string): Promise<any>;
|
||||
deleteListing(id: string): void;
|
||||
getAllListings(start?: number, end?: number): Promise<any>;
|
||||
find(criteria: ListingCriteria): Promise<any>;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { MailService } from './mail.service.js';
|
||||
import { MailInfo } from '../models/server.model.js';
|
||||
export declare class MailController {
|
||||
private mailService;
|
||||
constructor(mailService: MailService);
|
||||
sendEMail(id: string, mailInfo: MailInfo): any;
|
||||
}
|
||||
2
bizmatch-server/dist/mail/mail.module.d.ts
vendored
2
bizmatch-server/dist/mail/mail.module.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export declare class MailModule {
|
||||
}
|
||||
9
bizmatch-server/dist/mail/mail.service.d.ts
vendored
9
bizmatch-server/dist/mail/mail.service.d.ts
vendored
@@ -1,9 +0,0 @@
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { AuthService } from '../auth/auth.service.js';
|
||||
import { MailInfo } from '../models/server.model.js';
|
||||
export declare class MailService {
|
||||
private mailerService;
|
||||
private authService;
|
||||
constructor(mailerService: MailerService, authService: AuthService);
|
||||
sendInquiry(userId: string, mailInfo: MailInfo): Promise<void>;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
<p>Hey {{ name }},</p>
|
||||
<p>You got an inquiry a</p>
|
||||
<p>
|
||||
{{inquiry}}
|
||||
</p>
|
||||
1
bizmatch-server/dist/main.d.ts
vendored
1
bizmatch-server/dist/main.d.ts
vendored
@@ -1 +0,0 @@
|
||||
export {};
|
||||
126
bizmatch-server/dist/models/main.model.d.ts
vendored
126
bizmatch-server/dist/models/main.model.d.ts
vendored
@@ -1,126 +0,0 @@
|
||||
export interface KeyValue {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
export interface KeyValueStyle {
|
||||
name: string;
|
||||
value: string;
|
||||
icon: string;
|
||||
bgColorClass: string;
|
||||
textColorClass: string;
|
||||
}
|
||||
export type SelectOption<T = number> = {
|
||||
value: T;
|
||||
label: string;
|
||||
};
|
||||
export interface Listing {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
description: Array<string>;
|
||||
location: string;
|
||||
favoritesForUser: Array<string>;
|
||||
hideImage?: boolean;
|
||||
created: Date;
|
||||
updated: Date;
|
||||
}
|
||||
export interface BusinessListing extends Listing {
|
||||
listingsCategory: 'business';
|
||||
summary: Array<string>;
|
||||
type: string;
|
||||
price?: number;
|
||||
realEstateIncluded?: boolean;
|
||||
salesRevenue?: number;
|
||||
cashFlow?: number;
|
||||
netProfit?: number;
|
||||
inventory?: string;
|
||||
employees?: number;
|
||||
established?: number;
|
||||
reasonForSale?: string;
|
||||
brokerLicencing?: string;
|
||||
internals?: string;
|
||||
}
|
||||
export interface ProfessionalsBrokersListing extends Listing {
|
||||
listingsCategory: 'professionals_brokers';
|
||||
summary: string;
|
||||
address?: string;
|
||||
email?: string;
|
||||
website?: string;
|
||||
category?: 'Professionals' | 'Broker';
|
||||
}
|
||||
export interface InvestmentsListing extends Listing {
|
||||
listingsCategory: 'investment';
|
||||
email?: string;
|
||||
website?: string;
|
||||
phoneNumber?: string;
|
||||
}
|
||||
export type ListingType = BusinessListing | ProfessionalsBrokersListing | InvestmentsListing;
|
||||
export interface ListingCriteria {
|
||||
type: string;
|
||||
location: string;
|
||||
minPrice: string;
|
||||
maxPrice: string;
|
||||
realEstateChecked: boolean;
|
||||
listingsCategory: 'business' | 'professionals_brokers' | 'investment';
|
||||
category: 'professional|broker';
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
}
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
userId: string;
|
||||
level: string;
|
||||
start: Date;
|
||||
modified: Date;
|
||||
end: Date;
|
||||
status: string;
|
||||
invoices: Array<Invoice>;
|
||||
}
|
||||
export interface Invoice {
|
||||
id: string;
|
||||
date: Date;
|
||||
price: number;
|
||||
}
|
||||
export interface JwtToken {
|
||||
exp: number;
|
||||
iat: number;
|
||||
auth_time: number;
|
||||
jti: string;
|
||||
iss: string;
|
||||
aud: string;
|
||||
sub: string;
|
||||
typ: string;
|
||||
azp: string;
|
||||
nonce: string;
|
||||
session_state: string;
|
||||
acr: string;
|
||||
realm_access: Realmaccess;
|
||||
resource_access: Resourceaccess;
|
||||
scope: string;
|
||||
sid: string;
|
||||
email_verified: boolean;
|
||||
name: string;
|
||||
preferred_username: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
email: string;
|
||||
user_id: string;
|
||||
}
|
||||
interface Resourceaccess {
|
||||
account: Realmaccess;
|
||||
}
|
||||
interface Realmaccess {
|
||||
roles: string[];
|
||||
}
|
||||
export interface PageEvent {
|
||||
first: number;
|
||||
rows: number;
|
||||
page: number;
|
||||
pageCount: number;
|
||||
}
|
||||
export {};
|
||||
129
bizmatch-server/dist/models/main.model.ts
vendored
129
bizmatch-server/dist/models/main.model.ts
vendored
@@ -1,129 +0,0 @@
|
||||
export interface KeyValue {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
export interface KeyValueStyle {
|
||||
name: string;
|
||||
value: string;
|
||||
icon:string;
|
||||
bgColorClass:string;
|
||||
textColorClass:string;
|
||||
}
|
||||
export type SelectOption<T = number> = {
|
||||
value: T;
|
||||
label: string;
|
||||
};
|
||||
export interface Listing {
|
||||
id: string;
|
||||
userId: string;
|
||||
title: string;
|
||||
description: Array<string>;
|
||||
location: string;//enum
|
||||
favoritesForUser:Array<string>;
|
||||
hideImage?:boolean;
|
||||
created:Date;
|
||||
updated:Date;
|
||||
}
|
||||
export interface BusinessListing extends Listing {
|
||||
listingsCategory: 'business'; //enum
|
||||
summary: Array<string>;
|
||||
type: string; //enum
|
||||
price?: number;
|
||||
realEstateIncluded?: boolean;
|
||||
salesRevenue?: number;
|
||||
cashFlow?: number;
|
||||
netProfit?: number;
|
||||
inventory?: string;
|
||||
employees?: number;
|
||||
established?: number;
|
||||
reasonForSale?: string;
|
||||
brokerLicencing?: string;
|
||||
internals?: string;
|
||||
}
|
||||
export interface ProfessionalsBrokersListing extends Listing {
|
||||
listingsCategory: 'professionals_brokers'; //enum
|
||||
summary: string;
|
||||
address?: string;
|
||||
email?: string;
|
||||
website?: string;
|
||||
category?: 'Professionals' | 'Broker';
|
||||
}
|
||||
export interface InvestmentsListing extends Listing {
|
||||
listingsCategory: 'investment'; //enum
|
||||
email?: string;
|
||||
website?: string;
|
||||
phoneNumber?: string;
|
||||
}
|
||||
export type ListingType =
|
||||
| BusinessListing
|
||||
| ProfessionalsBrokersListing
|
||||
| InvestmentsListing;
|
||||
|
||||
export interface ListingCriteria {
|
||||
type:string,
|
||||
location:string,
|
||||
minPrice:string,
|
||||
maxPrice:string,
|
||||
realEstateChecked:boolean,
|
||||
listingsCategory:'business'|'professionals_brokers'|'investment',
|
||||
category:'professional|broker'
|
||||
}
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
}
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
userId:string
|
||||
level: string;
|
||||
start: Date;
|
||||
modified: Date;
|
||||
end: Date;
|
||||
status: string;
|
||||
invoices: Array<Invoice>;
|
||||
}
|
||||
export interface Invoice {
|
||||
id: string,
|
||||
date: Date,
|
||||
price: number
|
||||
}
|
||||
export interface JwtToken {
|
||||
exp: number;
|
||||
iat: number;
|
||||
auth_time: number;
|
||||
jti: string;
|
||||
iss: string;
|
||||
aud: string;
|
||||
sub: string;
|
||||
typ: string;
|
||||
azp: string;
|
||||
nonce: string;
|
||||
session_state: string;
|
||||
acr: string;
|
||||
realm_access: Realmaccess;
|
||||
resource_access: Resourceaccess;
|
||||
scope: string;
|
||||
sid: string;
|
||||
email_verified: boolean;
|
||||
name: string;
|
||||
preferred_username: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
email: string;
|
||||
user_id: string;
|
||||
}
|
||||
interface Resourceaccess {
|
||||
account: Realmaccess;
|
||||
}
|
||||
interface Realmaccess {
|
||||
roles: string[];
|
||||
}
|
||||
export interface PageEvent {
|
||||
first: number;
|
||||
rows: number;
|
||||
page: number;
|
||||
pageCount: number;
|
||||
}
|
||||
11
bizmatch-server/dist/models/server.model.d.ts
vendored
11
bizmatch-server/dist/models/server.model.d.ts
vendored
@@ -1,11 +0,0 @@
|
||||
export interface MailInfo {
|
||||
sender: Sender;
|
||||
userId: string;
|
||||
}
|
||||
export interface Sender {
|
||||
name: string;
|
||||
email: string;
|
||||
phoneNumber: string;
|
||||
state: string;
|
||||
comments: string;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { RedisService } from './redis.service.js';
|
||||
export declare class RedisController {
|
||||
private redisService;
|
||||
constructor(redisService: RedisService);
|
||||
}
|
||||
2
bizmatch-server/dist/redis/redis.module.d.ts
vendored
2
bizmatch-server/dist/redis/redis.module.d.ts
vendored
@@ -1,2 +0,0 @@
|
||||
export declare class RedisModule {
|
||||
}
|
||||
11
bizmatch-server/dist/redis/redis.service.d.ts
vendored
11
bizmatch-server/dist/redis/redis.service.d.ts
vendored
@@ -1,11 +0,0 @@
|
||||
export declare const LISTINGS = "LISTINGS";
|
||||
export declare const SUBSCRIPTIONS = "SUBSCRIPTIONS";
|
||||
export declare const USERS = "USERS";
|
||||
export declare class RedisService {
|
||||
private redis;
|
||||
setJson(id: string, value: any): Promise<void>;
|
||||
delete(id: string): Promise<void>;
|
||||
getJson(id: string, prefix: string): Promise<any>;
|
||||
getId(prefix: 'LISTINGS' | 'SUBSCRIPTIONS' | 'USERS'): Promise<string>;
|
||||
search(prefix: 'LISTINGS' | 'SUBSCRIPTIONS' | 'USERS', clause: string): Promise<any>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { SelectOptionsService } from './select-options.service.js';
|
||||
export declare class SelectOptionsController {
|
||||
private selectOptionsService;
|
||||
constructor(selectOptionsService: SelectOptionsService);
|
||||
getSelectOption(): any;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { KeyValue, KeyValueStyle } from '../models/main.model.js';
|
||||
export declare class SelectOptionsService {
|
||||
constructor();
|
||||
typesOfBusiness: Array<KeyValueStyle>;
|
||||
prices: Array<KeyValue>;
|
||||
listingCategories: Array<KeyValue>;
|
||||
categories: Array<KeyValueStyle>;
|
||||
private usStates;
|
||||
locations: Array<any>;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { FileService } from '../file/file.service.js';
|
||||
export declare class SubscriptionsController {
|
||||
private readonly fileService;
|
||||
constructor(fileService: FileService);
|
||||
findAll(): any;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
1
bizmatch-server/dist/utils.d.ts
vendored
1
bizmatch-server/dist/utils.d.ts
vendored
@@ -1 +0,0 @@
|
||||
export declare function convertStringToNullUndefined(value: any): any;
|
||||
12
bizmatch-server/drizzle.config.ts
Normal file
12
bizmatch-server/drizzle.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
export default defineConfig({
|
||||
schema: './src/drizzle/schema.ts',
|
||||
out: './src/drizzle/migrations',
|
||||
dialect: 'postgresql',
|
||||
// driver: 'pg',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
1075
bizmatch-server/importlog.txt
Normal file
1075
bizmatch-server/importlog.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,10 @@
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"assets": ["assets/**/*","**/*.hbs"],
|
||||
"assets": [
|
||||
"assets/**/*",
|
||||
"**/*.hbs"
|
||||
],
|
||||
"watchAssets": true
|
||||
}
|
||||
}
|
||||
@@ -9,16 +9,21 @@
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start": "HOST_NAME=localhost nest start",
|
||||
"start:dev": "HOST_NAME=dev.bizmatch.net nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"start:prod": "HOST_NAME=www.bizmatch.net node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"generate": "drizzle-kit generate",
|
||||
"drop": "drizzle-kit drop",
|
||||
"migrate": "tsx src/drizzle/migrate.ts",
|
||||
"import": "tsx src/drizzle/import.ts",
|
||||
"generateTypes": "tsx src/drizzle/generateTypes.ts src/drizzle/schema.ts src/models/db.model.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.10.3",
|
||||
@@ -29,22 +34,35 @@
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
"drizzle-orm": "^0.32.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"groq-sdk": "^0.5.0",
|
||||
"handlebars": "^4.7.8",
|
||||
"ioredis": "^5.3.2",
|
||||
"ky": "^1.2.0",
|
||||
"jwks-rsa": "^3.1.0",
|
||||
"ky": "^1.4.0",
|
||||
"nest-winston": "^1.9.4",
|
||||
"nodemailer": "^6.9.10",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"openai": "^4.52.6",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"pg": "^8.11.5",
|
||||
"pgvector": "^0.2.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sharp": "^0.33.2",
|
||||
"tsx": "^4.16.2",
|
||||
"urlcat": "^3.1.0",
|
||||
"winston": "^3.11.0"
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.24.4",
|
||||
"@babel/traverse": "^7.24.1",
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
@@ -56,19 +74,26 @@
|
||||
"@types/passport-google-oauth20": "^2.0.14",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"@types/passport-local": "^1.0.38",
|
||||
"@types/pg": "^8.11.5",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"commander": "^12.0.0",
|
||||
"drizzle-kit": "^0.23.0",
|
||||
"esbuild-register": "^3.5.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.5.0",
|
||||
"kysely-codegen": "^0.15.0",
|
||||
"pg-to-ts": "^4.1.1",
|
||||
"prettier": "^3.0.0",
|
||||
"rimraf": "^5.0.5",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
},
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Controller, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
|
||||
@Controller('account')
|
||||
export class AccountController {
|
||||
constructor(private fileService:FileService){}
|
||||
|
||||
@Post('uploadPhoto/:id')
|
||||
@UseInterceptors(FileInterceptor('file'),)
|
||||
uploadFile(@UploadedFile() file: Express.Multer.File,@Param('id') id:string) {
|
||||
this.fileService.storeFile(file,id);
|
||||
}
|
||||
}
|
||||
12
bizmatch-server/src/ai/ai.controller.ts
Normal file
12
bizmatch-server/src/ai/ai.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AiService } from './ai.service.js';
|
||||
|
||||
@Controller('ai')
|
||||
export class AiController {
|
||||
constructor(private readonly aiService: AiService) {}
|
||||
|
||||
@Post()
|
||||
async getBusinessCriteria(@Body('query') query: string) {
|
||||
return this.aiService.getBusinessCriteria(query);
|
||||
}
|
||||
}
|
||||
9
bizmatch-server/src/ai/ai.module.ts
Normal file
9
bizmatch-server/src/ai/ai.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AiController } from './ai.controller.js';
|
||||
import { AiService } from './ai.service.js';
|
||||
|
||||
@Module({
|
||||
controllers: [AiController],
|
||||
providers: [AiService],
|
||||
})
|
||||
export class AiModule {}
|
||||
94
bizmatch-server/src/ai/ai.service.ts
Normal file
94
bizmatch-server/src/ai/ai.service.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import Groq from 'groq-sdk';
|
||||
import OpenAI from 'openai';
|
||||
import { BusinessListingCriteria } from '../models/main.model';
|
||||
|
||||
const businessListingCriteriaStructure = {
|
||||
criteriaType: 'business | commercialProperty | broker',
|
||||
types: "'Automotive'|'Industrial Services'|'Food and Restaurant'|'Real Estate'|'Retail'|'Oilfield SVE and MFG.'|'Service'|'Advertising'|'Agriculture'|'Franchise'|'Professional'|'Manufacturing'",
|
||||
city: 'string',
|
||||
state: 'string',
|
||||
county: 'string',
|
||||
minPrice: 'number',
|
||||
maxPrice: 'number',
|
||||
minRevenue: 'number',
|
||||
maxRevenue: 'number',
|
||||
minCashFlow: 'number',
|
||||
maxCashFlow: 'number',
|
||||
minNumberEmployees: 'number',
|
||||
maxNumberEmployees: 'number',
|
||||
establishedSince: 'number',
|
||||
establishedUntil: 'number',
|
||||
realEstateChecked: 'boolean',
|
||||
leasedLocation: 'boolean',
|
||||
franchiseResale: 'boolean',
|
||||
title: 'string',
|
||||
brokerName: 'string',
|
||||
searchType: "'exact' | 'radius'",
|
||||
radius: "'0' | '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500'",
|
||||
};
|
||||
@Injectable()
|
||||
export class AiService {
|
||||
private readonly openai: OpenAI;
|
||||
private readonly groq: Groq;
|
||||
constructor() {
|
||||
this.openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY, // Verwenden Sie Umgebungsvariablen für den API-Schlüssel
|
||||
});
|
||||
this.groq = new Groq({ apiKey: process.env.GROQ_API_KEY });
|
||||
}
|
||||
|
||||
async getBusinessCriteria(query: string): Promise<BusinessListingCriteria> {
|
||||
// const prompt = `
|
||||
// Dieses Objekt ist wie folgt definiert: ${JSON.stringify(businessListingCriteriaStructure)}.
|
||||
// Die Antwort darf nur das von dir befüllte JSON als unformatierten Text enthalten so das es von mir mit JSON.parse() einlesbar ist!!!!
|
||||
// Falls es Ortsangaben gibt, dann befülle City, County und State wenn möglich Die Suchanfrage des Users lautet: "${query}"`;
|
||||
const prompt = `The Search Query of the User is: "${query}"`;
|
||||
let response = null;
|
||||
try {
|
||||
// response = await this.openai.chat.completions.create({
|
||||
// model: 'gpt-4o-mini',
|
||||
// //model: 'gpt-3.5-turbo',
|
||||
// max_tokens: 300,
|
||||
// messages: [
|
||||
// {
|
||||
// role: 'system',
|
||||
// content: `Please create unformatted JSON Object from a user input.
|
||||
// The type is: ${JSON.stringify(businessListingCriteriaStructure)}.,
|
||||
// If location details available please fill city, county and state as State Code`,
|
||||
// },
|
||||
// ],
|
||||
// temperature: 0.5,
|
||||
// response_format: { type: 'json_object' },
|
||||
// });
|
||||
|
||||
response = await this.groq.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: `Please create unformatted JSON Object from a user input.
|
||||
The type must be: ${JSON.stringify(businessListingCriteriaStructure)}.
|
||||
If location details available please fill city, county and state as State Code`,
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
model: 'llama-3.1-70b-versatile',
|
||||
//model: 'llama-3.1-8b-instant',
|
||||
temperature: 0.2,
|
||||
max_tokens: 300,
|
||||
response_format: { type: 'json_object' },
|
||||
});
|
||||
|
||||
const generatedCriteria = JSON.parse(response.choices[0]?.message?.content);
|
||||
return generatedCriteria;
|
||||
|
||||
// return response.choices[0]?.message?.content;
|
||||
} catch (error) {
|
||||
console.error(`Error calling GPT-4 API: ${response.choices[0]}`, error);
|
||||
throw new Error('Failed to generate business criteria');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
|
||||
import { AppService } from './app.service.js';
|
||||
import { AuthService } from './auth/auth.service.js';
|
||||
import { JwtAuthGuard } from './jwt-auth/jwt-auth.guard.js';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
constructor(
|
||||
private readonly appService: AppService,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
getHello(@Request() req): string {
|
||||
return req.user;
|
||||
//return 'dfgdf';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,51 +1,86 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import * as dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import { WinstonModule, utilities as nestWinstonModuleUtilities } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
import { AiModule } from './ai/ai.module.js';
|
||||
import { AppController } from './app.controller.js';
|
||||
import { AppService } from './app.service.js';
|
||||
import { ListingsController } from './listings/listings.controller.js';
|
||||
import { FileService } from './file/file.service.js';
|
||||
import { AuthService } from './auth/auth.service.js';
|
||||
import { AuthController } from './auth/auth.controller.js';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { SelectOptionsController } from './select-options/select-options.controller.js';
|
||||
import { SelectOptionsService } from './select-options/select-options.service.js';
|
||||
import { SubscriptionsController } from './subscriptions/subscriptions.controller.js';
|
||||
import { RedisModule } from './redis/redis.module.js';
|
||||
import { ListingsService } from './listings/listings.service.js';
|
||||
import { AccountController } from './account/account.controller.js';
|
||||
import { AccountService } from './account/account.service.js';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
|
||||
import * as winston from 'winston';
|
||||
import { MailModule } from './mail/mail.module.js';
|
||||
import { AuthModule } from './auth/auth.module.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
import { FileService } from './file/file.service.js';
|
||||
import { GeoModule } from './geo/geo.module.js';
|
||||
import { ImageModule } from './image/image.module.js';
|
||||
import { ListingsModule } from './listings/listings.module.js';
|
||||
import { MailModule } from './mail/mail.module.js';
|
||||
import { RequestDurationMiddleware } from './request-duration/request-duration.middleware.js';
|
||||
import { SelectOptionsModule } from './select-options/select-options.module.js';
|
||||
import { UserModule } from './user/user.module.js';
|
||||
// const __filename = fileURLToPath(import.meta.url);
|
||||
// const __dirname = path.dirname(__filename);
|
||||
|
||||
function loadEnvFiles() {
|
||||
// Load the .env file
|
||||
dotenv.config();
|
||||
console.log('Loaded .env file');
|
||||
|
||||
// Determine which additional env file to load
|
||||
let envFilePath = '';
|
||||
const host = process.env.HOST_NAME || '';
|
||||
|
||||
if (host.includes('localhost')) {
|
||||
envFilePath = '.env.local';
|
||||
} else if (host.includes('dev.bizmatch.net')) {
|
||||
envFilePath = '.env.dev';
|
||||
} else if (host.includes('www.bizmatch.net') || host.includes('bizmatch.net')) {
|
||||
envFilePath = '.env.prod';
|
||||
}
|
||||
|
||||
// Load the additional env file if it exists
|
||||
if (fs.existsSync(envFilePath)) {
|
||||
dotenv.config({ path: envFilePath });
|
||||
console.log(`Loaded ${envFilePath} file`);
|
||||
} else {
|
||||
console.log(`No additional .env file found for HOST_NAME: ${host}`);
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFiles();
|
||||
@Module({
|
||||
imports: [ConfigModule.forRoot(), RedisModule, MailModule, AuthModule,
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '..', 'public'), // `public` ist das Verzeichnis, wo Ihre statischen Dateien liegen
|
||||
}),
|
||||
imports: [
|
||||
ConfigModule.forRoot({ isGlobal: true }),
|
||||
MailModule,
|
||||
AuthModule,
|
||||
WinstonModule.forRoot({
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.ms(),
|
||||
nestWinstonModuleUtilities.format.nestLike('Bizmatch', {
|
||||
colors: true,
|
||||
prettyPrint: true,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
// other transports...
|
||||
],
|
||||
// other options
|
||||
})
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.ms(),
|
||||
nestWinstonModuleUtilities.format.nestLike('Bizmatch', {
|
||||
colors: true,
|
||||
prettyPrint: true,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
// other transports...
|
||||
],
|
||||
// other options
|
||||
}),
|
||||
GeoModule,
|
||||
UserModule,
|
||||
ListingsModule,
|
||||
SelectOptionsModule,
|
||||
ImageModule,
|
||||
PassportModule,
|
||||
AiModule,
|
||||
],
|
||||
controllers: [AppController, ListingsController, SelectOptionsController, SubscriptionsController, AccountController],
|
||||
providers: [AppService, FileService, SelectOptionsService, ListingsService, AccountService],
|
||||
controllers: [AppController],
|
||||
providers: [AppService, FileService],
|
||||
})
|
||||
export class AppModule {}
|
||||
export class AppModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(RequestDurationMiddleware).forRoutes('*');
|
||||
}
|
||||
}
|
||||
|
||||
1326
bizmatch-server/src/assets/broker.json
Normal file
1326
bizmatch-server/src/assets/broker.json
Normal file
File diff suppressed because it is too large
Load Diff
5018
bizmatch-server/src/assets/businesses.json
Normal file
5018
bizmatch-server/src/assets/businesses.json
Normal file
File diff suppressed because it is too large
Load Diff
1402
bizmatch-server/src/assets/commercials.json
Normal file
1402
bizmatch-server/src/assets/commercials.json
Normal file
File diff suppressed because it is too large
Load Diff
3597
bizmatch-server/src/assets/counties.json
Normal file
3597
bizmatch-server/src/assets/counties.json
Normal file
File diff suppressed because it is too large
Load Diff
63211
bizmatch-server/src/assets/counties_raw.csv
Normal file
63211
bizmatch-server/src/assets/counties_raw.csv
Normal file
File diff suppressed because it is too large
Load Diff
119805
bizmatch-server/src/assets/geo.json
Normal file
119805
bizmatch-server/src/assets/geo.json
Normal file
File diff suppressed because it is too large
Load Diff
119805
bizmatch-server/src/assets/geo1.json
Normal file
119805
bizmatch-server/src/assets/geo1.json
Normal file
File diff suppressed because it is too large
Load Diff
119811
bizmatch-server/src/assets/geo_.json
Normal file
119811
bizmatch-server/src/assets/geo_.json
Normal file
File diff suppressed because it is too large
Load Diff
40
bizmatch-server/src/auth/auth.controller.ts
Normal file
40
bizmatch-server/src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Controller, Get, Param, Put } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service.js';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@Get()
|
||||
getAccessToken(): any {
|
||||
return this.authService.getAccessToken();
|
||||
}
|
||||
|
||||
@Get('users')
|
||||
getUsers(): any {
|
||||
return this.authService.getUsers();
|
||||
}
|
||||
@Get('user/:userid')
|
||||
getUser(@Param('userid') userId: string): any {
|
||||
return this.authService.getUser(userId);
|
||||
}
|
||||
@Get('groups')
|
||||
getGroups(): any {
|
||||
return this.authService.getGroups();
|
||||
}
|
||||
|
||||
@Get('user/:userid/groups') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
|
||||
getGroupsForUsers(@Param('userid') userId: string): any {
|
||||
return this.authService.getGroupsForUser(userId);
|
||||
}
|
||||
|
||||
@Get('user/:userid/lastlogin') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth
|
||||
getLastLogin(@Param('userid') userId: string): any {
|
||||
return this.authService.getLastLogin(userId);
|
||||
}
|
||||
|
||||
@Put('user/:userid/group/:groupid') //e0811669-c7eb-4e5e-a699-e8334d5c5b01 -> aknuth //
|
||||
addUser2Group(@Param('userid') userId: string,@Param('groupid') groupId: string): any {
|
||||
return this.authService.addUser2Group(userId,groupId);
|
||||
}
|
||||
}
|
||||
13
bizmatch-server/src/auth/auth.module.ts
Normal file
13
bizmatch-server/src/auth/auth.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { JwtStrategy } from '../jwt.strategy.js';
|
||||
import { AuthController } from './auth.controller.js';
|
||||
import { AuthService } from './auth.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [PassportModule],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
119
bizmatch-server/src/auth/auth.service.ts
Normal file
119
bizmatch-server/src/auth/auth.service.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import ky from 'ky';
|
||||
import urlcat from 'urlcat';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
public async getAccessToken() {
|
||||
const form = new FormData();
|
||||
form.append('grant_type', 'password');
|
||||
form.append('username', process.env.user);
|
||||
form.append('password', process.env.password);
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('grant_type', 'password');
|
||||
params.append('username', process.env.user);
|
||||
params.append('password', process.env.password);
|
||||
const URL = `${process.env.host}${process.env.tokenURL}`;
|
||||
|
||||
const response = await ky
|
||||
.post(URL, {
|
||||
body: params.toString(),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: 'Basic YWRtaW4tY2xpOnE0RmJnazFkd0NaelFQZmt5VzhhM3NnckV5UHZlRUY3',
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return (<any>response).access_token;
|
||||
} catch (error) {
|
||||
if (error.name === 'HTTPError') {
|
||||
const errorJson = await error.response.json();
|
||||
console.error('Fehlerantwort vom Server:', errorJson);
|
||||
} else {
|
||||
console.error('Allgemeiner Fehler:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async getUsers() {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.usersURL}`;
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async getUser(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.userURL, { userid });
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async getGroups() {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = `${process.env.host}${process.env.groupsURL}`;
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
|
||||
public async getGroupsForUser(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.userGroupsURL, { userid });
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async getLastLogin(userid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.lastLoginURL, { userid });
|
||||
const response = await ky
|
||||
.get(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
public async addUser2Group(userid: string, groupid: string) {
|
||||
const token = await this.getAccessToken();
|
||||
const URL = urlcat(process.env.host, process.env.addUser2GroupURL, { userid, groupid });
|
||||
const response = await ky
|
||||
.put(URL, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
27
bizmatch-server/src/drizzle/drizzle.module.ts
Normal file
27
bizmatch-server/src/drizzle/drizzle.module.ts
Normal 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 './schema.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, logger:true });
|
||||
},
|
||||
},
|
||||
],
|
||||
exports: [PG_CONNECTION],
|
||||
})
|
||||
export class DrizzleModule {}
|
||||
143
bizmatch-server/src/drizzle/generateTypes.ts
Normal file
143
bizmatch-server/src/drizzle/generateTypes.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import fs from 'fs';
|
||||
import ts from 'typescript';
|
||||
export const TABLE_BO_MAPPING = {
|
||||
'users':'User',
|
||||
'businesses':'BusinessListing',
|
||||
'commercials':'CommercialPropertyListing'
|
||||
}
|
||||
function generateInterfaceDefinitions(inputFile: string, outputFile: string): void {
|
||||
const sourceFile = ts.createSourceFile(
|
||||
inputFile,
|
||||
fs.readFileSync(inputFile, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
);
|
||||
|
||||
let interfaceDefinitions = '';
|
||||
|
||||
ts.forEachChild(sourceFile, (node) => {
|
||||
if (ts.isVariableStatement(node)) {
|
||||
const variableDeclaration = node.declarationList.declarations[0];
|
||||
if (variableDeclaration.initializer && ts.isCallExpression(variableDeclaration.initializer)) {
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (initializer.expression.getText(sourceFile) === 'pgTable') {
|
||||
const tableName = initializer.arguments[0].getText(sourceFile).slice(1, -1); // Entfernen von zusätzlichen Anführungszeichen
|
||||
const tableDefinition = initializer.arguments[1];
|
||||
|
||||
const interfaceName = TABLE_BO_MAPPING[tableName] ? TABLE_BO_MAPPING[tableName] : tableName.charAt(0).toUpperCase() + tableName.slice(1);
|
||||
let interfaceDefinition = `export interface ${interfaceName} {\n`;
|
||||
|
||||
ts.forEachChild(tableDefinition, (propertyNode) => {
|
||||
if (ts.isPropertyAssignment(propertyNode)) {
|
||||
const propertyName = propertyNode.name.getText(sourceFile);
|
||||
const propertyDefinition = propertyNode.initializer;
|
||||
|
||||
if (ts.isCallExpression(propertyDefinition)) {
|
||||
const propertyType = getPropertyType(propertyDefinition, sourceFile);
|
||||
const isOptional = !hasNonOptionalModifier(propertyDefinition.expression);
|
||||
interfaceDefinition += ` ${propertyName}${isOptional ? '?' : ''}: ${propertyType};\n`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
interfaceDefinition += '}\n\n';
|
||||
interfaceDefinitions += interfaceDefinition;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
fs.writeFileSync(outputFile, interfaceDefinitions);
|
||||
console.log(`Interface definitions generated successfully. Output file: ${outputFile}`);
|
||||
}
|
||||
|
||||
function getPropertyType(propertyDefinition: ts.CallExpression, sourceFile: ts.SourceFile): string {
|
||||
const typeFunction = getTypeFunctionName(propertyDefinition.expression, sourceFile);
|
||||
|
||||
let propertyType = '';
|
||||
|
||||
switch (typeFunction) {
|
||||
case 'varchar':
|
||||
case 'char':
|
||||
case 'text':
|
||||
case 'uuid':
|
||||
propertyType = 'string';
|
||||
break;
|
||||
case 'integer':
|
||||
case 'numeric':
|
||||
case 'real':
|
||||
case 'doublePrecision':
|
||||
propertyType = 'number';
|
||||
break;
|
||||
case 'boolean':
|
||||
propertyType = 'boolean';
|
||||
break;
|
||||
case 'timestamp':
|
||||
propertyType = 'Date';
|
||||
break;
|
||||
default:
|
||||
propertyType = 'any';
|
||||
}
|
||||
|
||||
const isArray = hasArrayModifier(propertyDefinition.expression);
|
||||
return isArray ? `${propertyType}[]` : propertyType;
|
||||
}
|
||||
|
||||
function getTypeFunctionName(expression: ts.Expression, sourceFile: ts.SourceFile): string {
|
||||
// Prüfen, ob die aktuelle Expression ein Identifier ist (SyntaxKind.Identifier hat den Wert 80)
|
||||
if (expression.kind === ts.SyntaxKind.Identifier) {
|
||||
return expression.getText(sourceFile);
|
||||
}
|
||||
|
||||
// Wenn die Expression eine CallExpression oder eine PropertyAccessExpression ist,
|
||||
// gehe zur nächsten Expression-Ebene
|
||||
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
|
||||
return getTypeFunctionName(expression.expression, sourceFile);
|
||||
}
|
||||
|
||||
// Falls ein nicht unterstützter Expression-Typ vorliegt, gibt 'unknown' zurück
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function hasArrayModifier(expression: ts.Expression): boolean {
|
||||
// Prüfe, ob die aktuelle Expression eine CallExpression ist und der Funktionsname 'array' ist
|
||||
if (ts.isPropertyAccessExpression(expression) && expression.name.getText() === 'array') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wenn die Expression eine weitere CallExpression oder PropertyAccessExpression ist,
|
||||
// prüfe rekursiv die nächste Ebene
|
||||
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
|
||||
return hasArrayModifier(expression.expression);
|
||||
}
|
||||
|
||||
// Wenn keine weitere Ebene oder kein Array-Modifier gefunden wurde, gib false zurück
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasNonOptionalModifier(expression: ts.Expression): boolean {
|
||||
// Prüfe, ob die aktuelle Expression eine CallExpression ist und der Funktionsname 'notNull' ist
|
||||
if (ts.isPropertyAccessExpression(expression) && (expression.name.getText() === 'notNull' || expression.name.getText() === 'primaryKey')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Wenn die Expression eine weitere CallExpression oder PropertyAccessExpression ist,
|
||||
// prüfe rekursiv die nächste Ebene
|
||||
if (ts.isCallExpression(expression) || ts.isPropertyAccessExpression(expression)) {
|
||||
return hasNonOptionalModifier(expression.expression);
|
||||
}
|
||||
|
||||
// Wenn keine weitere Ebene oder kein NotNull-Modifier gefunden wurde, gib false zurück
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hauptprogramm
|
||||
const inputFile = process.argv[2]|| './src/drizzle/schema.ts';
|
||||
const outputFile = process.argv[3] || './model.ts';
|
||||
|
||||
if (!inputFile) {
|
||||
console.error('Please provide an input file.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
generateInterfaceDefinitions(inputFile, outputFile);
|
||||
345
bizmatch-server/src/drizzle/import.ts
Normal file
345
bizmatch-server/src/drizzle/import.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
import OpenAI from 'openai';
|
||||
import { join } from 'path';
|
||||
import pkg from 'pg';
|
||||
import { rimraf } from 'rimraf';
|
||||
import sharp from 'sharp';
|
||||
import { BusinessListingService } from 'src/listings/business-listing.service.js';
|
||||
import { CommercialPropertyService } from 'src/listings/commercial-property.service.js';
|
||||
import { Geo } from 'src/models/server.model.js';
|
||||
import winston from 'winston';
|
||||
import { User, UserData } from '../models/db.model.js';
|
||||
import { createDefaultBusinessListing, createDefaultCommercialPropertyListing, createDefaultUser, emailToDirName, KeyValueStyle } from '../models/main.model.js';
|
||||
import { SelectOptionsService } from '../select-options/select-options.service.js';
|
||||
import { convertUserToDrizzleUser } from '../utils.js';
|
||||
import * as schema from './schema.js';
|
||||
interface PropertyImportListing {
|
||||
id: string;
|
||||
userId: string;
|
||||
listingsCategory: 'commercialProperty';
|
||||
title: string;
|
||||
state: string;
|
||||
hasImages: boolean;
|
||||
price: number;
|
||||
city: string;
|
||||
description: string;
|
||||
type: number;
|
||||
imageOrder: any[];
|
||||
}
|
||||
interface BusinessImportListing {
|
||||
userId: string;
|
||||
listingsCategory: 'business';
|
||||
title: string;
|
||||
description: string;
|
||||
type: number;
|
||||
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: string;
|
||||
}
|
||||
const typesOfBusiness: Array<KeyValueStyle> = [
|
||||
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||
{ name: 'Real Estate', value: '3', icon: 'fa-solid fa-building', textColorClass: 'text-blue-400' },
|
||||
{ name: 'Uncategorized', value: '4', icon: 'fa-solid fa-question', textColorClass: 'text-cyan-400' },
|
||||
{ name: 'Retail', value: '5', icon: 'fa-solid fa-money-bill-wave', textColorClass: 'text-pink-400' },
|
||||
{ name: 'Oilfield SVE and MFG.', value: '6', icon: 'fa-solid fa-oil-well', textColorClass: 'text-indigo-400' },
|
||||
{ name: 'Service', value: '7', icon: 'fa-solid fa-umbrella', textColorClass: 'text-teal-400' },
|
||||
{ name: 'Advertising', value: '8', icon: 'fa-solid fa-rectangle-ad', textColorClass: 'text-orange-400' },
|
||||
{ name: 'Agriculture', value: '9', icon: 'fa-solid fa-wheat-awn', textColorClass: 'text-sky-400' },
|
||||
{ name: 'Franchise', value: '10', icon: 'fa-solid fa-star', textColorClass: 'text-purple-400' },
|
||||
{ name: 'Professional', value: '11', icon: 'fa-solid fa-user-gear', textColorClass: 'text-gray-400' },
|
||||
{ name: 'Manufacturing', value: '12', icon: 'fa-solid fa-industry', textColorClass: 'text-red-400' },
|
||||
{ name: 'Food and Restaurant', value: '13', icon: 'fa-solid fa-utensils', textColorClass: 'text-amber-700' },
|
||||
];
|
||||
const { Pool } = pkg;
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen
|
||||
});
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
// const pool = new Pool({connectionString})
|
||||
const client = new Pool({ connectionString });
|
||||
const db = drizzle(client, { schema, logger: true });
|
||||
const logger = winston.createLogger({
|
||||
transports: [new winston.transports.Console()],
|
||||
});
|
||||
const commService = new CommercialPropertyService(null, db);
|
||||
const businessService = new BusinessListingService(null, db);
|
||||
//Delete Content
|
||||
await db.delete(schema.commercials);
|
||||
await db.delete(schema.businesses);
|
||||
await db.delete(schema.users);
|
||||
let filePath = `./src/assets/geo.json`;
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
const geos = JSON.parse(rawData) as Geo;
|
||||
|
||||
const sso = new SelectOptionsService();
|
||||
//Broker
|
||||
filePath = `./data/broker.json`;
|
||||
let data: string = readFileSync(filePath, 'utf8');
|
||||
const usersData: UserData[] = JSON.parse(data); // Erwartet ein Array von Objekten
|
||||
const generatedUserData = [];
|
||||
console.log(usersData.length);
|
||||
let i = 0,
|
||||
male = 0,
|
||||
female = 0;
|
||||
const targetPathProfile = `./pictures/profile`;
|
||||
deleteFilesOfDir(targetPathProfile);
|
||||
const targetPathLogo = `./pictures/logo`;
|
||||
deleteFilesOfDir(targetPathLogo);
|
||||
const targetPathProperty = `./pictures/property`;
|
||||
deleteFilesOfDir(targetPathProperty);
|
||||
fs.ensureDirSync(`./pictures/logo`);
|
||||
fs.ensureDirSync(`./pictures/profile`);
|
||||
fs.ensureDirSync(`./pictures/property`);
|
||||
|
||||
//User
|
||||
for (let index = 0; index < usersData.length; index++) {
|
||||
const userData = usersData[index];
|
||||
const user: User = createDefaultUser('', '', '');
|
||||
user.licensedIn = [];
|
||||
userData.licensedIn.forEach(l => {
|
||||
console.log(l['value'], l['name']);
|
||||
user.licensedIn.push({ registerNo: l['value'], state: l['name'] });
|
||||
});
|
||||
user.areasServed = [];
|
||||
user.areasServed = userData.areasServed.map(l => {
|
||||
return { county: l.split(',')[0].trim(), state: l.split(',')[1].trim() };
|
||||
});
|
||||
user.hasCompanyLogo = true;
|
||||
user.hasProfile = true;
|
||||
user.firstname = userData.firstname;
|
||||
user.lastname = userData.lastname;
|
||||
user.email = userData.email;
|
||||
user.phoneNumber = userData.phoneNumber;
|
||||
user.description = userData.description;
|
||||
user.companyName = userData.companyName;
|
||||
user.companyOverview = userData.companyOverview;
|
||||
user.companyWebsite = userData.companyWebsite;
|
||||
const [city, state] = userData.companyLocation.split('-').map(e => e.trim());
|
||||
user.companyLocation = {};
|
||||
user.companyLocation.city = city;
|
||||
user.companyLocation.state = state;
|
||||
const cityGeo = geos.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||
user.companyLocation.latitude = cityGeo.latitude;
|
||||
user.companyLocation.longitude = cityGeo.longitude;
|
||||
user.offeredServices = userData.offeredServices;
|
||||
user.gender = userData.gender;
|
||||
user.customerType = 'professional';
|
||||
user.customerSubType = 'broker';
|
||||
user.created = new Date();
|
||||
user.updated = new Date();
|
||||
|
||||
const u = await db
|
||||
.insert(schema.users)
|
||||
.values(convertUserToDrizzleUser(user))
|
||||
.returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname });
|
||||
generatedUserData.push(u[0]);
|
||||
i++;
|
||||
logger.info(`user_${index} inserted`);
|
||||
if (u[0].gender === 'male') {
|
||||
male++;
|
||||
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
|
||||
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||
} else {
|
||||
female++;
|
||||
const data = readFileSync(`./pictures_base/profile/Frau_${female}.jpg`);
|
||||
await storeProfilePicture(data, emailToDirName(u[0].email));
|
||||
}
|
||||
const data = readFileSync(`./pictures_base/logo/${i}.jpg`);
|
||||
await storeCompanyLogo(data, emailToDirName(u[0].email));
|
||||
}
|
||||
|
||||
//Corporate Listings
|
||||
filePath = `./data/commercials.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const commercialJsonData = JSON.parse(data) as PropertyImportListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < commercialJsonData.length; index++) {
|
||||
const user = getRandomItem(generatedUserData);
|
||||
const commercial = createDefaultCommercialPropertyListing();
|
||||
const id = commercialJsonData[index].id;
|
||||
delete commercial.id;
|
||||
|
||||
commercial.email = user.email;
|
||||
commercial.type = sso.typesOfCommercialProperty.find(e => e.oldValue === String(commercialJsonData[index].type)).value;
|
||||
commercial.title = commercialJsonData[index].title;
|
||||
commercial.description = commercialJsonData[index].description;
|
||||
try {
|
||||
const cityGeo = geos.states.find(s => s.state_code === commercialJsonData[index].state).cities.find(c => c.name === commercialJsonData[index].city);
|
||||
commercial.location = {};
|
||||
commercial.location.latitude = cityGeo.latitude;
|
||||
commercial.location.longitude = cityGeo.longitude;
|
||||
commercial.location.city = commercialJsonData[index].city;
|
||||
commercial.location.state = commercialJsonData[index].state;
|
||||
// console.log(JSON.stringify(commercial.location));
|
||||
} catch (e) {
|
||||
console.log(`----------------> ERROR ${commercialJsonData[index].state} - ${commercialJsonData[index].city}`);
|
||||
continue;
|
||||
}
|
||||
commercial.price = commercialJsonData[index].price;
|
||||
commercial.listingsCategory = 'commercialProperty';
|
||||
commercial.draft = false;
|
||||
commercial.imageOrder = getFilenames(id);
|
||||
commercial.imagePath = emailToDirName(user.email);
|
||||
const insertionDate = getRandomDateWithinLastYear();
|
||||
commercial.created = insertionDate;
|
||||
commercial.updated = insertionDate;
|
||||
|
||||
const result = await commService.createListing(commercial); //await db.insert(schema.commercials).values(commercial).returning();
|
||||
try {
|
||||
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result.imagePath}/${result.serialId}`);
|
||||
} catch (err) {
|
||||
console.log(`----- No pictures available for ${id} ------ ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
//Business Listings
|
||||
filePath = `./data/businesses.json`;
|
||||
data = readFileSync(filePath, 'utf8');
|
||||
const businessJsonData = JSON.parse(data) as BusinessImportListing[]; // Erwartet ein Array von Objekten
|
||||
for (let index = 0; index < businessJsonData.length; index++) {
|
||||
const business = createDefaultBusinessListing(); //businessJsonData[index];
|
||||
delete business.id;
|
||||
const user = getRandomItem(generatedUserData);
|
||||
business.email = user.email;
|
||||
business.type = sso.typesOfBusiness.find(e => e.oldValue === String(businessJsonData[index].type)).value;
|
||||
business.title = businessJsonData[index].title;
|
||||
business.description = businessJsonData[index].description;
|
||||
try {
|
||||
const cityGeo = geos.states.find(s => s.state_code === businessJsonData[index].state).cities.find(c => c.name === businessJsonData[index].city);
|
||||
business.location = {};
|
||||
business.location.latitude = cityGeo.latitude;
|
||||
business.location.longitude = cityGeo.longitude;
|
||||
business.location.city = businessJsonData[index].city;
|
||||
business.location.state = businessJsonData[index].state;
|
||||
} catch (e) {
|
||||
console.log(`----------------> ERROR ${businessJsonData[index].state} - ${businessJsonData[index].city}`);
|
||||
continue;
|
||||
}
|
||||
business.price = businessJsonData[index].price;
|
||||
business.title = businessJsonData[index].title;
|
||||
business.draft = businessJsonData[index].draft;
|
||||
business.listingsCategory = 'business';
|
||||
business.realEstateIncluded = businessJsonData[index].realEstateIncluded;
|
||||
business.leasedLocation = businessJsonData[index].leasedLocation;
|
||||
business.franchiseResale = businessJsonData[index].franchiseResale;
|
||||
|
||||
business.salesRevenue = businessJsonData[index].salesRevenue;
|
||||
business.cashFlow = businessJsonData[index].cashFlow;
|
||||
business.supportAndTraining = businessJsonData[index].supportAndTraining;
|
||||
business.employees = businessJsonData[index].employees;
|
||||
business.established = businessJsonData[index].established;
|
||||
business.internalListingNumber = businessJsonData[index].internalListingNumber;
|
||||
business.reasonForSale = businessJsonData[index].reasonForSale;
|
||||
business.brokerLicencing = businessJsonData[index].brokerLicencing;
|
||||
business.internals = businessJsonData[index].internals;
|
||||
business.imageName = emailToDirName(user.email);
|
||||
business.created = new Date(businessJsonData[index].created);
|
||||
business.updated = new Date(businessJsonData[index].created);
|
||||
|
||||
await businessService.createListing(business); //db.insert(schema.businesses).values(business);
|
||||
}
|
||||
|
||||
//End
|
||||
await client.end();
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
async function createEmbedding(text: string): Promise<number[]> {
|
||||
const response = await openai.embeddings.create({
|
||||
model: 'text-embedding-3-small',
|
||||
input: text,
|
||||
});
|
||||
return response.data[0].embedding;
|
||||
}
|
||||
|
||||
function getRandomItem<T>(arr: T[]): T {
|
||||
if (arr.length === 0) {
|
||||
throw new Error('The array is empty.');
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * arr.length);
|
||||
return arr[randomIndex];
|
||||
}
|
||||
function getFilenames(id: string): string[] {
|
||||
try {
|
||||
let filePath = `./pictures_base/property/${id}`;
|
||||
return readdirSync(filePath);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
function getRandomDateWithinLastYear(): Date {
|
||||
const currentDate = new Date();
|
||||
const lastYear = new Date(currentDate.getFullYear() - 1, currentDate.getMonth(), currentDate.getDate());
|
||||
|
||||
const timeDiff = currentDate.getTime() - lastYear.getTime();
|
||||
const randomTimeDiff = Math.random() * timeDiff;
|
||||
const randomDate = new Date(lastYear.getTime() + randomTimeDiff);
|
||||
|
||||
return randomDate;
|
||||
}
|
||||
async function storeProfilePicture(buffer: Buffer, userId: string) {
|
||||
let quality = 50;
|
||||
const output = await sharp(buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`./pictures/profile/${userId}.avif`);
|
||||
}
|
||||
|
||||
async function storeCompanyLogo(buffer: Buffer, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const output = await sharp(buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||
}
|
||||
|
||||
function deleteFilesOfDir(directoryPath) {
|
||||
// Überprüfen, ob das Verzeichnis existiert
|
||||
if (existsSync(directoryPath)) {
|
||||
// Den Inhalt des Verzeichnisses synchron löschen
|
||||
try {
|
||||
readdirSync(directoryPath).forEach(file => {
|
||||
const filePath = join(directoryPath, file);
|
||||
// Wenn es sich um ein Verzeichnis handelt, rekursiv löschen
|
||||
if (statSync(filePath).isDirectory()) {
|
||||
rimraf.sync(filePath);
|
||||
} else {
|
||||
// Wenn es sich um eine Datei handelt, direkt löschen
|
||||
unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
console.log('Der Inhalt des Verzeichnisses wurde erfolgreich gelöscht.');
|
||||
} catch (err) {
|
||||
console.error('Fehler beim Löschen des Verzeichnisses:', err);
|
||||
}
|
||||
} else {
|
||||
console.log('Das Verzeichnis existiert nicht.');
|
||||
}
|
||||
}
|
||||
12
bizmatch-server/src/drizzle/migrate.ts
Normal file
12
bizmatch-server/src/drizzle/migrate.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'dotenv/config';
|
||||
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||
import pkg from 'pg';
|
||||
import * as schema from './schema.js';
|
||||
const { Pool } = pkg;
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
const pool = new Pool({ connectionString });
|
||||
const db = drizzle(pool, { schema });
|
||||
// This will run migrations on the database, skipping the ones already applied
|
||||
//await migrate(db, { migrationsFolder: './src/drizzle/migrations' });
|
||||
// Don't forget to close the connection, otherwise the script will hang
|
||||
//await pool.end();
|
||||
114
bizmatch-server/src/drizzle/migrations/0000_lean_marvex.sql
Normal file
114
bizmatch-server/src/drizzle/migrations/0000_lean_marvex.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."customerSubType" AS ENUM('broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."customerType" AS ENUM('buyer', 'professional');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."gender" AS ENUM('male', 'female');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."listingsCategory" AS ENUM('commercialProperty', 'business');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "businesses" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"email" varchar(255),
|
||||
"type" varchar(255),
|
||||
"title" varchar(255),
|
||||
"description" text,
|
||||
"city" varchar(255),
|
||||
"state" char(2),
|
||||
"price" double precision,
|
||||
"favoritesForUser" varchar(30)[],
|
||||
"draft" boolean,
|
||||
"listingsCategory" "listingsCategory",
|
||||
"realEstateIncluded" boolean,
|
||||
"leasedLocation" boolean,
|
||||
"franchiseResale" boolean,
|
||||
"salesRevenue" double precision,
|
||||
"cashFlow" double precision,
|
||||
"supportAndTraining" text,
|
||||
"employees" integer,
|
||||
"established" integer,
|
||||
"internalListingNumber" integer,
|
||||
"reasonForSale" varchar(255),
|
||||
"brokerLicencing" varchar(255),
|
||||
"internals" text,
|
||||
"imageName" varchar(200),
|
||||
"created" timestamp,
|
||||
"updated" timestamp,
|
||||
"latitude" double precision,
|
||||
"longitude" double precision
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "commercials" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"serialId" serial NOT NULL,
|
||||
"email" varchar(255),
|
||||
"type" varchar(255),
|
||||
"title" varchar(255),
|
||||
"description" text,
|
||||
"city" varchar(255),
|
||||
"state" char(2),
|
||||
"price" double precision,
|
||||
"favoritesForUser" varchar(30)[],
|
||||
"listingsCategory" "listingsCategory",
|
||||
"draft" boolean,
|
||||
"imageOrder" varchar(200)[],
|
||||
"imagePath" varchar(200),
|
||||
"created" timestamp,
|
||||
"updated" timestamp,
|
||||
"latitude" double precision,
|
||||
"longitude" double precision
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"firstname" varchar(255) NOT NULL,
|
||||
"lastname" varchar(255) NOT NULL,
|
||||
"email" varchar(255) NOT NULL,
|
||||
"phoneNumber" varchar(255),
|
||||
"description" text,
|
||||
"companyName" varchar(255),
|
||||
"companyOverview" text,
|
||||
"companyWebsite" varchar(255),
|
||||
"city" varchar(255),
|
||||
"state" char(2),
|
||||
"offeredServices" text,
|
||||
"areasServed" jsonb,
|
||||
"hasProfile" boolean,
|
||||
"hasCompanyLogo" boolean,
|
||||
"licensedIn" jsonb,
|
||||
"gender" "gender",
|
||||
"customerType" "customerType",
|
||||
"customerSubType" "customerSubType",
|
||||
"created" timestamp,
|
||||
"updated" timestamp,
|
||||
"latitude" double precision,
|
||||
"longitude" double precision,
|
||||
CONSTRAINT "users_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "businesses" ADD CONSTRAINT "businesses_email_users_email_fk" FOREIGN KEY ("email") REFERENCES "public"."users"("email") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "commercials" ADD CONSTRAINT "commercials_email_users_email_fk" FOREIGN KEY ("email") REFERENCES "public"."users"("email") ON DELETE no action ON UPDATE no action;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
541
bizmatch-server/src/drizzle/migrations/meta/0000_snapshot.json
Normal file
541
bizmatch-server/src/drizzle/migrations/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,541 @@
|
||||
{
|
||||
"id": "a8283ca6-2c10-42bb-a640-ca984544ba30",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.businesses": {
|
||||
"name": "businesses",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"city": {
|
||||
"name": "city",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "char(2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"price": {
|
||||
"name": "price",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"favoritesForUser": {
|
||||
"name": "favoritesForUser",
|
||||
"type": "varchar(30)[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"draft": {
|
||||
"name": "draft",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"listingsCategory": {
|
||||
"name": "listingsCategory",
|
||||
"type": "listingsCategory",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"realEstateIncluded": {
|
||||
"name": "realEstateIncluded",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"leasedLocation": {
|
||||
"name": "leasedLocation",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"franchiseResale": {
|
||||
"name": "franchiseResale",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"salesRevenue": {
|
||||
"name": "salesRevenue",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"cashFlow": {
|
||||
"name": "cashFlow",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"supportAndTraining": {
|
||||
"name": "supportAndTraining",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"employees": {
|
||||
"name": "employees",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"established": {
|
||||
"name": "established",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"internalListingNumber": {
|
||||
"name": "internalListingNumber",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"reasonForSale": {
|
||||
"name": "reasonForSale",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"brokerLicencing": {
|
||||
"name": "brokerLicencing",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"internals": {
|
||||
"name": "internals",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"imageName": {
|
||||
"name": "imageName",
|
||||
"type": "varchar(200)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created": {
|
||||
"name": "created",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated": {
|
||||
"name": "updated",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"latitude": {
|
||||
"name": "latitude",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"longitude": {
|
||||
"name": "longitude",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"businesses_email_users_email_fk": {
|
||||
"name": "businesses_email_users_email_fk",
|
||||
"tableFrom": "businesses",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"email"
|
||||
],
|
||||
"columnsTo": [
|
||||
"email"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.commercials": {
|
||||
"name": "commercials",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"serialId": {
|
||||
"name": "serialId",
|
||||
"type": "serial",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"city": {
|
||||
"name": "city",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "char(2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"price": {
|
||||
"name": "price",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"favoritesForUser": {
|
||||
"name": "favoritesForUser",
|
||||
"type": "varchar(30)[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"listingsCategory": {
|
||||
"name": "listingsCategory",
|
||||
"type": "listingsCategory",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"draft": {
|
||||
"name": "draft",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"imageOrder": {
|
||||
"name": "imageOrder",
|
||||
"type": "varchar(200)[]",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"imagePath": {
|
||||
"name": "imagePath",
|
||||
"type": "varchar(200)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created": {
|
||||
"name": "created",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated": {
|
||||
"name": "updated",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"latitude": {
|
||||
"name": "latitude",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"longitude": {
|
||||
"name": "longitude",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"commercials_email_users_email_fk": {
|
||||
"name": "commercials_email_users_email_fk",
|
||||
"tableFrom": "commercials",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"email"
|
||||
],
|
||||
"columnsTo": [
|
||||
"email"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {}
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"firstname": {
|
||||
"name": "firstname",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"lastname": {
|
||||
"name": "lastname",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"phoneNumber": {
|
||||
"name": "phoneNumber",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"description": {
|
||||
"name": "description",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"companyName": {
|
||||
"name": "companyName",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"companyOverview": {
|
||||
"name": "companyOverview",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"companyWebsite": {
|
||||
"name": "companyWebsite",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"city": {
|
||||
"name": "city",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "char(2)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"offeredServices": {
|
||||
"name": "offeredServices",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"areasServed": {
|
||||
"name": "areasServed",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"hasProfile": {
|
||||
"name": "hasProfile",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"hasCompanyLogo": {
|
||||
"name": "hasCompanyLogo",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"licensedIn": {
|
||||
"name": "licensedIn",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"gender": {
|
||||
"name": "gender",
|
||||
"type": "gender",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customerType": {
|
||||
"name": "customerType",
|
||||
"type": "customerType",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"customerSubType": {
|
||||
"name": "customerSubType",
|
||||
"type": "customerSubType",
|
||||
"typeSchema": "public",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created": {
|
||||
"name": "created",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated": {
|
||||
"name": "updated",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"latitude": {
|
||||
"name": "latitude",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"longitude": {
|
||||
"name": "longitude",
|
||||
"type": "double precision",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"public.customerSubType": {
|
||||
"name": "customerSubType",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"broker",
|
||||
"cpa",
|
||||
"attorney",
|
||||
"titleCompany",
|
||||
"surveyor",
|
||||
"appraiser"
|
||||
]
|
||||
},
|
||||
"public.customerType": {
|
||||
"name": "customerType",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"buyer",
|
||||
"professional"
|
||||
]
|
||||
},
|
||||
"public.gender": {
|
||||
"name": "gender",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"male",
|
||||
"female"
|
||||
]
|
||||
},
|
||||
"public.listingsCategory": {
|
||||
"name": "listingsCategory",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"commercialProperty",
|
||||
"business"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
13
bizmatch-server/src/drizzle/migrations/meta/_journal.json
Normal file
13
bizmatch-server/src/drizzle/migrations/meta/_journal.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1723045357281,
|
||||
"tag": "0000_lean_marvex",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
92
bizmatch-server/src/drizzle/schema.ts
Normal file
92
bizmatch-server/src/drizzle/schema.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core';
|
||||
import { AreasServed, LicensedIn } from '../models/db.model';
|
||||
export const PG_CONNECTION = 'PG_CONNECTION';
|
||||
export const genderEnum = pgEnum('gender', ['male', 'female']);
|
||||
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'professional']);
|
||||
export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
|
||||
|
||||
export const users = pgTable('users', {
|
||||
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||
firstname: varchar('firstname', { length: 255 }).notNull(),
|
||||
lastname: varchar('lastname', { length: 255 }).notNull(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
phoneNumber: varchar('phoneNumber', { length: 255 }),
|
||||
description: text('description'),
|
||||
companyName: varchar('companyName', { length: 255 }),
|
||||
companyOverview: text('companyOverview'),
|
||||
companyWebsite: varchar('companyWebsite', { length: 255 }),
|
||||
city: varchar('city', { length: 255 }),
|
||||
state: char('state', { length: 2 }),
|
||||
offeredServices: text('offeredServices'),
|
||||
areasServed: jsonb('areasServed').$type<AreasServed[]>(),
|
||||
hasProfile: boolean('hasProfile'),
|
||||
hasCompanyLogo: boolean('hasCompanyLogo'),
|
||||
licensedIn: jsonb('licensedIn').$type<LicensedIn[]>(),
|
||||
gender: genderEnum('gender'),
|
||||
customerType: customerTypeEnum('customerType'),
|
||||
customerSubType: customerSubTypeEnum('customerSubType'),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
latitude: doublePrecision('latitude'),
|
||||
longitude: doublePrecision('longitude'),
|
||||
// embedding: vector('embedding', { dimensions: 1536 }),
|
||||
});
|
||||
|
||||
export const businesses = pgTable('businesses', {
|
||||
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||
email: varchar('email', { length: 255 }).references(() => users.email),
|
||||
type: varchar('type', { length: 255 }),
|
||||
title: varchar('title', { length: 255 }),
|
||||
description: text('description'),
|
||||
city: varchar('city', { length: 255 }),
|
||||
state: char('state', { length: 2 }),
|
||||
// zipCode: integer('zipCode'),
|
||||
// county: varchar('county', { length: 255 }),
|
||||
price: doublePrecision('price'),
|
||||
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
||||
draft: boolean('draft'),
|
||||
listingsCategory: listingsCategoryEnum('listingsCategory'), //varchar('listingsCategory', { length: 255 }),
|
||||
realEstateIncluded: boolean('realEstateIncluded'),
|
||||
leasedLocation: boolean('leasedLocation'),
|
||||
franchiseResale: boolean('franchiseResale'),
|
||||
salesRevenue: doublePrecision('salesRevenue'),
|
||||
cashFlow: doublePrecision('cashFlow'),
|
||||
supportAndTraining: text('supportAndTraining'),
|
||||
employees: integer('employees'),
|
||||
established: integer('established'),
|
||||
internalListingNumber: integer('internalListingNumber'),
|
||||
reasonForSale: varchar('reasonForSale', { length: 255 }),
|
||||
brokerLicencing: varchar('brokerLicencing', { length: 255 }),
|
||||
internals: text('internals'),
|
||||
imageName: varchar('imageName', { length: 200 }),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
latitude: doublePrecision('latitude'),
|
||||
longitude: doublePrecision('longitude'),
|
||||
// embedding: vector('embedding', { dimensions: 1536 }),
|
||||
});
|
||||
|
||||
export const commercials = pgTable('commercials', {
|
||||
id: uuid('id').primaryKey().defaultRandom().notNull(),
|
||||
serialId: serial('serialId'),
|
||||
email: varchar('email', { length: 255 }).references(() => users.email),
|
||||
type: varchar('type', { length: 255 }),
|
||||
title: varchar('title', { length: 255 }),
|
||||
description: text('description'),
|
||||
city: varchar('city', { length: 255 }),
|
||||
state: char('state', { length: 2 }),
|
||||
price: doublePrecision('price'),
|
||||
favoritesForUser: varchar('favoritesForUser', { length: 30 }).array(),
|
||||
listingsCategory: listingsCategoryEnum('listingsCategory'), //listingsCategory: varchar('listingsCategory', { length: 255 }),
|
||||
draft: boolean('draft'),
|
||||
// zipCode: integer('zipCode'),
|
||||
// county: varchar('county', { length: 255 }),
|
||||
imageOrder: varchar('imageOrder', { length: 200 }).array(),
|
||||
imagePath: varchar('imagePath', { length: 200 }),
|
||||
created: timestamp('created'),
|
||||
updated: timestamp('updated'),
|
||||
latitude: doublePrecision('latitude'),
|
||||
longitude: doublePrecision('longitude'),
|
||||
// embedding: vector('embedding', { dimensions: 1536 }),
|
||||
});
|
||||
21
bizmatch-server/src/drizzle/test.js
Normal file
21
bizmatch-server/src/drizzle/test.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Angenommen, du hast eine Datei `databaseModels.js` mit deinen pgTable-Definitionen
|
||||
const { users } = require('./schema.js');
|
||||
|
||||
function generateTypeScriptInterface(tableDefinition, tableName) {
|
||||
let interfaceString = `export interface ${tableName} {\n`;
|
||||
for (const [column, definition] of Object.entries(tableDefinition)) {
|
||||
// Du musst die Definition parsen, um den korrekten Typ zu extrahieren
|
||||
const tsType = definition.type === 'uuid' ? 'string' :
|
||||
definition.type.startsWith('varchar') || definition.type === 'text' ? 'string' :
|
||||
definition.type === 'boolean' ? 'boolean' : 'any';
|
||||
interfaceString += ` ${column}${definition.optional ? '?' : ''}: ${tsType};\n`;
|
||||
}
|
||||
interfaceString += '}\n';
|
||||
return interfaceString;
|
||||
}
|
||||
|
||||
const userModelInterface = generateTypeScriptInterface(users.columns, 'User');
|
||||
fs.writeFileSync(path.join(__dirname, 'UserInterface.ts'), userModelInterface);
|
||||
@@ -1,29 +1,149 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { fstat, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import path from 'path';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import path, { join } from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { Logger } from 'winston';
|
||||
import { ImageProperty, Subscription } from '../models/main.model.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class FileService {
|
||||
private subscriptions: any;
|
||||
constructor() {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
this.loadSubscriptions();
|
||||
fs.ensureDirSync(`./pictures`);
|
||||
fs.ensureDirSync(`./pictures/profile`);
|
||||
fs.ensureDirSync(`./pictures/logo`);
|
||||
fs.ensureDirSync(`./pictures/property`);
|
||||
}
|
||||
// ############
|
||||
// Subscriptions
|
||||
// ############
|
||||
private loadSubscriptions(): void {
|
||||
const filePath = join(__dirname,'..', 'assets', 'subscriptions.json');
|
||||
const filePath = join(__dirname, '../..', 'assets', 'subscriptions.json');
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
this.subscriptions = JSON.parse(rawData);
|
||||
}
|
||||
getSubscriptions() {
|
||||
return this.subscriptions
|
||||
getSubscriptions(): Subscription[] {
|
||||
return this.subscriptions;
|
||||
}
|
||||
async storeFile(file: Express.Multer.File,id: string){
|
||||
const suffix = file.mimetype.includes('png')?'png':'jpg'
|
||||
await fs.outputFile(`./public/profile_${id}`,file.buffer);
|
||||
// ############
|
||||
// Profile
|
||||
// ############
|
||||
async storeProfilePicture(file: Express.Multer.File, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const output = await sharp(file.buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`./pictures/profile/${adjustedEmail}.avif`);
|
||||
}
|
||||
hasProfile(adjustedEmail: string) {
|
||||
return fs.existsSync(`./pictures/profile/${adjustedEmail}.avif`);
|
||||
}
|
||||
// ############
|
||||
// Logo
|
||||
// ############
|
||||
async storeCompanyLogo(file: Express.Multer.File, adjustedEmail: string) {
|
||||
let quality = 50;
|
||||
const output = await sharp(file.buffer)
|
||||
.resize({ width: 300 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`./pictures/logo/${adjustedEmail}.avif`); // Ersetze Dateierweiterung
|
||||
// await fs.outputFile(`./pictures/logo/${userId}`, file.buffer);
|
||||
}
|
||||
hasCompanyLogo(adjustedEmail: string) {
|
||||
return fs.existsSync(`./pictures/logo/${adjustedEmail}.avif`) ? true : false;
|
||||
}
|
||||
// ############
|
||||
// Property
|
||||
// ############
|
||||
async getPropertyImages(imagePath: string, serial: string): Promise<string[]> {
|
||||
const result: string[] = [];
|
||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||
if (fs.existsSync(directory)) {
|
||||
const files = await fs.readdir(directory);
|
||||
files.forEach(f => {
|
||||
result.push(f);
|
||||
});
|
||||
return result;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
async hasPropertyImages(imagePath: string, serial: string): Promise<boolean> {
|
||||
const result: ImageProperty[] = [];
|
||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||
if (fs.existsSync(directory)) {
|
||||
const files = await fs.readdir(directory);
|
||||
return files.length > 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async storePropertyPicture(file: Express.Multer.File, imagePath: string, serial: string): Promise<string> {
|
||||
const suffix = file.mimetype.includes('png') ? 'png' : 'jpg';
|
||||
const directory = `./pictures/property/${imagePath}/${serial}`;
|
||||
fs.ensureDirSync(`${directory}`);
|
||||
const imageName = await this.getNextImageName(directory);
|
||||
//await fs.outputFile(`${directory}/${imageName}`, file.buffer);
|
||||
await this.resizeImageToAVIF(file.buffer, 150 * 1024, imageName, directory);
|
||||
return `${imageName}.avif`;
|
||||
}
|
||||
// ############
|
||||
// utils
|
||||
// ############
|
||||
async getNextImageName(directory) {
|
||||
try {
|
||||
const files = await fs.readdir(directory);
|
||||
const imageNumbers = files
|
||||
.map(file => parseInt(file, 10)) // Dateinamen direkt in Zahlen umwandeln
|
||||
.filter(number => !isNaN(number)) // Sicherstellen, dass die Konvertierung gültig ist
|
||||
.sort((a, b) => a - b); // Aufsteigend sortieren
|
||||
|
||||
const nextNumber = imageNumbers.length > 0 ? Math.max(...imageNumbers) + 1 : 1;
|
||||
return `${nextNumber}`; // Keine Endung für den Dateinamen
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Lesen des Verzeichnisses:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async resizeImageToAVIF(buffer: Buffer, maxSize: number, imageName: string, directory: string) {
|
||||
let quality = 50; // AVIF kann mit niedrigeren Qualitätsstufen gute Ergebnisse erzielen
|
||||
let output;
|
||||
let start = Date.now();
|
||||
output = await sharp(buffer)
|
||||
.resize({ width: 1500 })
|
||||
.avif({ quality }) // Verwende AVIF
|
||||
//.webp({ quality }) // Verwende Webp
|
||||
.toBuffer();
|
||||
await sharp(output).toFile(`${directory}/${imageName}.avif`); // Ersetze Dateierweiterung
|
||||
let timeTaken = Date.now() - start;
|
||||
this.logger.info(`Quality: ${quality} - Time: ${timeTaken} milliseconds`);
|
||||
}
|
||||
deleteImage(path: string) {
|
||||
fs.unlinkSync(path);
|
||||
}
|
||||
|
||||
deleteDirectoryIfExists(imagePath) {
|
||||
const dirPath = `pictures/property/${imagePath}`;
|
||||
try {
|
||||
const exists = fs.pathExistsSync();
|
||||
if (exists) {
|
||||
fs.removeSync(dirPath);
|
||||
console.log(`Directory ${dirPath} was deleted.`);
|
||||
} else {
|
||||
console.log(`Directory ${dirPath} does not exist.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error while deleting ${dirPath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
27
bizmatch-server/src/geo/geo.controller.ts
Normal file
27
bizmatch-server/src/geo/geo.controller.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { CountyRequest } from 'src/models/server.model.js';
|
||||
import { GeoService } from './geo.service.js';
|
||||
|
||||
@Controller('geo')
|
||||
export class GeoController {
|
||||
constructor(private geoService: GeoService) {}
|
||||
|
||||
@Get(':prefix')
|
||||
findByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix);
|
||||
}
|
||||
@Get('citiesandstates/:prefix')
|
||||
findByCitiesAndStatesByPrefix(@Param('prefix') prefix: string): any {
|
||||
return this.geoService.findCitiesAndStatesStartingWith(prefix);
|
||||
}
|
||||
|
||||
@Get(':prefix/:state')
|
||||
findByPrefixAndState(@Param('prefix') prefix: string, @Param('state') state: string): any {
|
||||
return this.geoService.findCitiesStartingWith(prefix, state);
|
||||
}
|
||||
|
||||
@Post('counties')
|
||||
findByPrefixAndStates(@Body() countyRequest: CountyRequest): any {
|
||||
return this.geoService.findCountiesStartingWith(countyRequest.prefix, countyRequest.states);
|
||||
}
|
||||
}
|
||||
9
bizmatch-server/src/geo/geo.module.ts
Normal file
9
bizmatch-server/src/geo/geo.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GeoController } from './geo.controller.js';
|
||||
import { GeoService } from './geo.service.js';
|
||||
|
||||
@Module({
|
||||
controllers: [GeoController],
|
||||
providers: [GeoService]
|
||||
})
|
||||
export class GeoModule {}
|
||||
106
bizmatch-server/src/geo/geo.service.ts
Normal file
106
bizmatch-server/src/geo/geo.service.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readFileSync } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
import { CountyResult, GeoResult } from 'src/models/main.model.js';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { City, CountyData, Geo, State } from '../models/server.model.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class GeoService {
|
||||
geo: Geo;
|
||||
counties: CountyData[];
|
||||
constructor() {
|
||||
this.loadGeo();
|
||||
}
|
||||
private loadGeo(): void {
|
||||
const filePath = join(__dirname, '../..', 'assets', 'geo.json');
|
||||
const rawData = readFileSync(filePath, 'utf8');
|
||||
this.geo = JSON.parse(rawData);
|
||||
const countiesFilePath = join(__dirname, '../..', 'assets', 'counties.json');
|
||||
const rawCountiesData = readFileSync(countiesFilePath, 'utf8');
|
||||
this.counties = JSON.parse(rawCountiesData);
|
||||
}
|
||||
findCountiesStartingWith(prefix: string, states?: string[]) {
|
||||
let results: CountyResult[] = [];
|
||||
let idCounter = 1;
|
||||
|
||||
this.counties.forEach(stateData => {
|
||||
if (!states || states.includes(stateData.state)) {
|
||||
stateData.counties.forEach(county => {
|
||||
if (county.startsWith(prefix.toUpperCase())) {
|
||||
results.push({
|
||||
id: idCounter++,
|
||||
name: county,
|
||||
state: stateData.state_full,
|
||||
state_code: stateData.state,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
findCitiesStartingWith(prefix: string, state?: string): GeoResult[] {
|
||||
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.state_code,
|
||||
//state_code: state.state_code,
|
||||
latitude: city.latitude,
|
||||
longitude: city.longitude,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return state ? result.filter(e => e.state.toLowerCase() === state.toLowerCase()) : result;
|
||||
}
|
||||
findCitiesAndStatesStartingWith(prefix: string, state?: string): Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> {
|
||||
const results: Array<{ id: string; name: string; type: 'city' | 'state'; state: string }> = [];
|
||||
|
||||
const lowercasePrefix = prefix.toLowerCase();
|
||||
|
||||
//for (const country of this.geo) {
|
||||
// Suche nach passenden Staaten
|
||||
for (const state of this.geo.states) {
|
||||
if (state.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: state.id.toString(),
|
||||
name: state.name,
|
||||
type: 'state',
|
||||
state: state.state_code,
|
||||
});
|
||||
}
|
||||
|
||||
// Suche nach passenden Städten
|
||||
for (const city of state.cities) {
|
||||
if (city.name.toLowerCase().startsWith(lowercasePrefix)) {
|
||||
results.push({
|
||||
id: city.id.toString(),
|
||||
name: city.name,
|
||||
type: 'city',
|
||||
state: state.state_code,
|
||||
});
|
||||
}
|
||||
}
|
||||
//}
|
||||
}
|
||||
|
||||
return results.sort((a, b) => {
|
||||
if (a.type === 'state' && b.type === 'city') return -1;
|
||||
if (a.type === 'city' && b.type === 'state') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
getCityWithCoords(state: string, city: string): City {
|
||||
return this.geo.states.find(s => s.state_code === state).cities.find(c => c.name === city);
|
||||
}
|
||||
}
|
||||
55
bizmatch-server/src/image/image.controller.ts
Normal file
55
bizmatch-server/src/image/image.controller.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Controller, Delete, Inject, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
|
||||
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 { 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: CommercialPropertyService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
private selectOptions: SelectOptionsService,
|
||||
) {}
|
||||
// ############
|
||||
// Property
|
||||
// ############
|
||||
@Post('uploadPropertyPicture/:imagePath/:serial')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadPropertyPicture(@UploadedFile() file: Express.Multer.File, @Param('imagePath') imagePath: string, @Param('serial') serial: string) {
|
||||
const imagename = await this.fileService.storePropertyPicture(file, imagePath, serial);
|
||||
await this.listingService.addImage(imagePath, serial, imagename);
|
||||
}
|
||||
@Delete('propertyPicture/:imagePath/:serial/:imagename')
|
||||
async deletePropertyImagesById(@Param('imagePath') imagePath: string, @Param('serial') serial: string, @Param('imagename') imagename: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/property/${imagePath}/${serial}/${imagename}`);
|
||||
await this.listingService.deleteImage(imagePath, serial, imagename);
|
||||
}
|
||||
// ############
|
||||
// Profile
|
||||
// ############
|
||||
@Post('uploadProfile/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadProfile(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeProfilePicture(file, adjustedEmail);
|
||||
}
|
||||
@Delete('profile/:email/')
|
||||
async deleteProfileImagesById(@Param('email') email: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/profile/${email}.avif`);
|
||||
}
|
||||
// ############
|
||||
// Logo
|
||||
// ############
|
||||
@Post('uploadCompanyLogo/:email')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async uploadCompanyLogo(@UploadedFile() file: Express.Multer.File, @Param('email') adjustedEmail: string) {
|
||||
await this.fileService.storeCompanyLogo(file, adjustedEmail);
|
||||
}
|
||||
@Delete('logo/:email/')
|
||||
async deleteLogoImagesById(@Param('email') adjustedEmail: string): Promise<any> {
|
||||
this.fileService.deleteImage(`pictures/logo/${adjustedEmail}.avif`);
|
||||
}
|
||||
}
|
||||
13
bizmatch-server/src/image/image.module.ts
Normal file
13
bizmatch-server/src/image/image.module.ts
Normal file
@@ -0,0 +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';
|
||||
|
||||
@Module({
|
||||
imports: [ListingsModule],
|
||||
controllers: [ImageController],
|
||||
providers: [ImageService, FileService, SelectOptionsService],
|
||||
})
|
||||
export class ImageModule {}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AccountService {}
|
||||
export class ImageService {}
|
||||
18
bizmatch-server/src/jwt-auth/jwt-auth.guard.ts
Normal file
18
bizmatch-server/src/jwt-auth/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
|
||||
canActivate(context: ExecutionContext) {
|
||||
// Add your custom authentication logic here
|
||||
// for example, call super.logIn(request) to establish a session.
|
||||
return super.canActivate(context);
|
||||
}
|
||||
handleRequest(err, user, info) {
|
||||
// You can throw an exception based on either "info" or "err" arguments
|
||||
if (err || !user) {
|
||||
throw err || new UnauthorizedException(info);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
13
bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts
Normal file
13
bizmatch-server/src/jwt-auth/optional-jwt-auth.guard.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
@Injectable()
|
||||
export class OptionalJwtAuthGuard extends AuthGuard('jwt') {
|
||||
handleRequest(err, user, info) {
|
||||
// Wenn der Benutzer nicht authentifiziert ist, aber kein Fehler vorliegt, geben Sie null zurück
|
||||
if (err || !user) {
|
||||
return null;
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
45
bizmatch-server/src/jwt.strategy.ts
Normal file
45
bizmatch-server/src/jwt.strategy.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { passportJwtSecret } from 'jwks-rsa';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { Logger } from 'winston';
|
||||
import { JwtPayload, JwtUser } from './models/main.model';
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(
|
||||
configService: ConfigService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {
|
||||
const realm = configService.get<string>('REALM');
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKeyProvider: passportJwtSecret({
|
||||
cache: true,
|
||||
rateLimit: true,
|
||||
jwksRequestsPerMinute: 5,
|
||||
jwksUri: `https://auth.bizmatch.net/realms/${realm}/protocol/openid-connect/certs`,
|
||||
}),
|
||||
audience: 'account', // Keycloak Client ID
|
||||
authorize: '',
|
||||
issuer: `https://auth.bizmatch.net/realms/${realm}`,
|
||||
algorithms: ['RS256'],
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayload): Promise<JwtUser> {
|
||||
if (!payload) {
|
||||
this.logger.error('Invalid payload');
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
if (!payload.sub || !payload.preferred_username) {
|
||||
this.logger.error('Missing required claims');
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
const result = { userId: payload.sub, firstname: payload.given_name, lastname: payload.family_name, username: payload.preferred_username, roles: payload.realm_access?.roles };
|
||||
this.logger.info(`JWT User: ${JSON.stringify(result)}`); // Debugging: JWT Payload anzeigen
|
||||
return result;
|
||||
}
|
||||
}
|
||||
18
bizmatch-server/src/listings/broker-listings.controller.ts
Normal file
18
bizmatch-server/src/listings/broker-listings.controller.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
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,
|
||||
) {}
|
||||
|
||||
@Post('search')
|
||||
find(@Body() criteria: UserListingCriteria): any {
|
||||
return this.userService.searchUserListings(criteria);
|
||||
}
|
||||
}
|
||||
226
bizmatch-server/src/listings/business-listing.service.ts
Normal file
226
bizmatch-server/src/listings/business-listing.service.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import { BadRequestException, 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 { ZodError } from 'zod';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { businesses, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { BusinessListing, BusinessListingSchema } from '../models/db.model.js';
|
||||
import { BusinessListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
import { convertBusinessToDrizzleBusiness, convertDrizzleBusinessToBusiness, getDistanceQuery } from '../utils.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 geoService?: GeoService,
|
||||
) {}
|
||||
|
||||
private getWhereConditions(criteria: BusinessListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(businesses.city, `%${criteria.city}%`));
|
||||
}
|
||||
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||
whereConditions.push(sql`${getDistanceQuery(businesses, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
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.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}%`)));
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional'), eq(schema.users.customerSubType, 'broker')));
|
||||
return whereConditions;
|
||||
}
|
||||
async searchBusinessListings(criteria: BusinessListingCriteria, user: JwtUser) {
|
||||
const start = criteria.start ? criteria.start : 0;
|
||||
const length = criteria.length ? criteria.length : 12;
|
||||
const query = this.conn
|
||||
.select({
|
||||
business: businesses,
|
||||
brokerFirstName: schema.users.firstname,
|
||||
brokerLastName: schema.users.lastname,
|
||||
})
|
||||
.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).map(r => convertDrizzleBusinessToBusiness(r));
|
||||
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<BusinessListing> {
|
||||
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 convertDrizzleBusinessToBusiness(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));
|
||||
}
|
||||
const listings = (await this.conn
|
||||
.select()
|
||||
.from(businesses)
|
||||
.where(and(...conditions))) as BusinessListing[];
|
||||
|
||||
return listings.map(l => convertDrizzleBusinessToBusiness(l));
|
||||
}
|
||||
|
||||
// #### CREATE ########################################
|
||||
async createListing(data: BusinessListing): Promise<BusinessListing> {
|
||||
try {
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
data.updated = new Date();
|
||||
const validatedBusinessListing = BusinessListingSchema.parse(data);
|
||||
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
|
||||
const [createdListing] = await this.conn.insert(businesses).values(convertedBusinessListing).returning();
|
||||
return convertDrizzleBusinessToBusiness(createdListing);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// #### UPDATE Business ########################################
|
||||
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing> {
|
||||
try {
|
||||
data.updated = new Date();
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
const validatedBusinessListing = BusinessListingSchema.parse(data);
|
||||
const convertedBusinessListing = convertBusinessToDrizzleBusiness(data);
|
||||
const [updateListing] = await this.conn.update(businesses).set(convertedBusinessListing).where(eq(businesses.id, id)).returning();
|
||||
return convertDrizzleBusinessToBusiness(updateListing);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// #### 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`);
|
||||
}
|
||||
}
|
||||
61
bizmatch-server/src/listings/business-listings.controller.ts
Normal file
61
bizmatch-server/src/listings/business-listings.controller.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Body, Controller, Delete, Get, Inject, Param, Post, Put, Request, UseGuards } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { BusinessListing } from 'src/models/db.model.js';
|
||||
import { Logger } from 'winston';
|
||||
import { OptionalJwtAuthGuard } from '../jwt-auth/optional-jwt-auth.guard.js';
|
||||
import { BusinessListingCriteria, JwtUser } from '../models/main.model.js';
|
||||
import { BusinessListingService } from './business-listing.service.js';
|
||||
|
||||
@Controller('listings/business')
|
||||
export class BusinessListingsController {
|
||||
constructor(
|
||||
private readonly listingsService: BusinessListingService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Request() req, @Param('id') id: string): any {
|
||||
return this.listingsService.findBusinessesById(id, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('user/:userid')
|
||||
findByUserId(@Request() req, @Param('userid') userid: string): Promise<BusinessListing[]> {
|
||||
return this.listingsService.findBusinessesByEmail(userid, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('find')
|
||||
find(@Request() req, @Body() criteria: BusinessListingCriteria): any {
|
||||
return this.listingsService.searchBusinessListings(criteria, req.user as JwtUser);
|
||||
}
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: BusinessListingCriteria): Promise<number> {
|
||||
return this.listingsService.getBusinessListingsCount(criteria);
|
||||
}
|
||||
// @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);
|
||||
}
|
||||
@Put()
|
||||
update(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return this.listingsService.updateBusinessListing(listing.id, listing);
|
||||
}
|
||||
@Delete(':id')
|
||||
deleteById(@Param('id') id: string) {
|
||||
this.listingsService.deleteListing(id);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
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 { 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 { CommercialPropertyService } from './commercial-property.service.js';
|
||||
|
||||
@Controller('listings/commercialProperty')
|
||||
export class CommercialPropertyListingsController {
|
||||
constructor(
|
||||
private readonly listingsService: CommercialPropertyService,
|
||||
private fileService: FileService,
|
||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get(':id')
|
||||
findById(@Request() req, @Param('id') id: string): any {
|
||||
return this.listingsService.findCommercialPropertiesById(id, req.user as JwtUser);
|
||||
}
|
||||
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Get('user/:email')
|
||||
findByEmail(@Request() req, @Param('email') email: string): Promise<CommercialPropertyListing[]> {
|
||||
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||
}
|
||||
@UseGuards(OptionalJwtAuthGuard)
|
||||
@Post('find')
|
||||
async find(@Request() req, @Body() criteria: CommercialPropertyListingCriteria): Promise<any> {
|
||||
return await this.listingsService.searchCommercialProperties(criteria, req.user as JwtUser);
|
||||
}
|
||||
@Post('findTotal')
|
||||
findTotal(@Body() criteria: CommercialPropertyListingCriteria): Promise<number> {
|
||||
return this.listingsService.getCommercialPropertiesCount(criteria);
|
||||
}
|
||||
@Get('states/all')
|
||||
getStates(): any {
|
||||
return this.listingsService.getStates();
|
||||
}
|
||||
@Post()
|
||||
async create(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return await this.listingsService.createListing(listing);
|
||||
}
|
||||
@Put()
|
||||
async update(@Body() listing: any) {
|
||||
this.logger.info(`Save Listing`);
|
||||
return await this.listingsService.updateCommercialPropertyListing(listing.id, listing);
|
||||
}
|
||||
@Delete(':id/:imagePath')
|
||||
deleteById(@Param('id') id: string, @Param('imagePath') imagePath: string) {
|
||||
this.listingsService.deleteListing(id);
|
||||
this.fileService.deleteDirectoryIfExists(imagePath);
|
||||
}
|
||||
}
|
||||
199
bizmatch-server/src/listings/commercial-property.service.ts
Normal file
199
bizmatch-server/src/listings/commercial-property.service.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { BadRequestException, 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 { ZodError } from 'zod';
|
||||
import * as schema from '../drizzle/schema.js';
|
||||
import { commercials, PG_CONNECTION } from '../drizzle/schema.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model.js';
|
||||
import { CommercialPropertyListingCriteria, emailToDirName, JwtUser } from '../models/main.model.js';
|
||||
import { convertCommercialToDrizzleCommercial, convertDrizzleCommercialToCommercial, getDistanceQuery } from '../utils.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 geoService?: GeoService,
|
||||
) {}
|
||||
private getWhereConditions(criteria: CommercialPropertyListingCriteria): SQL[] {
|
||||
const whereConditions: SQL[] = [];
|
||||
|
||||
if (criteria.city && criteria.searchType === 'exact') {
|
||||
whereConditions.push(ilike(schema.commercials.city, `%${criteria.city}%`));
|
||||
}
|
||||
if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) {
|
||||
const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city);
|
||||
whereConditions.push(sql`${getDistanceQuery(commercials, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`);
|
||||
}
|
||||
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.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}%`)));
|
||||
}
|
||||
whereConditions.push(and(eq(schema.users.customerType, 'professional')));
|
||||
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({ commercial: commercials }).from(commercials).leftJoin(schema.users, eq(commercials.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 results = data.map(r => r.commercial).map(r => convertDrizzleCommercialToCommercial(r));
|
||||
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).leftJoin(schema.users, eq(commercials.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;
|
||||
}
|
||||
|
||||
// #### 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 convertDrizzleCommercialToCommercial(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));
|
||||
}
|
||||
const listings = (await this.conn
|
||||
.select()
|
||||
.from(commercials)
|
||||
.where(and(...conditions))) as CommercialPropertyListing[];
|
||||
return listings.map(l => convertDrizzleCommercialToCommercial(l)) 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 convertDrizzleCommercialToCommercial(result[0]) as CommercialPropertyListing;
|
||||
}
|
||||
// #### CREATE ########################################
|
||||
async createListing(data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
|
||||
try {
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
data.updated = new Date();
|
||||
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
|
||||
const convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
|
||||
const [createdListing] = await this.conn.insert(commercials).values(convertedCommercialPropertyListing).returning();
|
||||
return convertDrizzleCommercialToCommercial(createdListing);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// #### UPDATE CommercialProps ########################################
|
||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<CommercialPropertyListing> {
|
||||
try {
|
||||
data.updated = new Date();
|
||||
data.created = data.created ? (typeof data.created === 'string' ? new Date(data.created) : data.created) : new Date();
|
||||
const validatedCommercialPropertyListing = CommercialPropertyListingSchema.parse(data);
|
||||
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 convertedCommercialPropertyListing = convertCommercialToDrizzleCommercial(data);
|
||||
const [updateListing] = await this.conn.update(commercials).set(convertedCommercialPropertyListing).where(eq(commercials.id, id)).returning();
|
||||
return convertDrizzleCommercialToCommercial(updateListing);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// ##############################################################
|
||||
// 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`);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
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 { RedisService } from '../redis/redis.service.js';
|
||||
import { ListingsService } from './listings.service.js';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
@Controller('listings')
|
||||
export class ListingsController {
|
||||
// private readonly logger = new Logger(ListingsController.name);
|
||||
|
||||
constructor(private readonly listingsService:ListingsService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {
|
||||
}
|
||||
|
||||
@Get()
|
||||
findAll(): any {
|
||||
return this.listingsService.getAllListings();
|
||||
}
|
||||
@Get(':id')
|
||||
findById(@Param('id') id:string): any {
|
||||
return this.listingsService.getListingById(id);
|
||||
}
|
||||
// @Get(':type/:location/:minPrice/:maxPrice/:realEstateChecked')
|
||||
// find(@Param('type') type:string,@Param('location') location:string,@Param('minPrice') minPrice:string,@Param('maxPrice') maxPrice:string,@Param('realEstateChecked') realEstateChecked:boolean): any {
|
||||
// return this.listingsService.find(type,location,minPrice,maxPrice,realEstateChecked);
|
||||
// }
|
||||
@Post('search')
|
||||
find(@Body() criteria: any): any {
|
||||
return this.listingsService.find(criteria);
|
||||
}
|
||||
/**
|
||||
* @param listing updates a new listing
|
||||
*/
|
||||
@Put(':id')
|
||||
updateById(@Param('id') id:string, @Body() listing: any){
|
||||
this.logger.info(`Update by ID: ${id}`);
|
||||
this.listingsService.setListing(listing,id)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listing creates a new listing
|
||||
*/
|
||||
@Post()
|
||||
create(@Body() listing: any){
|
||||
this.logger.info(`Create Listing`);
|
||||
this.listingsService.setListing(listing)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id deletes a listing
|
||||
*/
|
||||
@Delete(':id')
|
||||
deleteById(@Param('id') id:string){
|
||||
this.listingsService.deleteListing(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
22
bizmatch-server/src/listings/listings.module.ts
Normal file
22
bizmatch-server/src/listings/listings.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
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 { GeoModule } from '../geo/geo.module.js';
|
||||
import { GeoService } from '../geo/geo.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, GeoModule],
|
||||
controllers: [BusinessListingsController, CommercialPropertyListingsController, UnknownListingsController, BrokerListingsController],
|
||||
providers: [BusinessListingService, CommercialPropertyService, FileService, UserService, BusinessListingService, CommercialPropertyService, GeoService],
|
||||
exports: [BusinessListingService, CommercialPropertyService],
|
||||
})
|
||||
export class ListingsModule {}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
BusinessListing,
|
||||
InvestmentsListing,
|
||||
ListingCriteria,
|
||||
ProfessionalsBrokersListing,
|
||||
} from '../models/main.model.js';
|
||||
import { LISTINGS, RedisService } from '../redis/redis.service.js';
|
||||
import { convertStringToNullUndefined } from '../utils.js';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
@Injectable()
|
||||
export class ListingsService {
|
||||
// private readonly logger = new Logger(ListingsService.name);
|
||||
constructor(private redisService: RedisService,@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
|
||||
async setListing(
|
||||
value: BusinessListing | ProfessionalsBrokersListing | InvestmentsListing,
|
||||
id?: string,
|
||||
) {
|
||||
if (!id) {
|
||||
id = await this.redisService.getId(LISTINGS);
|
||||
value.id = id;
|
||||
this.logger.info(`No ID - creating new one:${id}`)
|
||||
} else {
|
||||
this.logger.info(`ID available:${id}`)
|
||||
}
|
||||
this.redisService.setJson(id, value);
|
||||
}
|
||||
|
||||
async getListingById(id: string) {
|
||||
return await this.redisService.getJson(id, LISTINGS);
|
||||
}
|
||||
deleteListing(id: string){
|
||||
this.redisService.delete(id);
|
||||
this.logger.info(`delete listing with ID:${id}`)
|
||||
}
|
||||
async getAllListings(start?: number, end?: number) {
|
||||
const searchResult = await this.redisService.search(LISTINGS, '*');
|
||||
const listings = searchResult.slice(1).reduce((acc, item, index, array) => {
|
||||
// Jedes zweite Element (beginnend mit dem ersten) ist ein JSON-String des Listings
|
||||
if (index % 2 === 1) {
|
||||
try {
|
||||
const listing = JSON.parse(item[1]); // Parsen des JSON-Strings
|
||||
acc.push(listing);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Parsen des JSON-Strings: ', error);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return listings;
|
||||
}
|
||||
//criteria.type,criteria.location,criteria.minPrice,criteria.maxPrice,criteria.realEstateChecked,criteria.listingsCategory
|
||||
//async find(type:string,location:string,minPrice:string,maxPrice:string,realEstateChecked:boolean,listingsCategory:string): Promise<any> {
|
||||
async find(criteria:ListingCriteria): Promise<any> {
|
||||
let listings = await this.getAllListings();
|
||||
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.location)){
|
||||
console.log(criteria.location);
|
||||
listings=listings.filter(l=>l.location===criteria.location);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
18
bizmatch-server/src/listings/unknown-listings.controller.ts
Normal file
18
bizmatch-server/src/listings/unknown-listings.controller.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Controller, Inject } from '@nestjs/common';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { Logger } from 'winston';
|
||||
|
||||
@Controller('listings/undefined')
|
||||
export class UnknownListingsController {
|
||||
constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) {}
|
||||
|
||||
// @Get(':id')
|
||||
// async findById(@Param('id') id: string): Promise<any> {
|
||||
// const result = await this.listingsService.findById(id, businesses);
|
||||
// if (result) {
|
||||
// return result;
|
||||
// } else {
|
||||
// return await this.listingsService.findById(id, commercials);
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ErrorResponse, MailInfo } from '../models/main.model';
|
||||
import { MailService } from './mail.service.js';
|
||||
import { MailInfo } from '../models/server.model.js';
|
||||
|
||||
@Controller('mail')
|
||||
export class MailController {
|
||||
constructor(private mailService:MailService){
|
||||
|
||||
}
|
||||
@Post(':id')
|
||||
sendEMail(@Param('id') id:string,@Body() mailInfo: MailInfo): any {
|
||||
return this.mailService.sendInquiry(id,mailInfo);
|
||||
constructor(private mailService: MailService) {}
|
||||
@Post()
|
||||
sendEMail(@Body() mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
if (mailInfo.listing) {
|
||||
return this.mailService.sendInquiry(mailInfo);
|
||||
} else {
|
||||
return this.mailService.sendRequest(mailInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { MailService } from './mail.service.js';
|
||||
import { MailController } from './mail.controller.js';
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import path, { join } from 'path';
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter.js';
|
||||
import { Module } from '@nestjs/common';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { AuthModule } from '../auth/auth.module.js';
|
||||
import { DrizzleModule } from '../drizzle/drizzle.module.js';
|
||||
import { FileService } from '../file/file.service.js';
|
||||
import { GeoModule } from '../geo/geo.module.js';
|
||||
import { GeoService } from '../geo/geo.service.js';
|
||||
import { UserModule } from '../user/user.module.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
import { MailController } from './mail.controller.js';
|
||||
import { MailService } from './mail.service.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const user = process.env.amazon_user;
|
||||
const password = process.env.amazon_password;
|
||||
@Module({
|
||||
imports: [AuthModule,
|
||||
imports: [
|
||||
DrizzleModule,
|
||||
UserModule,
|
||||
GeoModule,
|
||||
MailerModule.forRoot({
|
||||
// transport: 'smtps://user@example.com:topsecret@smtp.example.com',
|
||||
// or
|
||||
transport: {
|
||||
host: 'smtp.gmail.com',
|
||||
secure: true,
|
||||
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
secure: false,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: 'andreas.knuth@gmail.com',
|
||||
pass: 'ksnh xjae dqbv xana',
|
||||
user: 'AKIAU6GDWVAQ2QNFLNWN',
|
||||
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
@@ -34,7 +42,7 @@ const __dirname = path.dirname(__filename);
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [MailService],
|
||||
controllers: [MailController]
|
||||
providers: [MailService, UserService, FileService, GeoService],
|
||||
controllers: [MailController],
|
||||
})
|
||||
export class MailModule {}
|
||||
|
||||
@@ -1,24 +1,84 @@
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthService } from '../auth/auth.service.js';
|
||||
import { MailInfo } from '../models/server.model.js';
|
||||
import { User } from 'src/models/main.model.js';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import path, { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { ZodError } from 'zod';
|
||||
import { SenderSchema } from '../models/db.model.js';
|
||||
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js';
|
||||
import { UserService } from '../user/user.service.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
constructor(private mailerService: MailerService, private authService:AuthService) {}
|
||||
constructor(
|
||||
private mailerService: MailerService,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
async sendInquiry(userId:string,mailInfo: MailInfo) {
|
||||
const user = await this.authService.getUser(userId) as User;
|
||||
await this.mailerService.sendMail({
|
||||
to: user.email,
|
||||
from: '"Bizmatch Team" <info@bizmatch.net>', // override default from
|
||||
subject: `Inquiry from ${mailInfo.sender.name}`,
|
||||
template: './inquiry', // `.hbs` extension is appended automatically
|
||||
context: { // ✏️ filling curly brackets with content
|
||||
name: user.firstname,
|
||||
inquiry:mailInfo.sender.comments
|
||||
},
|
||||
});
|
||||
async sendInquiry(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
const validatedSender = SenderSchema.parse(mailInfo.sender);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const user = await this.userService.getUserByMail(mailInfo.email);
|
||||
if (isEmpty(mailInfo.sender.name)) {
|
||||
return { fields: [{ fieldname: 'name', message: 'Required' }] };
|
||||
}
|
||||
await this.mailerService.sendMail({
|
||||
to: user.email,
|
||||
from: '"Bizmatch Team" <info@bizmatch.net>', // override default from
|
||||
subject: `Inquiry from ${mailInfo.sender.name}`,
|
||||
//template: './inquiry', // `.hbs` extension is appended automatically
|
||||
template: join(__dirname, '../..', 'mail/templates/inquiry.hbs'),
|
||||
context: {
|
||||
// ✏️ filling curly brackets with content
|
||||
name: user.firstname,
|
||||
inquiry: mailInfo.sender.comments,
|
||||
internalListingNumber: mailInfo.listing.internalListingNumber,
|
||||
title: mailInfo.listing.title,
|
||||
iname: mailInfo.sender.name,
|
||||
phone: mailInfo.sender.phoneNumber,
|
||||
email: mailInfo.sender.email,
|
||||
id: mailInfo.listing.id,
|
||||
url: mailInfo.url,
|
||||
},
|
||||
});
|
||||
}
|
||||
async sendRequest(mailInfo: MailInfo): Promise<void | ErrorResponse> {
|
||||
try {
|
||||
const validatedSender = SenderSchema.parse(mailInfo.sender);
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const formattedErrors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
throw new BadRequestException(formattedErrors);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await this.mailerService.sendMail({
|
||||
to: 'support@bizmatch.net',
|
||||
from: `"Bizmatch Support Team" <info@bizmatch.net>`,
|
||||
subject: `Support Request from ${mailInfo.sender.name}`,
|
||||
//template: './inquiry', // `.hbs` extension is appended automatically
|
||||
template: join(__dirname, '../..', 'mail/templates/request.hbs'),
|
||||
context: {
|
||||
// ✏️ filling curly brackets with content
|
||||
request: mailInfo.sender.comments,
|
||||
iname: mailInfo.sender.name,
|
||||
phone: mailInfo.sender.phoneNumber,
|
||||
email: mailInfo.sender.email,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,104 @@
|
||||
<p>Hey {{ name }},</p>
|
||||
<p>You got an inquiry a</p>
|
||||
<p>
|
||||
{{inquiry}}
|
||||
</p>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notification: New Buyer Lead</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #333333;
|
||||
}
|
||||
.subheader {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #555555;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-title {
|
||||
color: #1E90FF;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #1E90FF;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.info {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.info:nth-child(even) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
.info-value {
|
||||
margin-left: 10px;
|
||||
color: #555555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">Notification: New buyer lead from the Bizmatch Network</div>
|
||||
<div class="subheader">Dear {{name}},</div>
|
||||
<p>You've received a message regarding your "{{title}}" listing.</p>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Buyer Information</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Name:</span>
|
||||
<span class="info-value">{{iname}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Email:</span>
|
||||
<span class="info-value">{{email}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Phone:</span>
|
||||
<span class="info-value">{{phone}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Comments:</span>
|
||||
<span class="info-value">{{inquiry}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Listing Information</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Headline:</span>
|
||||
<span class="info-value">{{title}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Listing ID:</span>
|
||||
<span class="info-value"><a href="{{url}}/listing/{{id}}">{{id}}</a></span>
|
||||
</div>
|
||||
{{#if internalListingNumber}}
|
||||
<div class="info">
|
||||
<span class="info-label">Ref ID:</span>
|
||||
<span class="info-value">{{internalListingNumber}}</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
83
bizmatch-server/src/mail/templates/request.hbs
Normal file
83
bizmatch-server/src/mail/templates/request.hbs
Normal file
@@ -0,0 +1,83 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Notification: New User Request</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.container {
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.header {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #333333;
|
||||
}
|
||||
.subheader {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
color: #555555;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.section-title {
|
||||
color: #1E90FF;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #1E90FF;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.info {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.info:nth-child(even) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
.info-value {
|
||||
margin-left: 10px;
|
||||
color: #555555;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">Notification: New request from a user of the Bizmatch Network</div>
|
||||
<div class="section">
|
||||
<div class="section-title">Requester Information</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Name:</span>
|
||||
<span class="info-value">{{iname}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Email:</span>
|
||||
<span class="info-value">{{email}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Contact Phone:</span>
|
||||
<span class="info-value">{{phone}}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span class="info-label">Question:</span>
|
||||
<span class="info-value">{{request}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +1,16 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import express from 'express';
|
||||
import { AppModule } from './app.module.js';
|
||||
|
||||
async function bootstrap() {
|
||||
const server = express();
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('bizmatch');
|
||||
app.enableCors({
|
||||
origin: '*',
|
||||
//origin: 'http://localhost:4200', // Die URL Ihrer Angular-App
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
allowedHeaders: 'Content-Type, Accept',
|
||||
allowedHeaders: 'Content-Type, Accept, Authorization',
|
||||
});
|
||||
//origin: 'http://localhost:4200',
|
||||
await app.listen(3000);
|
||||
|
||||
290
bizmatch-server/src/models/db.model.ts
Normal file
290
bizmatch-server/src/models/db.model.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface UserData {
|
||||
id?: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
email: string;
|
||||
phoneNumber?: string;
|
||||
description?: string;
|
||||
companyName?: string;
|
||||
companyOverview?: string;
|
||||
companyWebsite?: string;
|
||||
companyLocation?: string;
|
||||
offeredServices?: string;
|
||||
areasServed?: string[];
|
||||
hasProfile?: boolean;
|
||||
hasCompanyLogo?: boolean;
|
||||
licensedIn?: string[];
|
||||
gender?: 'male' | 'female';
|
||||
customerType?: 'buyer' | 'broker' | 'professional';
|
||||
customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
created?: Date;
|
||||
updated?: Date;
|
||||
}
|
||||
export type Gender = 'male' | 'female';
|
||||
export type CustomerType = 'buyer' | 'professional';
|
||||
export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
export type ListingsCategory = 'commercialProperty' | 'business';
|
||||
|
||||
export const GenderEnum = z.enum(['male', 'female']);
|
||||
export const CustomerTypeEnum = z.enum(['buyer', 'professional']);
|
||||
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);
|
||||
const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']);
|
||||
const TypeEnum = z.enum([
|
||||
'automotive',
|
||||
'industrialServices',
|
||||
'foodAndRestaurant',
|
||||
'realEstate',
|
||||
'retail',
|
||||
'oilfield',
|
||||
'service',
|
||||
'advertising',
|
||||
'agriculture',
|
||||
'franchise',
|
||||
'professional',
|
||||
'manufacturing',
|
||||
'uncategorized',
|
||||
]);
|
||||
|
||||
const USStates = z.enum([
|
||||
'AL',
|
||||
'AK',
|
||||
'AZ',
|
||||
'AR',
|
||||
'CA',
|
||||
'CO',
|
||||
'CT',
|
||||
'DC',
|
||||
'DE',
|
||||
'FL',
|
||||
'GA',
|
||||
'HI',
|
||||
'ID',
|
||||
'IL',
|
||||
'IN',
|
||||
'IA',
|
||||
'KS',
|
||||
'KY',
|
||||
'LA',
|
||||
'ME',
|
||||
'MD',
|
||||
'MA',
|
||||
'MI',
|
||||
'MN',
|
||||
'MS',
|
||||
'MO',
|
||||
'MT',
|
||||
'NE',
|
||||
'NV',
|
||||
'NH',
|
||||
'NJ',
|
||||
'NM',
|
||||
'NY',
|
||||
'NC',
|
||||
'ND',
|
||||
'OH',
|
||||
'OK',
|
||||
'OR',
|
||||
'PA',
|
||||
'RI',
|
||||
'SC',
|
||||
'SD',
|
||||
'TN',
|
||||
'TX',
|
||||
'UT',
|
||||
'VT',
|
||||
'VA',
|
||||
'WA',
|
||||
'WV',
|
||||
'WI',
|
||||
'WY',
|
||||
]);
|
||||
export const AreasServedSchema = z.object({
|
||||
county: z.string().nonempty('County is required'),
|
||||
state: z.string().nonempty('State is required'),
|
||||
});
|
||||
|
||||
export const LicensedInSchema = z.object({
|
||||
registerNo: z.string().nonempty('Registration number is required'),
|
||||
state: z.string().nonempty('State is required'),
|
||||
});
|
||||
export const GeoSchema = z.object({
|
||||
city: z.string(),
|
||||
state: z.string().refine(val => USStates.safeParse(val).success, {
|
||||
message: 'Invalid state. Must be a valid 2-letter US state code.',
|
||||
}),
|
||||
latitude: z.number().refine(
|
||||
value => {
|
||||
return value >= -90 && value <= 90;
|
||||
},
|
||||
{
|
||||
message: 'Latitude muss zwischen -90 und 90 liegen',
|
||||
},
|
||||
),
|
||||
longitude: z.number().refine(
|
||||
value => {
|
||||
return value >= -180 && value <= 180;
|
||||
},
|
||||
{
|
||||
message: 'Longitude muss zwischen -180 und 180 liegen',
|
||||
},
|
||||
),
|
||||
});
|
||||
const phoneRegex = /^\(\d{3}\)\s\d{3}-\d{4}$/;
|
||||
|
||||
export const UserSchema = z
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
firstname: z.string().min(2, { message: 'First name must contain at least 2 characters' }),
|
||||
lastname: z.string().min(2, { message: 'Last name must contain at least 2 characters' }),
|
||||
email: z.string().email({ message: 'Invalid email address' }),
|
||||
phoneNumber: z.string().optional().nullable(),
|
||||
description: z.string().optional().nullable(),
|
||||
companyName: z.string().optional().nullable(),
|
||||
companyOverview: z.string().optional().nullable(),
|
||||
companyWebsite: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
|
||||
companyLocation: GeoSchema.optional().nullable(),
|
||||
offeredServices: z.string().optional().nullable(),
|
||||
areasServed: z.array(AreasServedSchema).optional().nullable(),
|
||||
hasProfile: z.boolean().optional().nullable(),
|
||||
hasCompanyLogo: z.boolean().optional().nullable(),
|
||||
licensedIn: z.array(LicensedInSchema).optional().nullable(),
|
||||
gender: GenderEnum.optional().nullable(),
|
||||
customerType: CustomerTypeEnum,
|
||||
customerSubType: CustomerSubTypeEnum.optional().nullable(),
|
||||
created: z.date().optional().nullable(),
|
||||
updated: z.date().optional().nullable(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.customerType === 'professional') {
|
||||
if (!data.customerSubType) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Customer subtype is required for professional customers',
|
||||
path: ['customerSubType'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.phoneNumber || !phoneRegex.test(data.phoneNumber)) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Phone number is required and must be in US format (XXX) XXX-XXXX for professional customers',
|
||||
path: ['phoneNumber'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.companyOverview || data.companyOverview.length < 10) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company overview must contain at least 10 characters for professional customers',
|
||||
path: ['companyOverview'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.description || data.description.length < 10) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Description must contain at least 10 characters for professional customers',
|
||||
path: ['description'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.offeredServices || data.offeredServices.length < 10) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Offered services must contain at least 10 characters for professional customers',
|
||||
path: ['offeredServices'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.companyLocation) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Company location is required for professional customers',
|
||||
path: ['companyLocation'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!data.areasServed || data.areasServed.length < 1) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'At least one area served is required for professional customers',
|
||||
path: ['areasServed'],
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type AreasServed = z.infer<typeof AreasServedSchema>;
|
||||
export type LicensedIn = z.infer<typeof LicensedInSchema>;
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
|
||||
export const BusinessListingSchema = z.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
email: z.string().email(),
|
||||
type: z.string().refine(val => TypeEnum.safeParse(val).success, {
|
||||
message: 'Invalid type. Must be one of: ' + TypeEnum.options.join(', '),
|
||||
}),
|
||||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().max(1000000000),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
draft: z.boolean(),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
realEstateIncluded: z.boolean().optional().nullable(),
|
||||
leasedLocation: z.boolean().optional().nullable(),
|
||||
franchiseResale: z.boolean().optional().nullable(),
|
||||
salesRevenue: z.number().positive().max(100000000),
|
||||
cashFlow: z.number().positive().max(100000000),
|
||||
supportAndTraining: z.string().min(5),
|
||||
employees: z.number().int().positive().max(100000).optional().nullable(),
|
||||
established: z.number().int().min(1800).max(2030).optional().nullable(),
|
||||
internalListingNumber: z.number().int().positive().optional().nullable(),
|
||||
reasonForSale: z.string().min(5).optional().nullable(),
|
||||
brokerLicencing: z.string().min(5).optional().nullable(),
|
||||
internals: z.string().min(5).optional().nullable(),
|
||||
imageName: z.string().optional().nullable(),
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
});
|
||||
export type BusinessListing = z.infer<typeof BusinessListingSchema>;
|
||||
|
||||
export const CommercialPropertyListingSchema = z
|
||||
.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
serialId: z.number().int().positive().optional().nullable(),
|
||||
email: z.string().email(),
|
||||
type: z.string().refine(val => PropertyTypeEnum.safeParse(val).success, {
|
||||
message: 'Invalid type. Must be one of: ' + PropertyTypeEnum.options.join(', '),
|
||||
}),
|
||||
title: z.string().min(10),
|
||||
description: z.string().min(10),
|
||||
location: GeoSchema,
|
||||
price: z.number().positive().max(1000000000),
|
||||
favoritesForUser: z.array(z.string()),
|
||||
listingsCategory: ListingsCategoryEnum,
|
||||
draft: z.boolean(),
|
||||
imageOrder: z.array(z.string()),
|
||||
imagePath: z.string().nullable().optional(),
|
||||
created: z.date(),
|
||||
updated: z.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type CommercialPropertyListing = z.infer<typeof CommercialPropertyListingSchema>;
|
||||
|
||||
export const SenderSchema = z.object({
|
||||
name: z.string().min(6, { message: 'Name must be at least 6 characters long' }),
|
||||
email: z.string().email({ message: 'Invalid email address' }),
|
||||
phoneNumber: z.string().regex(/^(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/, {
|
||||
message: 'Invalid US phone number format',
|
||||
}),
|
||||
state: z.string().refine(val => USStates.safeParse(val).success, {
|
||||
message: 'Invalid state. Must be a valid 2-letter US state code.',
|
||||
}),
|
||||
comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }),
|
||||
});
|
||||
export type Sender = z.infer<typeof SenderSchema>;
|
||||
@@ -1 +0,0 @@
|
||||
../../../common-models/src/main.model.ts
|
||||
354
bizmatch-server/src/models/main.model.ts
Normal file
354
bizmatch-server/src/models/main.model.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
import { BusinessListing, CommercialPropertyListing, Sender, User } from './db.model.js';
|
||||
|
||||
export interface StatesResult {
|
||||
state: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface KeyValue {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
export interface KeyValueRatio {
|
||||
label: string;
|
||||
value: number;
|
||||
}
|
||||
export interface KeyValueStyle {
|
||||
name: string;
|
||||
value: string;
|
||||
oldValue?: string;
|
||||
icon: string;
|
||||
textColorClass: string;
|
||||
}
|
||||
export type SelectOption<T = number> = {
|
||||
value: T;
|
||||
label: string;
|
||||
};
|
||||
export type ImageType = {
|
||||
name: 'propertyPicture' | 'companyLogo' | 'profile';
|
||||
upload: string;
|
||||
delete: string;
|
||||
};
|
||||
export type ListingCategory = {
|
||||
name: 'business' | 'commercialProperty';
|
||||
};
|
||||
|
||||
export type ListingType = BusinessListing | CommercialPropertyListing;
|
||||
|
||||
export type ResponseBusinessListingArray = {
|
||||
results: BusinessListing[];
|
||||
totalCount: number;
|
||||
};
|
||||
export type ResponseBusinessListing = {
|
||||
data: BusinessListing;
|
||||
};
|
||||
export type ResponseCommercialPropertyListingArray = {
|
||||
results: CommercialPropertyListing[];
|
||||
totalCount: number;
|
||||
};
|
||||
export type ResponseCommercialPropertyListing = {
|
||||
data: CommercialPropertyListing;
|
||||
};
|
||||
export type ResponseUsersArray = {
|
||||
results: User[];
|
||||
totalCount: number;
|
||||
};
|
||||
export interface ListCriteria {
|
||||
start: number;
|
||||
length: number;
|
||||
page: number;
|
||||
types: string[];
|
||||
state: string;
|
||||
city: string;
|
||||
prompt: string;
|
||||
searchType: 'exact' | 'radius';
|
||||
// radius: '5' | '20' | '50' | '100' | '200' | '300' | '400' | '500';
|
||||
radius: number;
|
||||
criteriaType: 'businessListings' | 'commercialPropertyListings' | 'brokerListings';
|
||||
}
|
||||
export interface BusinessListingCriteria extends ListCriteria {
|
||||
minPrice: number;
|
||||
maxPrice: number;
|
||||
minRevenue: number;
|
||||
maxRevenue: number;
|
||||
minCashFlow: number;
|
||||
maxCashFlow: number;
|
||||
minNumberEmployees: number;
|
||||
maxNumberEmployees: number;
|
||||
establishedSince: number;
|
||||
establishedUntil: number;
|
||||
realEstateChecked: boolean;
|
||||
leasedLocation: boolean;
|
||||
franchiseResale: boolean;
|
||||
title: string;
|
||||
brokerName: string;
|
||||
criteriaType: 'businessListings';
|
||||
}
|
||||
export interface CommercialPropertyListingCriteria extends ListCriteria {
|
||||
minPrice: number;
|
||||
maxPrice: number;
|
||||
title: string;
|
||||
criteriaType: 'commercialPropertyListings';
|
||||
}
|
||||
export interface UserListingCriteria extends ListCriteria {
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
companyName: string;
|
||||
counties: string[];
|
||||
criteriaType: 'brokerListings';
|
||||
}
|
||||
|
||||
export interface KeycloakUser {
|
||||
id: string;
|
||||
createdTimestamp?: number;
|
||||
username?: string;
|
||||
enabled?: boolean;
|
||||
totp?: boolean;
|
||||
emailVerified?: boolean;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
disableableCredentialTypes?: any[];
|
||||
requiredActions?: any[];
|
||||
notBefore?: number;
|
||||
access?: Access;
|
||||
}
|
||||
export interface JwtUser {
|
||||
userId: string;
|
||||
username: string;
|
||||
firstname: string;
|
||||
lastname: string;
|
||||
roles: string[];
|
||||
}
|
||||
export interface Access {
|
||||
manageGroupMembership: boolean;
|
||||
view: boolean;
|
||||
mapRoles: boolean;
|
||||
impersonate: boolean;
|
||||
manage: boolean;
|
||||
}
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
userId: string;
|
||||
level: string;
|
||||
start: Date;
|
||||
modified: Date;
|
||||
end: Date;
|
||||
status: string;
|
||||
invoices: Array<Invoice>;
|
||||
}
|
||||
export interface Invoice {
|
||||
id: string;
|
||||
date: Date;
|
||||
price: number;
|
||||
}
|
||||
export interface JwtToken {
|
||||
exp: number;
|
||||
iat: number;
|
||||
auth_time: number;
|
||||
jti: string;
|
||||
iss: string;
|
||||
aud: string;
|
||||
sub: string;
|
||||
typ: string;
|
||||
azp: string;
|
||||
nonce: string;
|
||||
session_state: string;
|
||||
acr: string;
|
||||
realm_access: Realmaccess;
|
||||
resource_access: Resourceaccess;
|
||||
scope: string;
|
||||
sid: string;
|
||||
email_verified: boolean;
|
||||
name: string;
|
||||
preferred_username: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
email: string;
|
||||
user_id: string;
|
||||
}
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
preferred_username: string;
|
||||
realm_access?: {
|
||||
roles?: string[];
|
||||
};
|
||||
[key: string]: any; // für andere optionale Felder im JWT-Payload
|
||||
}
|
||||
interface Resourceaccess {
|
||||
account: Realmaccess;
|
||||
}
|
||||
interface Realmaccess {
|
||||
roles: string[];
|
||||
}
|
||||
export interface PageEvent {
|
||||
first: number;
|
||||
rows: number;
|
||||
page: number;
|
||||
pageCount: number;
|
||||
}
|
||||
export interface AutoCompleteCompleteEvent {
|
||||
originalEvent: Event;
|
||||
query: string;
|
||||
}
|
||||
export interface MailInfo {
|
||||
sender: Sender;
|
||||
email: string;
|
||||
url: string;
|
||||
listing?: BusinessListing;
|
||||
}
|
||||
// export interface Sender {
|
||||
// name?: string;
|
||||
// email?: string;
|
||||
// phoneNumber?: string;
|
||||
// state?: string;
|
||||
// comments?: string;
|
||||
// }
|
||||
export interface ImageProperty {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
export interface ErrorResponse {
|
||||
fields?: FieldError[];
|
||||
general?: string[];
|
||||
}
|
||||
export interface FieldError {
|
||||
fieldname: string;
|
||||
message: string;
|
||||
}
|
||||
export interface UploadParams {
|
||||
type: 'uploadPropertyPicture' | 'uploadCompanyLogo' | 'uploadProfile';
|
||||
imagePath: string;
|
||||
serialId?: number;
|
||||
}
|
||||
export interface GeoResult {
|
||||
id: number;
|
||||
city: string;
|
||||
state: string;
|
||||
// state_code: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
export interface CityAndStateResult {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
state: string;
|
||||
}
|
||||
export interface CountyResult {
|
||||
id: number;
|
||||
name: string;
|
||||
state: string;
|
||||
state_code: string;
|
||||
}
|
||||
export function isEmpty(value: any): boolean {
|
||||
// Check for undefined or null
|
||||
if (value === undefined || value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for empty string or string with only whitespace
|
||||
if (typeof value === 'string') {
|
||||
return value.trim().length === 0;
|
||||
}
|
||||
|
||||
// Check for number and NaN
|
||||
if (typeof value === 'number') {
|
||||
return isNaN(value);
|
||||
}
|
||||
|
||||
// If it's not a string or number, it's not considered empty by this function
|
||||
return false;
|
||||
}
|
||||
export function emailToDirName(email: string): string {
|
||||
if (email === undefined || email === null) {
|
||||
return null;
|
||||
}
|
||||
// Entferne ungültige Zeichen und ersetze sie durch Unterstriche
|
||||
const sanitizedEmail = email.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
|
||||
// Entferne führende und nachfolgende Unterstriche
|
||||
const trimmedEmail = sanitizedEmail.replace(/^_+|_+$/g, '');
|
||||
|
||||
// Ersetze mehrfache aufeinanderfolgende Unterstriche durch einen einzelnen Unterstrich
|
||||
const normalizedEmail = trimmedEmail.replace(/_+/g, '_');
|
||||
|
||||
return normalizedEmail;
|
||||
}
|
||||
export const LISTINGS_PER_PAGE = 12;
|
||||
export interface ValidationMessage {
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
export function createDefaultUser(email: string, firstname: string, lastname: string): User {
|
||||
return {
|
||||
id: undefined,
|
||||
email,
|
||||
firstname,
|
||||
lastname,
|
||||
phoneNumber: null,
|
||||
description: null,
|
||||
companyName: null,
|
||||
companyOverview: null,
|
||||
companyWebsite: null,
|
||||
companyLocation: null,
|
||||
offeredServices: null,
|
||||
areasServed: [],
|
||||
hasProfile: false,
|
||||
hasCompanyLogo: false,
|
||||
licensedIn: [],
|
||||
gender: null,
|
||||
customerType: 'buyer',
|
||||
customerSubType: null,
|
||||
created: new Date(),
|
||||
updated: new Date(),
|
||||
};
|
||||
}
|
||||
export function createDefaultCommercialPropertyListing(): CommercialPropertyListing {
|
||||
return {
|
||||
id: undefined,
|
||||
serialId: undefined,
|
||||
email: null,
|
||||
type: null,
|
||||
title: null,
|
||||
description: null,
|
||||
location: null,
|
||||
price: null,
|
||||
favoritesForUser: [],
|
||||
draft: false,
|
||||
imageOrder: [],
|
||||
imagePath: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
listingsCategory: 'commercialProperty',
|
||||
};
|
||||
}
|
||||
export function createDefaultBusinessListing(): BusinessListing {
|
||||
return {
|
||||
id: undefined,
|
||||
email: null,
|
||||
type: null,
|
||||
title: null,
|
||||
description: null,
|
||||
location: null,
|
||||
price: null,
|
||||
favoritesForUser: [],
|
||||
draft: false,
|
||||
realEstateIncluded: false,
|
||||
leasedLocation: false,
|
||||
franchiseResale: false,
|
||||
salesRevenue: null,
|
||||
cashFlow: null,
|
||||
supportAndTraining: null,
|
||||
employees: null,
|
||||
established: null,
|
||||
internalListingNumber: null,
|
||||
reasonForSale: null,
|
||||
brokerLicencing: null,
|
||||
internals: null,
|
||||
created: null,
|
||||
updated: null,
|
||||
listingsCategory: 'business',
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,72 @@
|
||||
export interface MailInfo {
|
||||
sender:Sender;
|
||||
userId:string;
|
||||
}
|
||||
export interface Sender {
|
||||
name:string;
|
||||
email:string;
|
||||
phoneNumber:string;
|
||||
state:string;
|
||||
comments:string;
|
||||
export interface Geo {
|
||||
id: number;
|
||||
name: string;
|
||||
iso3: string;
|
||||
iso2: string;
|
||||
numeric_code: string;
|
||||
phone_code: string;
|
||||
capital: string;
|
||||
currency: string;
|
||||
currency_name: string;
|
||||
currency_symbol: string;
|
||||
tld: string;
|
||||
native: string;
|
||||
region: string;
|
||||
region_id: string;
|
||||
subregion: string;
|
||||
subregion_id: string;
|
||||
nationality: string;
|
||||
timezones: Timezone[];
|
||||
translations: Translations;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
emoji: string;
|
||||
emojiU: string;
|
||||
states: State[];
|
||||
}
|
||||
export interface State {
|
||||
id: number;
|
||||
name: string;
|
||||
state_code: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
type: string;
|
||||
cities: City[];
|
||||
}
|
||||
export interface City {
|
||||
id: number;
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
export interface Translations {
|
||||
kr: string;
|
||||
'pt-BR': string;
|
||||
pt: string;
|
||||
nl: string;
|
||||
hr: string;
|
||||
fa: string;
|
||||
de: string;
|
||||
es: string;
|
||||
fr: string;
|
||||
ja: string;
|
||||
it: string;
|
||||
cn: string;
|
||||
tr: string;
|
||||
}
|
||||
export interface Timezone {
|
||||
zoneName: string;
|
||||
gmtOffset: number;
|
||||
gmtOffsetName: string;
|
||||
abbreviation: string;
|
||||
tzName: string;
|
||||
}
|
||||
export interface CountyData {
|
||||
state: string;
|
||||
state_full: string;
|
||||
counties: string[];
|
||||
}
|
||||
export interface CountyRequest {
|
||||
prefix: string;
|
||||
states: string[];
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Body, Controller, Get, HttpStatus, Param, Post, Res } from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
import { RedisService } from './redis.service.js';
|
||||
|
||||
@Controller('redis')
|
||||
export class RedisController {
|
||||
constructor(private redisService:RedisService){}
|
||||
|
||||
// @Get(':id')
|
||||
// getById(@Param('id') id:string){
|
||||
// return this.redisService.getListingById(id);
|
||||
// }
|
||||
|
||||
// @Post(':id')
|
||||
// updateById(@Body() listing: any){
|
||||
// this.redisService.setListing(listing);
|
||||
// }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user