feat: Implement menu item and category management with Redis caching for restaurant data.
This commit is contained in:
@@ -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"
|
||||
},
|
||||
|
||||
@@ -7,6 +7,8 @@ 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';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -17,6 +19,8 @@ import { FileModule } from './file/file.module';
|
||||
RestaurantModule,
|
||||
ItemModule,
|
||||
FileModule,
|
||||
RedisModule,
|
||||
CacheModule,
|
||||
],
|
||||
})
|
||||
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
+7
@@ -0,0 +1,7 @@
|
||||
export const keys = {
|
||||
restaurantBySlug: (slug: string) => `restaurant:slug:${slug}`,
|
||||
restaurantCategories: (restaurantId: string) => `restaurant:${restaurantId}:categories`,
|
||||
restaurantItems: (restaurantId: string) => `restaurant:${restaurantId}:items`,
|
||||
category: (categoryId: string) => `category:${categoryId}`,
|
||||
item: (itemId: string) => `item:${itemId}`,
|
||||
};
|
||||
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,36 @@ 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 category = await this.prisma.category.findFirstOrThrow({
|
||||
where: { id: categoryId },
|
||||
include: { items: true, restaurant: true },
|
||||
}).catch(() => {
|
||||
throw new NotFoundException('Category not found');
|
||||
});
|
||||
|
||||
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,6 +43,9 @@ export class CategoryService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.cache.del(keys.restaurantBySlug(restaurant.slug));
|
||||
await this.cache.del(keys.restaurantCategories(restaurant.id));
|
||||
|
||||
return CategoryDto.fromEntity(category);
|
||||
}
|
||||
|
||||
@@ -38,20 +61,13 @@ export class CategoryService {
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const category = await this.prisma.category.findUnique({
|
||||
where: { id },
|
||||
include: { items: true },
|
||||
});
|
||||
|
||||
if (!category) throw new NotFoundException('Category not found');
|
||||
|
||||
|
||||
const category = await this._getCategorie(id);
|
||||
|
||||
return CategoryDto.fromEntity(category);
|
||||
}
|
||||
|
||||
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 +75,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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
|
||||
@@ -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,6 +32,11 @@ export class ItemService {
|
||||
},
|
||||
});
|
||||
|
||||
if (category.restaurant) {
|
||||
await this.cache.del(keys.restaurantBySlug(category.restaurant.slug));
|
||||
}
|
||||
await this.cache.del(keys.restaurantCategories(category.restaurantId));
|
||||
|
||||
return MenuItemDto.fromEntity(item);
|
||||
}
|
||||
|
||||
@@ -62,6 +70,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 +81,32 @@ 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));
|
||||
|
||||
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));
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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({
|
||||
@@ -69,6 +74,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: {
|
||||
@@ -83,6 +92,8 @@ export class RestaurantService {
|
||||
throw new NotFoundException('Restaurant not found');
|
||||
}
|
||||
|
||||
await this.cache.set(cacheKey, restaurant);
|
||||
|
||||
return RestaurantDto.fromEntity(restaurant);
|
||||
}
|
||||
|
||||
@@ -100,11 +111,23 @@ export class RestaurantService {
|
||||
},
|
||||
});
|
||||
|
||||
// Invalidate cache by slug and by id
|
||||
await this.cache.del(keys.restaurantBySlug(updated.slug));
|
||||
await this.cache.del(keys.restaurantCategories(updated.id));
|
||||
await this.cache.del(keys.restaurantItems(updated.id));
|
||||
|
||||
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.restaurantCategories(deleted.id));
|
||||
await this.cache.del(keys.restaurantItems(deleted.id));
|
||||
|
||||
return deleted;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user