Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ad4871bdc | |||
| 66cab810c1 | |||
| 1bc01337fc | |||
| 186bcbf6b8 | |||
| 5dc5cd87a8 | |||
| 08404eb145 | |||
| fdfaafcdda | |||
| 018d9ad41d | |||
| ba38ce2865 | |||
| 812b2171b2 |
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
redis:
|
||||
image: redis:latest
|
||||
restart: always
|
||||
ports:
|
||||
- "6379:6379"
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
Generated
+208
-1
@@ -10,6 +10,7 @@
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@prisma/client": "^6.18.0",
|
||||
@@ -18,9 +19,11 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cloudinary": "^2.8.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"jose": "3.16.0",
|
||||
"multer": "^2.0.2",
|
||||
"nodemailer": "^7.0.11",
|
||||
"redis": "^5.10.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
@@ -2180,6 +2183,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
|
||||
"integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@@ -3174,6 +3183,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/config": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz",
|
||||
"integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "16.4.7",
|
||||
"dotenv-expand": "12.0.1",
|
||||
"lodash": "4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"rxjs": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/config/node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/core": {
|
||||
"version": "11.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.8.tgz",
|
||||
@@ -3776,6 +3812,66 @@
|
||||
"@prisma/debug": "6.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.10.0.tgz",
|
||||
"integrity": "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/client": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/client/-/client-5.10.0.tgz",
|
||||
"integrity": "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cluster-key-slot": "1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/json": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/json/-/json-5.10.0.tgz",
|
||||
"integrity": "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/search": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/search/-/search-5.10.0.tgz",
|
||||
"integrity": "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/time-series": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.10.0.tgz",
|
||||
"integrity": "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@redis/client": "^5.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@simplewebauthn/browser": {
|
||||
"version": "13.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz",
|
||||
@@ -6565,6 +6661,15 @@
|
||||
"node": ">=9"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -6912,6 +7017,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -6963,7 +7077,6 @@
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"devOptional": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -6972,6 +7085,21 @@
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv-expand": {
|
||||
"version": "12.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz",
|
||||
"integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dset": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
|
||||
@@ -8445,6 +8573,30 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.8.2",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz",
|
||||
"integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "1.4.0",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"denque": "^2.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.isarguments": "^3.1.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
@@ -9663,6 +9815,18 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@@ -10857,6 +11021,43 @@
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-5.10.0.tgz",
|
||||
"integrity": "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@redis/bloom": "5.10.0",
|
||||
"@redis/client": "5.10.0",
|
||||
"@redis/json": "5.10.0",
|
||||
"@redis/search": "5.10.0",
|
||||
"@redis/time-series": "5.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
@@ -11297,6 +11498,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@prisma/client": "^6.18.0",
|
||||
@@ -29,9 +30,11 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cloudinary": "^2.8.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"jose": "3.16.0",
|
||||
"multer": "^2.0.2",
|
||||
"nodemailer": "^7.0.11",
|
||||
"redis": "^5.10.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
|
||||
+1
-1
@@ -7,6 +7,6 @@ export default defineConfig({
|
||||
},
|
||||
engine: 'classic',
|
||||
datasource: {
|
||||
url: 'postgresql://postgres:root@localhost:5432/zemenu?schema=public',
|
||||
url: 'postgresql://zemenu_db_user:Bx5QvACdW9iMC7k6guIQHZUM60c0a6jZ@dpg-d4n9ekggjchc73buc9qg-a.oregon-postgres.render.com/zemenu_db',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Restaurant" ADD COLUMN "banner" TEXT;
|
||||
@@ -0,0 +1,11 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "ContactMessage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"subject" TEXT,
|
||||
"message" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ContactMessage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
+11
-1
@@ -5,7 +5,7 @@ generator client {
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = "postgresql://postgres:root@localhost:5432/zemenu?schema=public"
|
||||
url = "postgresql://zemenu_db_user:Bx5QvACdW9iMC7k6guIQHZUM60c0a6jZ@dpg-d4n9ekggjchc73buc9qg-a.oregon-postgres.render.com/zemenu_db"
|
||||
}
|
||||
|
||||
model User {
|
||||
@@ -84,6 +84,7 @@ model Restaurant {
|
||||
slug String @unique // used as /domain.com/slug
|
||||
description String?
|
||||
logo String? // image URL
|
||||
banner String?
|
||||
ownerId String
|
||||
owner User @relation(fields: [ownerId], references: [id])
|
||||
|
||||
@@ -134,3 +135,12 @@ model File {
|
||||
|
||||
@@index([folder, originalName])
|
||||
}
|
||||
|
||||
model ContactMessage {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
email String
|
||||
subject String?
|
||||
message String
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ import { CategoryModule } from './category/category.module';
|
||||
import { RestaurantModule } from './restaurant/restaurant.module';
|
||||
import { ItemModule } from './item/item.module';
|
||||
import { FileModule } from './file/file.module';
|
||||
import { RedisModule } from './redis/redis.module';
|
||||
import { CacheModule } from './cache/cache.module';
|
||||
import { ContactModule } from './contact/contact.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -17,6 +20,9 @@ import { FileModule } from './file/file.module';
|
||||
RestaurantModule,
|
||||
ItemModule,
|
||||
FileModule,
|
||||
RedisModule,
|
||||
CacheModule,
|
||||
ContactModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export const serialize = (v: any) => JSON.stringify(v);
|
||||
export const deserialize = (s: string | null) => (s ? JSON.parse(s) : null);
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
export const keys = {
|
||||
restaurantBySlug: (slug: string) => `restaurant:slug:${slug}`,
|
||||
restaurantById: (id: string) => `restaurant:id:${id}`,
|
||||
restaurantCategories: (restaurantId: string) =>
|
||||
`restaurant:${restaurantId}:categories`,
|
||||
restaurantItems: (restaurantId: string) => `restaurant:${restaurantId}:items`,
|
||||
userRestaurants: (ownerId: string) => `user:${ownerId}:restaurants`,
|
||||
category: (categoryId: string) => `category:${categoryId}`,
|
||||
categoryItems: (categoryId: string) => `category:${categoryId}:items`,
|
||||
item: (itemId: string) => `item:${itemId}`,
|
||||
user: (userId: string) => `user:${userId}`,
|
||||
};
|
||||
Vendored
+11
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CacheService } from './cache.service';
|
||||
import { RedisModule } from '../redis/redis.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
|
||||
@Module({
|
||||
imports: [RedisModule, ConfigModule],
|
||||
providers: [CacheService],
|
||||
exports: [CacheService],
|
||||
})
|
||||
export class CacheModule {}
|
||||
Vendored
+74
@@ -0,0 +1,74 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import type { Redis } from 'ioredis';
|
||||
import { REDIS_CLIENT } from '../redis/redis.types';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { serialize, deserialize } from './cache.helpers';
|
||||
|
||||
@Injectable()
|
||||
export class CacheService {
|
||||
private readonly ttl: number;
|
||||
private readonly enabled: boolean;
|
||||
private readonly logger = new Logger(CacheService.name);
|
||||
|
||||
constructor(
|
||||
@Inject(REDIS_CLIENT) private readonly redis: Redis,
|
||||
private readonly config: ConfigService,
|
||||
) {
|
||||
this.ttl = Number(config.get<number>('REDIS_DEFAULT_TTL_SECONDS') ?? 600);
|
||||
this.enabled = config.get<boolean>('REDIS_CACHE_ENABLED') ?? true;
|
||||
}
|
||||
|
||||
async get<T = any>(key: string): Promise<T | null> {
|
||||
if (!this.enabled) return null;
|
||||
try {
|
||||
const raw = await this.redis.get(key);
|
||||
return deserialize(raw);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Redis GET failed for ${key}: ${err}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async set(key: string, value: any, ttlSeconds?: number): Promise<void> {
|
||||
if (!this.enabled) return;
|
||||
try {
|
||||
const payload = serialize(value);
|
||||
const ttl = ttlSeconds ?? this.ttl;
|
||||
if (ttl > 0) {
|
||||
await this.redis.set(key, payload, 'EX', Math.floor(ttl));
|
||||
} else {
|
||||
await this.redis.set(key, payload);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`Redis SET failed for ${key}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
async del(key: string): Promise<void> {
|
||||
if (!this.enabled) return;
|
||||
try {
|
||||
await this.redis.del(key);
|
||||
} catch (err) {
|
||||
this.logger.warn(`Redis DEL failed for ${key}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
// optional: delete many keys by pattern (use with caution in prod)
|
||||
async delByPattern(pattern: string): Promise<void> {
|
||||
if (!this.enabled) return;
|
||||
try {
|
||||
const stream = this.redis.scanStream({
|
||||
match: pattern,
|
||||
count: 100,
|
||||
});
|
||||
|
||||
for await (const keys of stream) {
|
||||
if (keys.length) {
|
||||
await this.redis.del(...keys);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.warn(`Redis DELBYPATTERN failed for ${pattern}: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,10 @@ import { CategoryController } from './category.controller';
|
||||
import { CategoryService } from './category.service';
|
||||
import { RestaurantModule } from 'src/restaurant/restaurant.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { CacheModule } from 'src/cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [RestaurantModule, PrismaModule],
|
||||
imports: [RestaurantModule, PrismaModule, CacheModule],
|
||||
controllers: [CategoryController],
|
||||
providers: [CategoryService],
|
||||
exports: [CategoryService],
|
||||
|
||||
@@ -4,16 +4,45 @@ import { RestaurantService } from 'src/restaurant/restaurant.service';
|
||||
import { CreateCategoryDto } from './dto/create-category.dto';
|
||||
import { UpdateCategoryDto } from './dto/update-category.dto';
|
||||
import { CategoryDto } from './dto/category.dto';
|
||||
import { CacheService } from 'src/cache/cache.service';
|
||||
import { keys } from 'src/cache/cache.keys';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private readonly restaurantService: RestaurantService,
|
||||
private cache: CacheService,
|
||||
) {}
|
||||
|
||||
async _getCategorie(categoryId: string) {
|
||||
const cacheKey = keys.category(categoryId);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const category = await this.prisma.category
|
||||
.findFirstOrThrow({
|
||||
where: { id: categoryId },
|
||||
include: { items: true, restaurant: true },
|
||||
})
|
||||
.catch(() => {
|
||||
throw new NotFoundException('Category not found');
|
||||
});
|
||||
|
||||
await this.cache.set(cacheKey, category, 600);
|
||||
return category;
|
||||
}
|
||||
|
||||
async create(dto: CreateCategoryDto, userId: string) {
|
||||
await this.restaurantService.verifyOwnership(userId, dto.restaurantId);
|
||||
const restaurant = await this.prisma.restaurant
|
||||
.findUniqueOrThrow({
|
||||
where: { id: dto.restaurantId },
|
||||
})
|
||||
.catch(() => {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
});
|
||||
|
||||
await this.restaurantService.verifyOwnership(userId, restaurant.id);
|
||||
|
||||
const category = await this.prisma.category.create({
|
||||
data: {
|
||||
@@ -23,35 +52,43 @@ export class CategoryService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.cache.del(keys.restaurantBySlug(restaurant.slug));
|
||||
await this.cache.del(keys.restaurantCategories(restaurant.id));
|
||||
|
||||
return CategoryDto.fromEntity(category);
|
||||
}
|
||||
|
||||
async findAllForRestaurant(restaurantId: string, userId: string) {
|
||||
await this.restaurantService.verifyOwnership(userId, restaurantId);
|
||||
|
||||
const cacheKey = keys.restaurantCategories(restaurantId);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const categories = await this.prisma.category.findMany({
|
||||
where: { restaurantId },
|
||||
include: { items: true },
|
||||
});
|
||||
|
||||
return categories.map(CategoryDto.fromEntity);
|
||||
const result = categories.map(CategoryDto.fromEntity);
|
||||
await this.cache.set(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const category = await this.prisma.category.findUnique({
|
||||
where: { id },
|
||||
include: { items: true },
|
||||
});
|
||||
const cacheKey = keys.category(id);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
if (!category) throw new NotFoundException('Category not found');
|
||||
const category = await this._getCategorie(id);
|
||||
const result = CategoryDto.fromEntity(category);
|
||||
|
||||
|
||||
|
||||
return CategoryDto.fromEntity(category);
|
||||
await this.cache.set(cacheKey, result, 600);
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateCategoryDto, userId: string) {
|
||||
const category = await this.findOne(id);
|
||||
const category = await this._getCategorie(id);
|
||||
|
||||
await this.restaurantService.verifyOwnership(userId, category.restaurantId);
|
||||
|
||||
@@ -59,18 +96,29 @@ export class CategoryService {
|
||||
where: { id },
|
||||
data: {
|
||||
name: dto.name,
|
||||
restaurantId: dto.restaurantId,
|
||||
// restaurantId: dto.restaurantId,
|
||||
},
|
||||
});
|
||||
|
||||
if (category.restaurant) {
|
||||
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
|
||||
}
|
||||
await this.cache.del(keys.restaurantCategories(category.restaurantId));
|
||||
|
||||
return CategoryDto.fromEntity(updated);
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string) {
|
||||
const category = await this.findOne(id);
|
||||
const category = await this._getCategorie(id);
|
||||
|
||||
await this.restaurantService.verifyOwnership(userId, category.restaurantId);
|
||||
|
||||
if (category.restaurant) {
|
||||
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
|
||||
}
|
||||
|
||||
await this.cache.del(keys.restaurantCategories(category.restaurantId));
|
||||
|
||||
return this.prisma.category.delete({ where: { id } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MenuItemDto } from "src/item/dto/menu-item.dto";
|
||||
import { RestaurantDto } from "src/restaurant/dto/restaurant.dto";
|
||||
|
||||
export class CategoryDto {
|
||||
id: string;
|
||||
@@ -6,6 +7,7 @@ export class CategoryDto {
|
||||
image: string;
|
||||
restaurantId: string;
|
||||
items?: MenuItemDto[];
|
||||
restaurant?: RestaurantDto;
|
||||
|
||||
static fromEntity(entity: any): CategoryDto {
|
||||
return {
|
||||
@@ -14,6 +16,7 @@ export class CategoryDto {
|
||||
image: entity.image,
|
||||
restaurantId: entity.restaurantId,
|
||||
items: entity.items?.map(MenuItemDto.fromEntity) ?? [],
|
||||
restaurant: entity.restaurant ? RestaurantDto.fromEntity(entity.restaurant) : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||
import { ContactService } from './contact.service';
|
||||
import { AllowAnonymous, AuthGuard } from '@thallesp/nestjs-better-auth';
|
||||
import { CreateContactDto } from './dto/createContant.dto';
|
||||
|
||||
@Controller('contact')
|
||||
@UseGuards(AuthGuard)
|
||||
export class ContactController {
|
||||
constructor(private readonly contactService: ContactService) {}
|
||||
|
||||
@Post()
|
||||
@AllowAnonymous()
|
||||
async create(@Body() data: CreateContactDto) {
|
||||
return this.contactService.create(data);
|
||||
}
|
||||
|
||||
@Get()
|
||||
async findAll() {
|
||||
return this.contactService.findAll();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ContactService } from './contact.service';
|
||||
import { ContactController } from './contact.controller';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [ContactService],
|
||||
controllers: [ContactController],
|
||||
})
|
||||
export class ContactModule {}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { emailService } from '../shared/services/email.service';
|
||||
import { CreateContactDto } from './dto/createContant.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ContactService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async create(data: CreateContactDto) {
|
||||
const message = await this.prisma.contactMessage.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
subject: data.subject,
|
||||
message: data.message,
|
||||
},
|
||||
});
|
||||
|
||||
// Send email notification (fire and forget, or await if critical)
|
||||
// We'll catch errors so it doesn't fail the request if email fails
|
||||
try {
|
||||
await emailService.sendContactMessageNotification({
|
||||
name: message.name,
|
||||
email: message.email,
|
||||
subject: message.subject || undefined,
|
||||
message: message.message,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to send contact notification email:', error);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
return this.prisma.contactMessage.findMany({
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { IsString, IsEmail, IsNotEmpty, IsOptional, MaxLength } from 'class-validator';
|
||||
|
||||
export class CreateContactDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(100)
|
||||
name: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsNotEmpty()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(20)
|
||||
subject?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(1000)
|
||||
message: string;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { CategoryDto } from 'src/category/dto/category.dto';
|
||||
|
||||
export class MenuItemDto {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
price: number;
|
||||
image?: string;
|
||||
categoryId?: string;
|
||||
categoryId: string;
|
||||
category?: CategoryDto;
|
||||
restaurantId: string;
|
||||
createdAt?: Date;
|
||||
|
||||
@@ -15,7 +18,10 @@ export class MenuItemDto {
|
||||
description: entity.description ?? undefined,
|
||||
price: entity.price,
|
||||
image: entity.image ?? undefined,
|
||||
categoryId: entity.categoryId ?? undefined,
|
||||
categoryId: entity.categoryId,
|
||||
category: entity.category
|
||||
? CategoryDto.fromEntity(entity.category)
|
||||
: undefined,
|
||||
restaurantId: entity.restaurantId,
|
||||
createdAt: entity.createdAt ? new Date(entity.createdAt) : undefined,
|
||||
};
|
||||
|
||||
@@ -4,9 +4,10 @@ import { ItemService } from './item.service';
|
||||
import { RestaurantModule } from 'src/restaurant/restaurant.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { CategoryModule } from 'src/category/category.module';
|
||||
import { CacheModule } from 'src/cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [RestaurantModule, CategoryModule, PrismaModule],
|
||||
imports: [RestaurantModule, CategoryModule, PrismaModule, CacheModule],
|
||||
controllers: [ItemController],
|
||||
providers: [ItemService],
|
||||
})
|
||||
|
||||
+68
-12
@@ -5,6 +5,8 @@ import { CreateItemDto } from './dto/create-item.dto';
|
||||
import { UpdateItemDto } from './dto/update-item.dto';
|
||||
import { MenuItemDto } from './dto/menu-item.dto';
|
||||
import { CategoryService } from 'src/category/category.service';
|
||||
import { CacheService } from 'src/cache/cache.service';
|
||||
import { keys } from 'src/cache/cache.keys';
|
||||
|
||||
@Injectable()
|
||||
export class ItemService {
|
||||
@@ -12,10 +14,11 @@ export class ItemService {
|
||||
private prisma: PrismaService,
|
||||
private readonly restaurantService: RestaurantService,
|
||||
private readonly categoryService: CategoryService,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
async create(dto: CreateItemDto, userId: string) {
|
||||
const category = await this.categoryService.findOne(dto.categoryId);
|
||||
const category = await this.categoryService._getCategorie(dto.categoryId);
|
||||
await this.restaurantService.verifyOwnership(userId, category.restaurantId);
|
||||
|
||||
const item = await this.prisma.menuItem.create({
|
||||
@@ -29,32 +32,58 @@ export class ItemService {
|
||||
},
|
||||
});
|
||||
|
||||
if (category.restaurant) {
|
||||
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
|
||||
}
|
||||
await this.cache.del(keys.restaurantCategories(category.restaurantId));
|
||||
await this.cache.del(keys.restaurantItems(category.restaurantId));
|
||||
await this.cache.del(keys.categoryItems(category.id));
|
||||
await this.cache.del(keys.category(category.id));
|
||||
|
||||
return MenuItemDto.fromEntity(item);
|
||||
}
|
||||
|
||||
async findAllByCategory(categoryId: string) {
|
||||
const cacheKey = keys.categoryItems(categoryId);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const items = await this.prisma.menuItem.findMany({
|
||||
where: { categoryId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return items.map(MenuItemDto.fromEntity);
|
||||
const result = items.map(MenuItemDto.fromEntity);
|
||||
await this.cache.set(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
async findAllForRestaurant(restaurantId: string) {
|
||||
const cacheKey = keys.restaurantItems(restaurantId);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const items = await this.prisma.menuItem.findMany({
|
||||
where: { restaurantId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
return items.map(MenuItemDto.fromEntity);
|
||||
const result = items.map(MenuItemDto.fromEntity);
|
||||
await this.cache.set(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const cacheKey = keys.item(id);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const item = await this.prisma.menuItem.findUnique({ where: { id } });
|
||||
|
||||
if (!item) throw new NotFoundException('Menu item not found');
|
||||
|
||||
return MenuItemDto.fromEntity(item);
|
||||
const result = MenuItemDto.fromEntity(item);
|
||||
await this.cache.set(cacheKey, result, 600);
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateItemDto, userId: string) {
|
||||
@@ -62,6 +91,10 @@ export class ItemService {
|
||||
|
||||
await this.restaurantService.verifyOwnership(userId, item.restaurantId);
|
||||
|
||||
const category = await this.categoryService._getCategorie(
|
||||
dto.categoryId != null ? dto.categoryId : item.categoryId,
|
||||
);
|
||||
|
||||
const updated = await this.prisma.menuItem.update({
|
||||
where: { id },
|
||||
data: {
|
||||
@@ -69,23 +102,46 @@ export class ItemService {
|
||||
description: dto.description,
|
||||
price: dto.price,
|
||||
image: dto.image,
|
||||
categoryId: dto.categoryId
|
||||
? (await this.categoryService.findOne(dto.categoryId)).id
|
||||
: item.categoryId,
|
||||
categoryId: category.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (category.restaurant) {
|
||||
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
|
||||
}
|
||||
await this.cache.del(keys.restaurantCategories(category.restaurantId));
|
||||
await this.cache.del(keys.restaurantItems(category.restaurantId));
|
||||
await this.cache.del(keys.categoryItems(category.id));
|
||||
await this.cache.del(keys.category(category.id));
|
||||
await this.cache.del(keys.item(id));
|
||||
|
||||
// If category changed, invalidate old category caches too
|
||||
if (dto.categoryId && dto.categoryId !== item.categoryId) {
|
||||
await this.cache.del(keys.categoryItems(item.categoryId));
|
||||
await this.cache.del(keys.category(item.categoryId));
|
||||
}
|
||||
|
||||
return MenuItemDto.fromEntity(updated);
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string) {
|
||||
const item = await this.findOne(id);
|
||||
|
||||
await this.restaurantService.verifyOwnership(
|
||||
userId,
|
||||
item.restaurantId,
|
||||
);
|
||||
await this.restaurantService.verifyOwnership(userId, item.restaurantId);
|
||||
|
||||
return this.prisma.menuItem.delete({ where: { id } });
|
||||
const deleted = await this.prisma.menuItem.delete({ where: { id } });
|
||||
|
||||
const category = await this.categoryService._getCategorie(item.categoryId);
|
||||
|
||||
if (category.restaurant) {
|
||||
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
|
||||
}
|
||||
await this.cache.del(keys.restaurantCategories(category.restaurantId));
|
||||
await this.cache.del(keys.restaurantItems(category.restaurantId));
|
||||
await this.cache.del(keys.categoryItems(category.id));
|
||||
await this.cache.del(keys.category(category.id));
|
||||
await this.cache.del(keys.item(id));
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, { bodyParser: false });
|
||||
|
||||
app.enableCors({
|
||||
origin: ['http://localhost:5173', 'http://localhost:5174'],
|
||||
origin: ['http://localhost:5173', 'http://localhost:5174', 'http://localhost:3000', 'http://localhost:3001'],
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { RedisProvider } from './redis.provider';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
providers: [RedisProvider],
|
||||
exports: [RedisProvider],
|
||||
})
|
||||
export class RedisModule {}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Provider } from '@nestjs/common';
|
||||
import IORedis from 'ioredis';
|
||||
import { REDIS_CLIENT } from './redis.types';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
export const RedisProvider = {
|
||||
provide: REDIS_CLIENT,
|
||||
useFactory: (config: ConfigService) => {
|
||||
const url = config.get<string>('REDIS_URL') ?? 'redis://127.0.0.1:6379';
|
||||
const client = new IORedis(url, {
|
||||
// optional production tuning:
|
||||
lazyConnect: false,
|
||||
maxRetriesPerRequest: 3,
|
||||
enableOfflineQueue: true,
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
// log error with your logger
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Redis error', err);
|
||||
});
|
||||
|
||||
return client;
|
||||
},
|
||||
inject: [ConfigService],
|
||||
} as Provider;
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { Redis } from 'ioredis';
|
||||
export const REDIS_CLIENT = 'REDIS_CLIENT';
|
||||
export type RedisClient = Redis;
|
||||
@@ -17,6 +17,10 @@ export class CreateRestaurantDto {
|
||||
@IsString()
|
||||
logo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
banner?: string;
|
||||
|
||||
@IsOptional()
|
||||
template?: string;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ export class RestaurantDto {
|
||||
slug: string;
|
||||
description?: string;
|
||||
logo?: string;
|
||||
banner?: string;
|
||||
template?: string;
|
||||
categories?: CategoryDto[];
|
||||
createdAt?: Date;
|
||||
|
||||
@@ -16,6 +18,8 @@ export class RestaurantDto {
|
||||
slug: entity.slug,
|
||||
description: entity.description ?? undefined,
|
||||
logo: entity.logo ?? undefined,
|
||||
banner: entity.banner ?? undefined,
|
||||
template: entity.template ?? undefined,
|
||||
categories: entity.categories?.map(CategoryDto.fromEntity) ?? [],
|
||||
createdAt: entity.createdAt ? new Date(entity.createdAt) : undefined,
|
||||
};
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { RestaurantService } from './restaurant.service';
|
||||
import { RestaurantController } from './restaurant.controller';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { CacheModule } from 'src/cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, CacheModule],
|
||||
providers: [RestaurantService],
|
||||
controllers: [RestaurantController],
|
||||
exports: [RestaurantService],
|
||||
|
||||
@@ -7,10 +7,15 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { CreateRestaurantDto } from './dto/create-restaurant.dto';
|
||||
import { UpdateRestaurantDto } from './dto/update-restaurant.dto';
|
||||
import { RestaurantDto } from './dto/restaurant.dto';
|
||||
import { CacheService } from 'src/cache/cache.service';
|
||||
import { keys } from 'src/cache/cache.keys';
|
||||
|
||||
@Injectable()
|
||||
export class RestaurantService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private cache: CacheService,
|
||||
) {}
|
||||
|
||||
async verifyOwnership(userId: string, restaurantId: string) {
|
||||
const owned = await this.prisma.restaurant.findFirst({
|
||||
@@ -29,20 +34,29 @@ export class RestaurantService {
|
||||
slug: dto.slug,
|
||||
description: dto.description,
|
||||
logo: dto.logo,
|
||||
banner: dto.banner,
|
||||
ownerId,
|
||||
template: dto.template ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
await this.cache.del(keys.userRestaurants(ownerId));
|
||||
|
||||
return RestaurantDto.fromEntity(restaurant);
|
||||
}
|
||||
|
||||
async findByOwner(ownerId: string) {
|
||||
const cacheKey = keys.userRestaurants(ownerId);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const restaurants = await this.prisma.restaurant.findMany({
|
||||
where: { ownerId },
|
||||
});
|
||||
|
||||
return restaurants.map(RestaurantDto.fromEntity);
|
||||
const result = restaurants.map(RestaurantDto.fromEntity);
|
||||
await this.cache.set(cacheKey, result, 300);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findAll() {
|
||||
@@ -51,6 +65,10 @@ export class RestaurantService {
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const cacheKey = keys.restaurantById(id);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const restaurant = await this.prisma.restaurant.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
@@ -59,7 +77,9 @@ export class RestaurantService {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
}
|
||||
|
||||
return RestaurantDto.fromEntity(restaurant);
|
||||
const result = RestaurantDto.fromEntity(restaurant);
|
||||
await this.cache.set(cacheKey, result, 600);
|
||||
return result;
|
||||
}
|
||||
|
||||
async findMyRestaurant(id: string, userId: string) {
|
||||
@@ -68,6 +88,10 @@ export class RestaurantService {
|
||||
}
|
||||
|
||||
async findBySlug(slug: string) {
|
||||
const cacheKey = keys.restaurantBySlug(slug);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const restaurant = await this.prisma.restaurant.findUnique({
|
||||
where: { slug },
|
||||
include: {
|
||||
@@ -82,6 +106,8 @@ export class RestaurantService {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
}
|
||||
|
||||
await this.cache.set(cacheKey, restaurant);
|
||||
|
||||
return RestaurantDto.fromEntity(restaurant);
|
||||
}
|
||||
|
||||
@@ -99,11 +125,27 @@ export class RestaurantService {
|
||||
},
|
||||
});
|
||||
|
||||
// Invalidate cache by slug and by id
|
||||
await this.cache.del(keys.restaurantBySlug(updated.slug));
|
||||
await this.cache.del(keys.restaurantById(updated.id));
|
||||
await this.cache.del(keys.restaurantCategories(updated.id));
|
||||
await this.cache.del(keys.restaurantItems(updated.id));
|
||||
await this.cache.del(keys.userRestaurants(userId));
|
||||
|
||||
return RestaurantDto.fromEntity(updated);
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string) {
|
||||
await this.verifyOwnership(userId, id);
|
||||
return this.prisma.restaurant.delete({ where: { id } });
|
||||
const deleted = await this.prisma.restaurant.delete({ where: { id } });
|
||||
|
||||
// invalidate
|
||||
await this.cache.del(keys.restaurantBySlug(deleted.slug));
|
||||
await this.cache.del(keys.restaurantById(deleted.id));
|
||||
await this.cache.del(keys.restaurantCategories(deleted.id));
|
||||
await this.cache.del(keys.restaurantItems(deleted.id));
|
||||
await this.cache.del(keys.userRestaurants(userId));
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,5 +47,8 @@ export const config = {
|
||||
user: process.env.EMAIL_USER,
|
||||
password: process.env.EMAIL_PASSWORD,
|
||||
from: process.env.EMAIL_FROM || '"Zemenu" <no-reply@zemenu.com>',
|
||||
contactNotificationEmails: process.env.CONTACT_NOTIFICATION_EMAILS
|
||||
? process.env.CONTACT_NOTIFICATION_EMAILS.split(',').map(email => email.trim())
|
||||
: [process.env.EMAIL_FROM || 'no-reply@zemenu.com'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -52,6 +52,21 @@ export class EmailService {
|
||||
await this.sendEmail(to, 'Reset your password', html);
|
||||
}
|
||||
|
||||
async sendContactMessageNotification(data: { name: string; email: string; subject?: string; message: string }) {
|
||||
const content = `
|
||||
<p>You have received a new contact message from <strong>${data.name}</strong> (${data.email}).</p>
|
||||
<p><strong>Subject:</strong> ${data.subject || 'No Subject'}</p>
|
||||
<hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;">
|
||||
<p style="white-space: pre-wrap;">${data.message}</p>
|
||||
`;
|
||||
|
||||
const html = this.getHtmlTemplate('New Contact Message', content);
|
||||
|
||||
// Send to the configured notification emails
|
||||
const recipients = config.email.contactNotificationEmails;
|
||||
await this.sendEmail(recipients.join(','), `New Contact Message: ${data.subject || 'No Subject'}`, html);
|
||||
}
|
||||
|
||||
private getHtmlTemplate(title: string, content: string): string {
|
||||
const logoUrl = 'https://res.cloudinary.com/dmxoohiwo/image/upload/v1764315105/logo_fpczsd.png';
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { CacheModule } from 'src/cache/cache.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, CacheModule],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
})
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { CacheService } from 'src/cache/cache.service';
|
||||
import { keys } from 'src/cache/cache.keys';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly cache: CacheService,
|
||||
) {}
|
||||
|
||||
async getAllUsers() {
|
||||
return this.prisma.user.findMany({
|
||||
@@ -12,11 +17,17 @@ export class UserService {
|
||||
}
|
||||
|
||||
async getUserById(id: string) {
|
||||
const cacheKey = keys.user(id);
|
||||
const cached = await this.cache.get(cacheKey);
|
||||
if (cached) return cached;
|
||||
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: { accounts: true, sessions: true, restaurants: true },
|
||||
});
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
|
||||
await this.cache.set(cacheKey, user, 600);
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -25,7 +36,14 @@ export class UserService {
|
||||
await this.prisma.session.deleteMany({ where: { userId: id } });
|
||||
await this.prisma.account.deleteMany({ where: { userId: id } });
|
||||
await this.prisma.restaurant.deleteMany({ where: { ownerId: id } });
|
||||
return this.prisma.user.delete({ where: { id } });
|
||||
|
||||
const deleted = await this.prisma.user.delete({ where: { id } });
|
||||
|
||||
// Invalidate caches
|
||||
await this.cache.del(keys.user(id));
|
||||
await this.cache.del(keys.userRestaurants(id));
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
async getSessionsByUserId(id: string) {
|
||||
|
||||
Reference in New Issue
Block a user