add cache interceptor for hot endpoints
deploy to danak / build_and_deploy (push) Has been cancelled
deploy to danak / build_and_deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
/** Default TTL (seconds) for public read endpoints */
|
||||
export const PUBLIC_CACHE_TTL = 300;
|
||||
|
||||
export const CacheKeyPrefixes = {
|
||||
FOODS_BY_RESTAURANT: 'public:foods:restaurant',
|
||||
CATEGORIES_BY_RESTAURANT: 'public:categories:restaurant',
|
||||
RESTAURANT_SPEC: 'public:restaurants',
|
||||
} as const;
|
||||
|
||||
export const CacheKeys = {
|
||||
foodsByRestaurant: (slug: string) => `${CacheKeyPrefixes.FOODS_BY_RESTAURANT}:${slug}`,
|
||||
categoriesByRestaurant: (slug: string) => `${CacheKeyPrefixes.CATEGORIES_BY_RESTAURANT}:${slug}`,
|
||||
restaurantSpec: (slug: string) => `${CacheKeyPrefixes.RESTAURANT_SPEC}:${slug}`,
|
||||
} as const;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { SetMetadata, UseInterceptors, applyDecorators } from '@nestjs/common';
|
||||
import { HttpCacheInterceptor } from 'src/core/interceptors/http-cache.interceptor';
|
||||
import { PUBLIC_CACHE_TTL } from '../constants/cache-keys.constant';
|
||||
|
||||
export const CACHE_RESPONSE_KEY = 'cache_response';
|
||||
|
||||
export interface CacheResponseOptions {
|
||||
/** Cache key prefix; route params are appended (e.g. `public:foods:restaurant` + `:slug`) */
|
||||
keyPrefix: string;
|
||||
/** TTL in seconds */
|
||||
ttl?: number;
|
||||
/** Route param names to include in the cache key (defaults to all route params) */
|
||||
params?: string[];
|
||||
}
|
||||
|
||||
export const CacheResponse = (options: CacheResponseOptions) =>
|
||||
applyDecorators(
|
||||
SetMetadata(CACHE_RESPONSE_KEY, { ttl: PUBLIC_CACHE_TTL, ...options }),
|
||||
UseInterceptors(HttpCacheInterceptor),
|
||||
);
|
||||
@@ -0,0 +1,63 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable, from, of } from 'rxjs';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
import {
|
||||
CACHE_RESPONSE_KEY,
|
||||
CacheResponseOptions,
|
||||
} from 'src/common/decorators/cache-response.decorator';
|
||||
import { PUBLIC_CACHE_TTL } from 'src/common/constants/cache-keys.constant';
|
||||
import { CacheService } from 'src/modules/utils/cache.service';
|
||||
|
||||
@Injectable()
|
||||
export class HttpCacheInterceptor implements NestInterceptor {
|
||||
constructor(
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly reflector: Reflector,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const options = this.reflector.get<CacheResponseOptions>(
|
||||
CACHE_RESPONSE_KEY,
|
||||
context.getHandler(),
|
||||
);
|
||||
if (!options) {
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest<{ params: Record<string, string> }>();
|
||||
const key = this.buildCacheKey(options, request.params);
|
||||
const ttl =
|
||||
options.ttl ??
|
||||
this.configService.get<number>('PUBLIC_CACHE_TTL', PUBLIC_CACHE_TTL);
|
||||
|
||||
return from(this.cacheService.get<unknown>(key)).pipe(
|
||||
mergeMap(cached => {
|
||||
if (cached !== undefined && cached !== null) {
|
||||
return of(cached);
|
||||
}
|
||||
|
||||
return next.handle().pipe(
|
||||
mergeMap(async data => {
|
||||
const serialized = JSON.parse(JSON.stringify(data));
|
||||
await this.cacheService.set(key, serialized, ttl);
|
||||
return data;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private buildCacheKey(
|
||||
options: CacheResponseOptions,
|
||||
params: Record<string, string>,
|
||||
): string {
|
||||
const paramValues = options.params?.length
|
||||
? options.params.map(name => params[name]).filter(Boolean)
|
||||
: Object.values(params);
|
||||
|
||||
return [options.keyPrefix, ...paramValues].join(':');
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ import { RestId } from 'src/common/decorators';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
import { CacheResponse } from 'src/common/decorators/cache-response.decorator';
|
||||
import { CacheKeyPrefixes } from 'src/common/constants/cache-keys.constant';
|
||||
|
||||
@ApiTags('category')
|
||||
@Controller()
|
||||
@@ -26,6 +28,7 @@ export class CategoryController {
|
||||
constructor(private readonly categoryService: CategoryService) { }
|
||||
|
||||
@Get('public/categories/restaurant/:slug')
|
||||
@CacheResponse({ keyPrefix: CacheKeyPrefixes.CATEGORIES_BY_RESTAURANT, params: ['slug'] })
|
||||
@ApiOperation({ summary: 'Get all categories by restaurant slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
|
||||
|
||||
@@ -19,6 +19,8 @@ import { OptionalAuthGuard } from 'src/modules/auth/guards/optinalAuth.guard';
|
||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { CacheResponse } from 'src/common/decorators/cache-response.decorator';
|
||||
import { CacheKeyPrefixes } from 'src/common/constants/cache-keys.constant';
|
||||
|
||||
@ApiTags('foods')
|
||||
@Controller()
|
||||
@@ -26,6 +28,7 @@ export class FoodController {
|
||||
constructor(private readonly foodsService: FoodService) { }
|
||||
|
||||
@Get('public/foods/restaurant/:slug')
|
||||
@CacheResponse({ keyPrefix: CacheKeyPrefixes.FOODS_BY_RESTAURANT, params: ['slug'] })
|
||||
@ApiOperation({ summary: 'Get all foods by restaurant slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', required: true, description: 'Restaurant Slug' })
|
||||
|
||||
@@ -8,6 +8,8 @@ import { Category } from '../entities/category.entity';
|
||||
import { CategoryMessage, RestMessage } from 'src/common/enums/message.enum';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service';
|
||||
import { CacheService } from 'src/modules/utils/cache.service';
|
||||
import { CacheKeys } from 'src/common/constants/cache-keys.constant';
|
||||
|
||||
@Injectable()
|
||||
export class CategoryService {
|
||||
@@ -15,6 +17,7 @@ export class CategoryService {
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly restaurantsService: RestaurantsService,
|
||||
private readonly em: EntityManager,
|
||||
private readonly cacheService: CacheService,
|
||||
) { }
|
||||
|
||||
async create(restId: string, dto: CreateCategoryDto): Promise<Category> {
|
||||
@@ -31,6 +34,7 @@ export class CategoryService {
|
||||
|
||||
const category = this.categoryRepository.create(data);
|
||||
await this.em.persistAndFlush(category);
|
||||
await this.invalidateCategoriesCache(restaurant.slug);
|
||||
return category;
|
||||
}
|
||||
|
||||
@@ -58,6 +62,8 @@ export class CategoryService {
|
||||
const cat = await this.findOneOrFail(restId, id)
|
||||
this.em.assign(cat, dto);
|
||||
await this.em.persistAndFlush(cat);
|
||||
const restaurant = await this.restaurantsService.findOneOrFail(restId);
|
||||
await this.invalidateCategoriesCache(restaurant.slug);
|
||||
return cat;
|
||||
}
|
||||
|
||||
@@ -65,5 +71,12 @@ export class CategoryService {
|
||||
const cat = await this.findOneOrFail(restId, id)
|
||||
cat.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(cat);
|
||||
const restaurant = await this.restaurantsService.findOneOrFail(restId);
|
||||
await this.invalidateCategoriesCache(restaurant.slug);
|
||||
}
|
||||
|
||||
private async invalidateCategoriesCache(slug?: string): Promise<void> {
|
||||
if (!slug) return;
|
||||
await this.cacheService.del(CacheKeys.categoriesByRestaurant(slug));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import { MealType } from '../interface/food.interface';
|
||||
import { Inventory } from 'src/modules/inventory/entities/inventory.entity';
|
||||
import { RestaurantsService } from 'src/modules/restaurants/providers/restaurants.service';
|
||||
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
|
||||
import { CacheService } from 'src/modules/utils/cache.service';
|
||||
import { CacheKeys } from 'src/common/constants/cache-keys.constant';
|
||||
|
||||
@Injectable()
|
||||
export class FoodService {
|
||||
@@ -20,6 +22,7 @@ export class FoodService {
|
||||
private readonly categoryRepository: CategoryRepository,
|
||||
private readonly restService: RestaurantsService,
|
||||
private readonly em: EntityManager,
|
||||
private readonly cacheService: CacheService,
|
||||
) { }
|
||||
|
||||
async create(restId: string, createFoodDto: CreateFoodDto) {
|
||||
@@ -72,6 +75,8 @@ export class FoodService {
|
||||
: null;
|
||||
const savedInventory = inventory?.id ? await this.em.findOne(Inventory, { id: inventory.id }) : inventory;
|
||||
|
||||
await this.invalidateRestaurantFoodsCache(restaurant.slug);
|
||||
|
||||
return { food: savedFood ?? food, inventory: savedInventory ?? inventory };
|
||||
}
|
||||
|
||||
@@ -233,8 +238,7 @@ export class FoodService {
|
||||
// Re-load the food to ensure populated relations and stable instance
|
||||
const savedFood = await this.foodRepository.findOne({ id: food.id }, { populate: ['category', 'restaurant'] });
|
||||
|
||||
// Invalidate cache for the restaurant if present (optional)
|
||||
// await this.invalidateRestaurantFoodsCache(savedFood?.restaurant?.slug);
|
||||
await this.invalidateRestaurantFoodsCache(savedFood?.restaurant?.slug ?? food.restaurant?.slug);
|
||||
|
||||
return savedFood ?? food;
|
||||
}
|
||||
@@ -245,12 +249,16 @@ export class FoodService {
|
||||
throw new NotFoundException(FoodMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
// const restaurantSlug = food.restaurant.slug;
|
||||
const restaurantSlug = food.restaurant.slug;
|
||||
food.deletedAt = new Date();
|
||||
await this.em.persistAndFlush(food);
|
||||
|
||||
// Invalidate cache for the restaurant
|
||||
// await this.invalidateRestaurantFoodsCache(restaurantSlug);
|
||||
await this.invalidateRestaurantFoodsCache(restaurantSlug);
|
||||
}
|
||||
|
||||
private async invalidateRestaurantFoodsCache(slug?: string): Promise<void> {
|
||||
if (!slug) return;
|
||||
await this.cacheService.del(CacheKeys.foodsByRestaurant(slug));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Processor, WorkerHost, OnWorkerEvent } from '@nestjs/bullmq';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { Job } from 'bullmq';
|
||||
import { CreateRequestContext } from '@mikro-orm/core';
|
||||
import { MikroORM } from '@mikro-orm/postgresql';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { UserRepository } from 'src/modules/users/repositories/user.repository';
|
||||
import { SmsNotificationQueueJob } from '../interfaces/jobs-queue.interface';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
@@ -20,7 +20,7 @@ export class SmsProcessor extends WorkerHost {
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly smsService: SmsService,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly orm: MikroORM,
|
||||
private readonly em: EntityManager,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { EnsureRequestContext } from '@mikro-orm/core';
|
||||
import { MikroORM } from '@mikro-orm/postgresql';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { OrderCreatedEvent, OrderStatusChangedEvent } from '../events/order.events';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
@@ -26,7 +26,7 @@ export class OrderListeners {
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly userService: UserService,
|
||||
private readonly orm: MikroORM,
|
||||
private readonly em: EntityManager,
|
||||
) {
|
||||
this.orderCreatedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_CREATED') ?? '123';
|
||||
this.orderStatusChangedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_STATUS_CHANGE') ?? '123';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { EnsureRequestContext } from '@mikro-orm/core';
|
||||
import { MikroORM } from '@mikro-orm/postgresql';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { PagerCreatedEvent } from '../events/pager.events';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
@@ -17,7 +17,7 @@ export class PagerListeners {
|
||||
private readonly adminService: AdminRepository,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly orm: MikroORM,
|
||||
private readonly em: EntityManager,
|
||||
) {
|
||||
this.smsPatternPagerCreated = this.configService.get<string>('SMS_PATTERN_PAGER_CREATED') ?? '123';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { EnsureRequestContext } from '@mikro-orm/core';
|
||||
import { MikroORM } from '@mikro-orm/postgresql';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
@@ -20,7 +20,7 @@ export class PaymentListeners {
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly orderService: OrdersService,
|
||||
private readonly orm: MikroORM,
|
||||
private readonly em: EntityManager,
|
||||
) {
|
||||
this.orderCreatedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ORDER_CREATED') ?? '123';
|
||||
this.paymentSucceedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_ONLINE_PAYMENT_SUCCEED') ?? '123';
|
||||
|
||||
@@ -19,6 +19,8 @@ import { API_HEADER_SLUG } from 'src/common/constants';
|
||||
import { SuperAdminAuthGuard } from 'src/modules/auth/guards/superAdminAuth.guard';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { CacheResponse } from 'src/common/decorators/cache-response.decorator';
|
||||
import { CacheKeyPrefixes } from 'src/common/constants/cache-keys.constant';
|
||||
|
||||
@ApiTags('restaurants')
|
||||
@Controller()
|
||||
@@ -26,6 +28,7 @@ export class RestaurantsController {
|
||||
constructor(private readonly restaurantsService: RestaurantsService) { }
|
||||
|
||||
@Get('public/restaurants/:slug')
|
||||
@CacheResponse({ keyPrefix: CacheKeyPrefixes.RESTAURANT_SPEC, params: ['slug'] })
|
||||
@ApiOperation({ summary: 'Get restaurant specification by slug' })
|
||||
@ApiHeader(API_HEADER_SLUG)
|
||||
@ApiParam({ name: 'slug', required: true, description: 'Restaurant slug' })
|
||||
|
||||
@@ -27,6 +27,8 @@ import { Schedule } from '../entities/schedule.entity';
|
||||
import { WalletTransaction } from '../../users/entities/wallet-transaction.entity';
|
||||
import { SmsLog } from '../../notifications/entities/smsLogs.entity';
|
||||
import { Notification } from '../../notifications/entities/notification.entity';
|
||||
import { CacheService } from 'src/modules/utils/cache.service';
|
||||
import { CacheKeys } from 'src/common/constants/cache-keys.constant';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@@ -34,6 +36,7 @@ export class RestaurantsService {
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly restRepository: RestRepository,
|
||||
private readonly cacheService: CacheService,
|
||||
) { }
|
||||
|
||||
async setupRestuarant(dto: CreateRestaurantDto): Promise<Restaurant> {
|
||||
@@ -184,13 +187,28 @@ export class RestaurantsService {
|
||||
|
||||
}
|
||||
|
||||
const previousSlug = restaurant.slug;
|
||||
this.restRepository.assign(restaurant, dto);
|
||||
|
||||
await this.em.persistAndFlush(restaurant);
|
||||
|
||||
await this.invalidateRestaurantCache(previousSlug);
|
||||
if (restaurant.slug !== previousSlug) {
|
||||
await this.invalidateRestaurantCache(restaurant.slug);
|
||||
}
|
||||
|
||||
return restaurant;
|
||||
}
|
||||
|
||||
private async invalidateRestaurantCache(slug?: string): Promise<void> {
|
||||
if (!slug) return;
|
||||
await Promise.all([
|
||||
this.cacheService.del(CacheKeys.restaurantSpec(slug)),
|
||||
this.cacheService.del(CacheKeys.foodsByRestaurant(slug)),
|
||||
this.cacheService.del(CacheKeys.categoriesByRestaurant(slug)),
|
||||
]);
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
// Soft delete the restaurant by setting isActive to false
|
||||
const restaurant = await this.restRepository.findOne({ id });
|
||||
|
||||
@@ -11,11 +11,12 @@ import { ScheduleController } from './controllers/schedule.controller';
|
||||
import { RestaurantCrone } from './crone/restaurant.crone';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
|
||||
@Module({
|
||||
controllers: [RestaurantsController, ScheduleController],
|
||||
providers: [RestaurantsService, RestRepository, ScheduleRepository, ScheduleService, RestaurantCrone],
|
||||
imports: [MikroOrmModule.forFeature([Restaurant, Schedule]), JwtModule, forwardRef(() => AuthModule)],
|
||||
imports: [MikroOrmModule.forFeature([Restaurant, Schedule]), JwtModule, forwardRef(() => AuthModule), UtilsModule],
|
||||
exports: [RestRepository, ScheduleRepository, ScheduleService, RestaurantsService],
|
||||
})
|
||||
export class RestaurantsModule { }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { EnsureRequestContext } from '@mikro-orm/core';
|
||||
import { MikroORM } from '@mikro-orm/postgresql';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { ReviewCreatedEvent } from '../events/review.events';
|
||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
@@ -18,7 +18,7 @@ export class ReviewListeners {
|
||||
private readonly adminService: AdminRepository,
|
||||
private readonly notificationService: NotificationService,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly orm: MikroORM,
|
||||
private readonly em: EntityManager,
|
||||
) {
|
||||
this.reviewCreatedSmsTemplateId = this.configService.get<string>('SMS_PATTERN_REVIEW_CREATED') ?? '123';
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpCacheInterceptor } from 'src/core/interceptors/http-cache.interceptor';
|
||||
import { CacheService } from './cache.service';
|
||||
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [],
|
||||
providers: [CacheService, ],
|
||||
exports: [CacheService],
|
||||
providers: [CacheService, HttpCacheInterceptor],
|
||||
exports: [CacheService, HttpCacheInterceptor],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
|
||||
Reference in New Issue
Block a user