Forráskód Böngészése

feat: add CacheSyncAction table and Prisma model for Redis cache synchronization

- Created a new migration file to define the CacheSyncAction table in MySQL with relevant fields and indexes.
- Added a corresponding Prisma model for CacheSyncAction, including fields for entity type, operation, status, attempts, and timestamps.
- Updated project structure by removing obsolete files and directories related to previous implementations.
Dave 4 hónapja
szülő
commit
84c83b12af

+ 6 - 0
.env.mgnt.dev

@@ -11,6 +11,12 @@ MYSQL_URL="mysql://root:123456@localhost:3306/box_admin"
 MONGO_URL="mongodb://admin:ZXcv%21%21996@localhost:27017/box_admin?authSource=admin"
 # MONGO_URL="mongodb://msAdmin:Fl1%2A29MJe%26jLvj@localhost:27017/box_admin?authSource=admin"
 
+# Redis Config
+REDIS_HOST=127.0.0.1
+REDIS_PORT=6379
+REDIS_PASSWORD=
+REDIS_DB=0
+REDIS_KEY_PREFIX=box:
 
 # App set to 0.0.0.0 for local LAN access
 APP_HOST=0.0.0.0

+ 4 - 0
apps/box-mgnt-api/src/app.module.ts

@@ -12,6 +12,8 @@ import { validateEnvironment } from './config/env.validation';
 import { LoggerModule } from 'nestjs-pino';
 import { MgntBackendModule } from './mgnt-backend/mgnt-backend.module';
 import pinoConfig from '@box/common/config/pino.config'; // adjust if your export name differs
+import { RedisModule } from './redis/redis.module';
+import { CacheSyncModule } from './cache-sync/cache-sync.module';
 
 @Module({
   imports: [
@@ -22,6 +24,8 @@ import pinoConfig from '@box/common/config/pino.config'; // adjust if your expor
       validate: validateEnvironment,
     }),
     ConfigModule.forFeature(appConfigFactory),
+    RedisModule.forRoot(),
+    CacheSyncModule,
 
     CommonModule,
 

+ 76 - 0
apps/box-mgnt-api/src/cache-sync/cache-sync-debug.controller.ts

@@ -0,0 +1,76 @@
+import {
+  Body,
+  Controller,
+  DefaultValuePipe,
+  Logger,
+  ParseIntPipe,
+  Post,
+  Query,
+} from '@nestjs/common';
+import { CacheSyncService } from './cache-sync.service';
+
+@Controller('mgnt-debug/cache-sync')
+export class CacheSyncDebugController {
+  private readonly logger = new Logger(CacheSyncDebugController.name);
+
+  constructor(private readonly cacheSyncService: CacheSyncService) {}
+
+  /**
+   * POST /mgnt-debug/cache-sync/channel/refresh-all
+   * Schedules a CHANNEL + REFRESH_ALL action.
+   */
+  @Post('channel/refresh-all')
+  async scheduleChannelRefreshAll() {
+    await this.cacheSyncService.scheduleChannelRefreshAll();
+    this.logger.log('Scheduled CHANNEL REFRESH_ALL');
+    return { ok: true, message: 'Scheduled CHANNEL REFRESH_ALL' };
+  }
+
+  /**
+   * POST /mgnt-debug/cache-sync/category/refresh-all
+   * Schedules a CATEGORY + REFRESH_ALL action.
+   */
+  @Post('category/refresh-all')
+  async scheduleCategoryRefreshAll() {
+    await this.cacheSyncService.scheduleCategoryRefreshAll();
+    this.logger.log('Scheduled CATEGORY REFRESH_ALL');
+    return { ok: true, message: 'Scheduled CATEGORY REFRESH_ALL' };
+  }
+
+  /**
+   * POST /mgnt-debug/cache-sync/ad/refresh
+   * Body: { adId: number; adType?: string }
+   * Schedules AD REFRESH (+ optional AD_POOL REBUILD_POOL if adType provided).
+   */
+  @Post('ad/refresh')
+  async scheduleAdRefresh(
+    @Body('adId', ParseIntPipe) adId: number,
+    @Body('adType') adType?: string,
+  ) {
+    await this.cacheSyncService.scheduleAdRefresh(adId, adType);
+    this.logger.log(
+      `Scheduled AD REFRESH for adId=${adId}, adType=${adType ?? 'N/A'}`,
+    );
+    return {
+      ok: true,
+      message: 'Scheduled AD REFRESH (and pool rebuild if adType provided)',
+      adId,
+      adType: adType ?? null,
+    };
+  }
+
+  /**
+   * POST /mgnt-debug/cache-sync/process-once?limit=20
+   * Manually processes pending actions (single batch).
+   */
+  @Post('process-once')
+  async processOnce(
+    @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number,
+  ) {
+    await this.cacheSyncService.processPendingOnce(limit);
+    return {
+      ok: true,
+      message: `Processed up to ${limit} pending actions (see logs).`,
+    };
+  }
+}

+ 17 - 0
apps/box-mgnt-api/src/cache-sync/cache-sync.module.ts

@@ -0,0 +1,17 @@
+import { Module } from '@nestjs/common';
+// ⬇️ Adjust this to your actual DB module that provides PrismaService
+import { SharedModule } from '@box/db/shared.module'; // TODO: change to your real module
+import { RedisModule } from '../redis/redis.module';
+import { CacheSyncService } from './cache-sync.service';
+import { CacheSyncDebugController } from './cache-sync-debug.controller';
+
+@Module({
+  imports: [
+    SharedModule, // from libs/db/src/shared.module.ts (most likely)
+    RedisModule.forRoot(),
+  ],
+  providers: [CacheSyncService],
+  controllers: [CacheSyncDebugController],
+  exports: [CacheSyncService],
+})
+export class CacheSyncModule {}

+ 209 - 0
apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts

@@ -0,0 +1,209 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { Prisma } from '@prisma/mysql/client'; // adjust if your generated types are in a different package
+import { RedisService } from '../redis/redis.service';
+// ⬇️ Adjust this import to your actual db lib (e.g., '@box/db')
+import { MysqlPrismaService } from '@box/db/prisma/mysql-prisma.service'; // TODO: change to your real path
+
+import {
+  CacheEntityType,
+  CacheOperation,
+  CacheStatus,
+  CachePayload,
+} from './cache-sync.types';
+
+@Injectable()
+export class CacheSyncService {
+  private readonly logger = new Logger(CacheSyncService.name);
+
+  constructor(
+    private readonly prisma: MysqlPrismaService,
+    private readonly redis: RedisService,
+  ) {}
+
+  // Utility to get "now" as BigInt epoch millis
+  private nowBigInt(): bigint {
+    return BigInt(Date.now());
+  }
+
+  /**
+   * Core generic scheduler.
+   * Domain code should call this via convenience helpers,
+   * but it's reusable for any entityType/operation.
+   */
+  async scheduleAction(params: {
+    entityType: CacheEntityType;
+    operation: CacheOperation;
+    entityId?: bigint | number | null;
+    payload?: CachePayload | Prisma.JsonValue | null;
+    delayMs?: number; // optional backoff for first attempt
+  }): Promise<void> {
+    const { entityType, operation, entityId, payload, delayMs } = params;
+    const now = this.nowBigInt();
+    const nextAttemptAt =
+      delayMs && delayMs > 0
+        ? now + BigInt(delayMs) // schedule in the future
+        : now;
+
+    await this.prisma.cacheSyncAction.create({
+      data: {
+        entityType,
+        operation,
+        entityId: entityId != null ? BigInt(entityId) : null,
+        status: CacheStatus.PENDING,
+        attempts: 0,
+        nextAttemptAt,
+        payload: (payload ?? null) as Prisma.JsonValue,
+        createdAt: now,
+        updatedAt: now,
+      },
+    });
+
+    this.logger.debug(
+      `Scheduled CacheSyncAction: entityType=${entityType}, operation=${operation}, entityId=${entityId ?? 'null'}`,
+    );
+  }
+
+  // Convenience helpers — you can add more as needed
+
+  async scheduleChannelRefreshAll(): Promise<void> {
+    await this.scheduleAction({
+      entityType: CacheEntityType.CHANNEL,
+      operation: CacheOperation.REFRESH_ALL,
+    });
+  }
+
+  async scheduleCategoryRefreshAll(): Promise<void> {
+    await this.scheduleAction({
+      entityType: CacheEntityType.CATEGORY,
+      operation: CacheOperation.REFRESH_ALL,
+    });
+  }
+
+  async scheduleAdRefresh(
+    adId: number | bigint,
+    adType?: string,
+  ): Promise<void> {
+    await this.scheduleAction({
+      entityType: CacheEntityType.AD,
+      operation: CacheOperation.REFRESH,
+      entityId: adId,
+      payload: adType ? { type: adType } : undefined,
+    });
+
+    if (adType) {
+      await this.scheduleAction({
+        entityType: CacheEntityType.AD_POOL,
+        operation: CacheOperation.REBUILD_POOL,
+        payload: { type: adType },
+      });
+    }
+  }
+
+  /**
+   * Minimal processing loop (single batch).
+   * Later you can move this into a @Cron job using @nestjs/schedule.
+   */
+  async processPendingOnce(limit = 20): Promise<void> {
+    const now = this.nowBigInt();
+
+    const actions = await this.prisma.cacheSyncAction.findMany({
+      where: {
+        status: CacheStatus.PENDING,
+        nextAttemptAt: {
+          lte: now,
+        },
+      },
+      orderBy: { id: 'asc' },
+      take: limit,
+    });
+
+    if (!actions.length) {
+      this.logger.debug('No pending CacheSyncAction to process.');
+      return;
+    }
+
+    this.logger.log(`Processing ${actions.length} pending CacheSyncAction(s).`);
+
+    for (const action of actions) {
+      try {
+        await this.handleSingleAction(action);
+      } catch (err: any) {
+        this.logger.error(
+          `Error processing CacheSyncAction id=${action.id}: ${err?.message ?? err}`,
+        );
+
+        const attempts = action.attempts + 1;
+        const maxAttempts = 5;
+        const backoffMs = Math.min(60000, 5000 * attempts); // simple backoff up to 60s
+
+        await this.prisma.cacheSyncAction.update({
+          where: { id: action.id },
+          data: {
+            status:
+              attempts >= maxAttempts
+                ? CacheStatus.GAVE_UP
+                : CacheStatus.PENDING,
+            attempts,
+            lastError: err?.message ?? String(err),
+            nextAttemptAt: this.nowBigInt() + BigInt(backoffMs),
+            updatedAt: this.nowBigInt(),
+          },
+        });
+      }
+    }
+  }
+
+  /**
+   * For now this just demonstrates Redis usage + marks SUCCESS.
+   * Later you'll plug in real logic (channels/categories/ads/videos).
+   */
+  private async handleSingleAction(
+    action: Prisma.CacheSyncActionGetPayload<unknown>, // loose type; you can refine
+  ): Promise<void> {
+    // TODO: replace this with real logic per entityType/operation
+    //       e.g., rebuild channels:all, ads pools, etc.
+
+    // Example: write a simple trace into Redis so you can see it's working.
+    const redisKey = `mgnt:cache-sync:last-processed:${action.id}`;
+    await this.redis.setJson(
+      redisKey,
+      {
+        id: action.id,
+        entityType: action.entityType,
+        operation: action.operation,
+        processedAt: Date.now(),
+      },
+      3600,
+    );
+
+    // Here is where you'd branch by entityType/operation, e.g.:
+    //
+    // switch (action.entityType) {
+    //   case CacheEntityType.CHANNEL:
+    //     if (action.operation === CacheOperation.REFRESH_ALL) {
+    //       await this.rebuildChannelsAll();
+    //     }
+    //     break;
+    //   case CacheEntityType.AD:
+    //     if (action.operation === CacheOperation.REFRESH) {
+    //       await this.refreshAdById(action.entityId!);
+    //     }
+    //     break;
+    //   // ...
+    // }
+
+    await this.prisma.cacheSyncAction.update({
+      where: { id: action.id },
+      data: {
+        status: CacheStatus.SUCCESS,
+        attempts: action.attempts + 1,
+        lastError: null,
+        updatedAt: this.nowBigInt(),
+      },
+    });
+
+    this.logger.debug(
+      `Processed CacheSyncAction id=${action.id}, entityType=${action.entityType}, operation=${action.operation}`,
+    );
+  }
+}

+ 35 - 0
apps/box-mgnt-api/src/cache-sync/cache-sync.types.ts

@@ -0,0 +1,35 @@
+// Keep enums string-based so they map nicely to DB values
+
+export enum CacheEntityType {
+  AD = 'AD',
+  AD_POOL = 'AD_POOL',
+  CHANNEL = 'CHANNEL',
+  CATEGORY = 'CATEGORY',
+  VIDEO_LIST = 'VIDEO_LIST',
+}
+
+export enum CacheOperation {
+  REFRESH = 'REFRESH', // refresh single item (e.g., ads:byId:<id>)
+  INVALIDATE = 'INVALIDATE', // delete cache only
+  REBUILD_POOL = 'REBUILD_POOL', // rebuild a list/pool (e.g., ads:pool:<type>:active)
+  REFRESH_ALL = 'REFRESH_ALL', // rebuild full set (e.g., channels:all)
+}
+
+export enum CacheStatus {
+  PENDING = 'PENDING',
+  SUCCESS = 'SUCCESS',
+  FAILED = 'FAILED',
+  GAVE_UP = 'GAVE_UP',
+}
+
+export interface CachePayload {
+  // For ads pools, e.g. { type: 'BANNER' }
+  type?: string;
+
+  // For video lists, e.g. { scope: 'HOME', page: 1 }
+  scope?: string;
+  page?: number;
+
+  // Allow extra fields for future use
+  [key: string]: unknown;
+}

+ 1 - 0
apps/box-mgnt-api/src/redis/redis.constants.ts

@@ -0,0 +1 @@
+export const REDIS_CLIENT = Symbol('REDIS_CLIENT');

+ 53 - 0
apps/box-mgnt-api/src/redis/redis.module.ts

@@ -0,0 +1,53 @@
+import { DynamicModule, Global, Module } from '@nestjs/common';
+import { ConfigService } from '@nestjs/config';
+import Redis from 'ioredis';
+import { REDIS_CLIENT } from './redis.constants';
+import { RedisService } from './redis.service';
+
+@Global()
+@Module({})
+export class RedisModule {
+  static forRoot(): DynamicModule {
+    return {
+      module: RedisModule,
+      providers: [
+        {
+          provide: REDIS_CLIENT,
+          inject: [ConfigService],
+          useFactory: (configService: ConfigService) => {
+            const host = configService.get<string>('REDIS_HOST', '127.0.0.1');
+            const port = configService.get<number>('REDIS_PORT', 6379);
+            const password = configService.get<string | undefined>(
+              'REDIS_PASSWORD',
+            );
+            const db = configService.get<number>('REDIS_DB', 0);
+            const keyPrefix = configService.get<string>(
+              'REDIS_KEY_PREFIX',
+              'box:',
+            );
+
+            const redis = new Redis({
+              host,
+              port,
+              password: password || undefined,
+              db,
+              keyPrefix,
+              lazyConnect: true,
+            });
+
+            // Optional logging / error handling
+            redis.on('error', (err) => {
+              // Avoid throwing here, just log; Nest will keep running
+              // You can hook this into your logger later
+              console.error('[Redis] error:', err?.message ?? err);
+            });
+
+            return redis;
+          },
+        },
+        RedisService,
+      ],
+      exports: [REDIS_CLIENT, RedisService],
+    };
+  }
+}

+ 76 - 0
apps/box-mgnt-api/src/redis/redis.service.ts

@@ -0,0 +1,76 @@
+import { Inject, Injectable } from '@nestjs/common';
+import type Redis from 'ioredis';
+import { REDIS_CLIENT } from './redis.constants';
+
+@Injectable()
+export class RedisService {
+  constructor(
+    @Inject(REDIS_CLIENT)
+    private readonly client: Redis,
+  ) {}
+
+  async ping(): Promise<string> {
+    return this.client.ping();
+  }
+
+  async get<T = unknown>(key: string): Promise<T | null> {
+    const raw = await this.client.get(key);
+    if (!raw) {
+      return null;
+    }
+
+    try {
+      return JSON.parse(raw) as T;
+    } catch {
+      // fallback for plain string
+      return raw as unknown as T;
+    }
+  }
+
+  async setJson(
+    key: string,
+    value: unknown,
+    ttlSeconds?: number,
+  ): Promise<'OK' | null> {
+    const payload = JSON.stringify(value);
+
+    if (ttlSeconds && ttlSeconds > 0) {
+      return this.client.set(key, payload, 'EX', ttlSeconds);
+    }
+
+    return this.client.set(key, payload);
+  }
+
+  async del(key: string | string[]): Promise<number> {
+    if (Array.isArray(key)) {
+      if (!key.length) {
+        return 0;
+      }
+      return this.client.del(...key);
+    }
+
+    return this.client.del(key);
+  }
+
+  // For lists/pools (ads, videos)
+  async setList(
+    key: string,
+    values: (string | number)[],
+    ttlSeconds?: number,
+  ): Promise<void> {
+    // Clear existing
+    await this.client.del(key);
+
+    if (values.length > 0) {
+      await this.client.rpush(key, ...values.map((v) => v.toString()));
+    }
+
+    if (ttlSeconds && ttlSeconds > 0) {
+      await this.client.expire(key, ttlSeconds);
+    }
+  }
+
+  async getList(key: string): Promise<string[]> {
+    return this.client.lrange(key, 0, -1);
+  }
+}

+ 4 - 3
package.json

@@ -28,13 +28,13 @@
     "@nestjs/axios": "^3.0.2",
     "@nestjs/cache-manager": "^2.2.2",
     "@nestjs/common": "^10.3.8",
-    "@nestjs/config": "^3.2.2",
+    "@nestjs/config": "^3.3.0",
     "@nestjs/core": "^10.3.8",
     "@nestjs/devtools-integration": "^0.1.6",
     "@nestjs/jwt": "^10.2.0",
     "@nestjs/mapped-types": "^2.0.5",
     "@nestjs/passport": "^10.0.3",
-    "@nestjs/platform-express": "^11.1.2",
+    "@nestjs/platform-express": "^10.4.20",
     "@nestjs/platform-fastify": "^10.3.8",
     "@nestjs/swagger": "^7.0.0",
     "@prisma/client": "^5.15.0",
@@ -51,6 +51,7 @@
     "dotenv": "^16.5.0",
     "exceljs": "^4.4.0",
     "fastify": "^4.26.2",
+    "ioredis": "^5.8.2",
     "lib-qqwry": "^1.3.4",
     "lodash": "^4.17.21",
     "mongodb": "^6.20.0",
@@ -73,7 +74,7 @@
   "devDependencies": {
     "@nestjs/cli": "^10.3.2",
     "@nestjs/schematics": "^10.1.1",
-    "@nestjs/testing": "^11.1.6",
+    "@nestjs/testing": "^10.4.20",
     "@swc/cli": "^0.3.12",
     "@swc/core": "^1.4.17",
     "@types/ali-oss": "^6.16.11",

+ 231 - 177
pnpm-lock.yaml

@@ -33,11 +33,11 @@ importers:
         specifier: ^10.3.8
         version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       '@nestjs/config':
-        specifier: ^3.2.2
+        specifier: ^3.3.0
         version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)
       '@nestjs/core':
         specifier: ^10.3.8
-        version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+        version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       '@nestjs/devtools-integration':
         specifier: ^0.1.6
         version: 0.1.6(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
@@ -51,8 +51,8 @@ importers:
         specifier: ^10.0.3
         version: 10.0.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
       '@nestjs/platform-express':
-        specifier: ^11.1.2
-        version: 11.1.9(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
+        specifier: ^10.4.20
+        version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
       '@nestjs/platform-fastify':
         specifier: ^10.3.8
         version: 10.4.20(@fastify/static@6.12.0)(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
@@ -101,6 +101,9 @@ importers:
       fastify:
         specifier: ^4.26.2
         version: 4.29.1
+      ioredis:
+        specifier: ^5.8.2
+        version: 5.8.2
       lib-qqwry:
         specifier: ^1.3.4
         version: 1.3.4
@@ -163,8 +166,8 @@ importers:
         specifier: ^10.1.1
         version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3)
       '@nestjs/testing':
-        specifier: ^11.1.6
-        version: 11.1.9(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@11.1.9)
+        specifier: ^10.4.20
+        version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20)
       '@swc/cli':
         specifier: ^0.3.12
         version: 0.3.14(@swc/core@1.15.2)(chokidar@3.6.0)
@@ -876,6 +879,9 @@ packages:
     resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==}
     deprecated: Use @eslint/object-schema instead
 
+  '@ioredis/commands@1.4.0':
+    resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
+
   '@isaacs/cliui@8.0.2':
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
@@ -1235,11 +1241,11 @@ packages:
       '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0
       passport: ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0
 
-  '@nestjs/platform-express@11.1.9':
-    resolution: {integrity: sha512-GVd3+0lO0mJq2m1kl9hDDnVrX3Nd4oH3oDfklz0pZEVEVS0KVSp63ufHq2Lu9cyPdSBuelJr9iPm2QQ1yX+Kmw==}
+  '@nestjs/platform-express@10.4.20':
+    resolution: {integrity: sha512-rh97mX3rimyf4xLMLHuTOBKe6UD8LOJ14VlJ1F/PTd6C6ZK9Ak6EHuJvdaGcSFQhd3ZMBh3I6CuujKGW9pNdIg==}
     peerDependencies:
-      '@nestjs/common': ^11.0.0
-      '@nestjs/core': ^11.0.0
+      '@nestjs/common': ^10.0.0
+      '@nestjs/core': ^10.0.0
 
   '@nestjs/platform-fastify@10.4.20':
     resolution: {integrity: sha512-XnfNNjZ0d0qo7qBQtnK4NPsYlUzy8Y7LlZ4i9YawO37T+dnVQewTv9La+sP8yS38pOwPlKqeWLR5RVwMKcoCCQ==}
@@ -1276,13 +1282,13 @@ packages:
       class-validator:
         optional: true
 
-  '@nestjs/testing@11.1.9':
-    resolution: {integrity: sha512-UFxerBDdb0RUNxQNj25pvkvNE7/vxKhXYWBt3QuwBFnYISzRIzhVlyIqLfoV5YI3zV0m0Nn4QAn1KM0zzwfEng==}
+  '@nestjs/testing@10.4.20':
+    resolution: {integrity: sha512-nMkRDukDKskdPruM6EsgMq7yJua+CPZM6I6FrLP8yXw8BiVSPv9Nm0CtcGGwt3kgZF9hfxKjGqLjsvVBsv6Vfw==}
     peerDependencies:
-      '@nestjs/common': ^11.0.0
-      '@nestjs/core': ^11.0.0
-      '@nestjs/microservices': ^11.0.0
-      '@nestjs/platform-express': ^11.0.0
+      '@nestjs/common': ^10.0.0
+      '@nestjs/core': ^10.0.0
+      '@nestjs/microservices': ^10.0.0
+      '@nestjs/platform-express': ^10.0.0
     peerDependenciesMeta:
       '@nestjs/microservices':
         optional: true
@@ -2083,8 +2089,8 @@ packages:
   abstract-logging@2.0.1:
     resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
 
-  accepts@2.0.0:
-    resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
+  accepts@1.3.8:
+    resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
     engines: {node: '>= 0.6'}
 
   acorn-jsx@5.3.2:
@@ -2215,6 +2221,9 @@ packages:
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
+  array-flatten@1.1.1:
+    resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
+
   array-timsort@1.0.3:
     resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
 
@@ -2316,9 +2325,9 @@ packages:
   bluebird@3.4.7:
     resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==}
 
-  body-parser@2.2.0:
-    resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
-    engines: {node: '>=18'}
+  body-parser@1.20.3:
+    resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
+    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
 
   bowser@2.12.1:
     resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==}
@@ -2513,6 +2522,10 @@ packages:
     resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
     engines: {node: '>=0.8'}
 
+  cluster-key-slot@1.1.2:
+    resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
+    engines: {node: '>=0.10.0'}
+
   co@4.6.0:
     resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
     engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -2587,10 +2600,6 @@ packages:
     resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
     engines: {node: '>= 0.6'}
 
-  content-disposition@1.0.1:
-    resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
-    engines: {node: '>=18'}
-
   content-type@1.0.5:
     resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
     engines: {node: '>= 0.6'}
@@ -2598,9 +2607,12 @@ packages:
   convert-source-map@2.0.0:
     resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
 
-  cookie-signature@1.2.2:
-    resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
-    engines: {node: '>=6.6.0'}
+  cookie-signature@1.0.6:
+    resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==}
+
+  cookie@0.7.1:
+    resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==}
+    engines: {node: '>= 0.6'}
 
   cookie@0.7.2:
     resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
@@ -2654,6 +2666,14 @@ packages:
   dayjs@1.11.19:
     resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
 
+  debug@2.6.9:
+    resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
   debug@4.4.3:
     resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
     engines: {node: '>=6.0'}
@@ -2708,10 +2728,18 @@ packages:
   delegates@1.0.0:
     resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
 
+  denque@2.1.0:
+    resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
+    engines: {node: '>=0.10'}
+
   depd@2.0.0:
     resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
     engines: {node: '>= 0.8'}
 
+  destroy@1.2.0:
+    resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
+    engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+
   detect-libc@2.1.2:
     resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
     engines: {node: '>=8'}
@@ -2783,6 +2811,10 @@ packages:
   emoji-regex@9.2.2:
     resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
 
+  encodeurl@1.0.2:
+    resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
+    engines: {node: '>= 0.8'}
+
   encodeurl@2.0.0:
     resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
     engines: {node: '>= 0.8'}
@@ -2950,9 +2982,9 @@ packages:
     resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==}
     engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
 
-  express@5.1.0:
-    resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
-    engines: {node: '>= 18'}
+  express@4.21.2:
+    resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
+    engines: {node: '>= 0.10.0'}
 
   ext-list@2.2.2:
     resolution: {integrity: sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==}
@@ -3068,8 +3100,8 @@ packages:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
 
-  finalhandler@2.1.0:
-    resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
+  finalhandler@1.3.1:
+    resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
     engines: {node: '>= 0.8'}
 
   find-my-way@8.2.2:
@@ -3131,9 +3163,9 @@ packages:
     resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
     engines: {node: '>=0.8'}
 
-  fresh@2.0.0:
-    resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
-    engines: {node: '>= 0.8'}
+  fresh@0.5.2:
+    resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
+    engines: {node: '>= 0.6'}
 
   fs-constants@1.0.0:
     resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@@ -3322,14 +3354,6 @@ packages:
     resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
     engines: {node: '>=0.10.0'}
 
-  iconv-lite@0.6.3:
-    resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
-    engines: {node: '>=0.10.0'}
-
-  iconv-lite@0.7.0:
-    resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==}
-    engines: {node: '>=0.10.0'}
-
   ieee754@1.2.1:
     resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
 
@@ -3368,6 +3392,10 @@ packages:
     resolution: {integrity: sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==}
     engines: {node: '>=18'}
 
+  ioredis@5.8.2:
+    resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==}
+    engines: {node: '>=12.22.0'}
+
   ipaddr.js@1.9.1:
     resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
     engines: {node: '>= 0.10'}
@@ -3431,9 +3459,6 @@ packages:
     resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==}
     engines: {node: '>=0.10.0'}
 
-  is-promise@4.0.0:
-    resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
-
   is-stream@1.1.0:
     resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
     engines: {node: '>=0.10.0'}
@@ -3758,6 +3783,9 @@ packages:
   lodash.includes@4.3.0:
     resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
 
+  lodash.isarguments@3.1.0:
+    resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
+
   lodash.isboolean@3.0.3:
     resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
 
@@ -3847,10 +3875,6 @@ packages:
     resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
     engines: {node: '>= 0.6'}
 
-  media-typer@1.1.0:
-    resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
-    engines: {node: '>= 0.8'}
-
   memfs@3.5.3:
     resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==}
     engines: {node: '>= 4.0.0'}
@@ -3858,9 +3882,8 @@ packages:
   memory-pager@1.5.0:
     resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
 
-  merge-descriptors@2.0.0:
-    resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
-    engines: {node: '>=18'}
+  merge-descriptors@1.0.3:
+    resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
 
   merge-stream@2.0.0:
     resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -3889,9 +3912,10 @@ packages:
     resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
     engines: {node: '>= 0.6'}
 
-  mime-types@3.0.1:
-    resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
-    engines: {node: '>= 0.6'}
+  mime@1.6.0:
+    resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
+    engines: {node: '>=4'}
+    hasBin: true
 
   mime@2.6.0:
     resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
@@ -3987,6 +4011,9 @@ packages:
       socks:
         optional: true
 
+  ms@2.0.0:
+    resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+
   ms@2.1.3:
     resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
 
@@ -4009,8 +4036,8 @@ packages:
   natural-compare@1.4.0:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
 
-  negotiator@1.0.0:
-    resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
+  negotiator@0.6.3:
+    resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
     engines: {node: '>= 0.6'}
 
   neo-async@2.6.2:
@@ -4203,15 +4230,15 @@ packages:
     resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
     engines: {node: '>=16 || 14 >=14.18'}
 
+  path-to-regexp@0.1.12:
+    resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
+
   path-to-regexp@3.3.0:
     resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==}
 
   path-to-regexp@6.3.0:
     resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
 
-  path-to-regexp@8.3.0:
-    resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
-
   path-type@4.0.0:
     resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
     engines: {node: '>=8'}
@@ -4362,6 +4389,10 @@ packages:
     engines: {node: '>=10.13.0'}
     hasBin: true
 
+  qs@6.13.0:
+    resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
+    engines: {node: '>=0.6'}
+
   qs@6.14.0:
     resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
     engines: {node: '>=0.6'}
@@ -4383,9 +4414,9 @@ packages:
     resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
     engines: {node: '>= 0.6'}
 
-  raw-body@3.0.1:
-    resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==}
-    engines: {node: '>= 0.10'}
+  raw-body@2.5.2:
+    resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==}
+    engines: {node: '>= 0.8'}
 
   react-is@18.3.1:
     resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
@@ -4416,6 +4447,14 @@ packages:
     resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
     engines: {node: '>= 12.13.0'}
 
+  redis-errors@1.2.0:
+    resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
+    engines: {node: '>=4'}
+
+  redis-parser@3.0.0:
+    resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
+    engines: {node: '>=4'}
+
   reflect-metadata@0.2.2:
     resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
 
@@ -4488,10 +4527,6 @@ packages:
     deprecated: Rimraf versions prior to v4 are no longer supported
     hasBin: true
 
-  router@2.2.0:
-    resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
-    engines: {node: '>= 18'}
-
   run-async@2.4.1:
     resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
     engines: {node: '>=0.12.0'}
@@ -4560,16 +4595,16 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
-  send@1.2.0:
-    resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
-    engines: {node: '>= 18'}
+  send@0.19.0:
+    resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
+    engines: {node: '>= 0.8.0'}
 
   serialize-javascript@6.0.2:
     resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
 
-  serve-static@2.2.0:
-    resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
-    engines: {node: '>= 18'}
+  serve-static@1.16.2:
+    resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
+    engines: {node: '>= 0.8.0'}
 
   set-blocking@2.0.0:
     resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
@@ -4680,14 +4715,13 @@ packages:
     resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
     engines: {node: '>=10'}
 
+  standard-as-callback@2.1.0:
+    resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
+
   statuses@2.0.1:
     resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
     engines: {node: '>= 0.8'}
 
-  statuses@2.0.2:
-    resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
-    engines: {node: '>= 0.8'}
-
   stream-wormhole@1.1.0:
     resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==}
     engines: {node: '>=4.0.0'}
@@ -4991,10 +5025,6 @@ packages:
     resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
     engines: {node: '>= 0.6'}
 
-  type-is@2.0.1:
-    resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
-    engines: {node: '>= 0.6'}
-
   typedarray@0.0.6:
     resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
 
@@ -6190,6 +6220,8 @@ snapshots:
 
   '@humanwhocodes/object-schema@2.0.3': {}
 
+  '@ioredis/commands@1.4.0': {}
+
   '@isaacs/cliui@8.0.2':
     dependencies:
       string-width: 5.1.2
@@ -6545,7 +6577,7 @@ snapshots:
   '@nestjs/cache-manager@2.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(cache-manager@5.7.6)(rxjs@7.8.2)':
     dependencies:
       '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
-      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       cache-manager: 5.7.6
       rxjs: 7.8.2
 
@@ -6600,7 +6632,7 @@ snapshots:
       lodash: 4.17.21
       rxjs: 7.8.2
 
-  '@nestjs/core@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
+  '@nestjs/core@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)':
     dependencies:
       '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       '@nuxtjs/opencollective': 0.3.2
@@ -6612,14 +6644,14 @@ snapshots:
       tslib: 2.8.1
       uid: 2.0.2
     optionalDependencies:
-      '@nestjs/platform-express': 11.1.9(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
+      '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
     transitivePeerDependencies:
       - encoding
 
   '@nestjs/devtools-integration@0.1.6(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)':
     dependencies:
       '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
-      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       chalk: 4.1.2
       node-fetch: 2.7.0
     transitivePeerDependencies:
@@ -6652,14 +6684,14 @@ snapshots:
       '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       passport: 0.7.0
 
-  '@nestjs/platform-express@11.1.9(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)':
+  '@nestjs/platform-express@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)':
     dependencies:
       '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
-      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+      body-parser: 1.20.3
       cors: 2.8.5
-      express: 5.1.0
+      express: 4.21.2
       multer: 2.0.2
-      path-to-regexp: 8.3.0
       tslib: 2.8.1
     transitivePeerDependencies:
       - supports-color
@@ -6670,7 +6702,7 @@ snapshots:
       '@fastify/formbody': 7.4.0
       '@fastify/middie': 8.3.3
       '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
-      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       fastify: 4.28.1
       light-my-request: 6.3.0
       path-to-regexp: 3.3.0
@@ -6704,7 +6736,7 @@ snapshots:
     dependencies:
       '@microsoft/tsdoc': 0.15.1
       '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
-      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)
       js-yaml: 4.1.0
       lodash: 4.17.21
@@ -6716,13 +6748,13 @@ snapshots:
       class-transformer: 0.5.1
       class-validator: 0.14.2
 
-  '@nestjs/testing@11.1.9(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@11.1.9)':
+  '@nestjs/testing@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20)':
     dependencies:
       '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)
-      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.9)(reflect-metadata@0.2.2)(rxjs@7.8.2)
+      '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2)
       tslib: 2.8.1
     optionalDependencies:
-      '@nestjs/platform-express': 11.1.9(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
+      '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)
 
   '@noble/hashes@1.8.0': {}
 
@@ -7692,10 +7724,10 @@ snapshots:
 
   abstract-logging@2.0.1: {}
 
-  accepts@2.0.0:
+  accepts@1.3.8:
     dependencies:
-      mime-types: 3.0.1
-      negotiator: 1.0.0
+      mime-types: 2.1.35
+      negotiator: 0.6.3
 
   acorn-jsx@5.3.2(acorn@8.15.0):
     dependencies:
@@ -7839,6 +7871,8 @@ snapshots:
 
   argparse@2.0.1: {}
 
+  array-flatten@1.1.1: {}
+
   array-timsort@1.0.3: {}
 
   array-union@2.1.0: {}
@@ -7967,17 +8001,20 @@ snapshots:
 
   bluebird@3.4.7: {}
 
-  body-parser@2.2.0:
+  body-parser@1.20.3:
     dependencies:
       bytes: 3.1.2
       content-type: 1.0.5
-      debug: 4.4.3
+      debug: 2.6.9
+      depd: 2.0.0
+      destroy: 1.2.0
       http-errors: 2.0.0
-      iconv-lite: 0.6.3
+      iconv-lite: 0.4.24
       on-finished: 2.4.1
-      qs: 6.14.0
-      raw-body: 3.0.1
-      type-is: 2.0.1
+      qs: 6.13.0
+      raw-body: 2.5.2
+      type-is: 1.6.18
+      unpipe: 1.0.0
     transitivePeerDependencies:
       - supports-color
 
@@ -8181,6 +8218,8 @@ snapshots:
 
   clone@1.0.4: {}
 
+  cluster-key-slot@1.1.2: {}
+
   co@4.6.0: {}
 
   codepage@1.15.0: {}
@@ -8247,13 +8286,13 @@ snapshots:
     dependencies:
       safe-buffer: 5.2.1
 
-  content-disposition@1.0.1: {}
-
   content-type@1.0.5: {}
 
   convert-source-map@2.0.0: {}
 
-  cookie-signature@1.2.2: {}
+  cookie-signature@1.0.6: {}
+
+  cookie@0.7.1: {}
 
   cookie@0.7.2: {}
 
@@ -8302,6 +8341,10 @@ snapshots:
 
   dayjs@1.11.19: {}
 
+  debug@2.6.9:
+    dependencies:
+      ms: 2.0.0
+
   debug@4.4.3:
     dependencies:
       ms: 2.1.3
@@ -8339,8 +8382,12 @@ snapshots:
 
   delegates@1.0.0: {}
 
+  denque@2.1.0: {}
+
   depd@2.0.0: {}
 
+  destroy@1.2.0: {}
+
   detect-libc@2.1.2: {}
 
   detect-newline@3.1.0: {}
@@ -8401,6 +8448,8 @@ snapshots:
 
   emoji-regex@9.2.2: {}
 
+  encodeurl@1.0.2: {}
+
   encodeurl@2.0.0: {}
 
   end-of-stream@1.4.5:
@@ -8622,34 +8671,38 @@ snapshots:
       jest-mock: 30.2.0
       jest-util: 30.2.0
 
-  express@5.1.0:
+  express@4.21.2:
     dependencies:
-      accepts: 2.0.0
-      body-parser: 2.2.0
-      content-disposition: 1.0.1
+      accepts: 1.3.8
+      array-flatten: 1.1.1
+      body-parser: 1.20.3
+      content-disposition: 0.5.4
       content-type: 1.0.5
-      cookie: 0.7.2
-      cookie-signature: 1.2.2
-      debug: 4.4.3
+      cookie: 0.7.1
+      cookie-signature: 1.0.6
+      debug: 2.6.9
+      depd: 2.0.0
       encodeurl: 2.0.0
       escape-html: 1.0.3
       etag: 1.8.1
-      finalhandler: 2.1.0
-      fresh: 2.0.0
+      finalhandler: 1.3.1
+      fresh: 0.5.2
       http-errors: 2.0.0
-      merge-descriptors: 2.0.0
-      mime-types: 3.0.1
+      merge-descriptors: 1.0.3
+      methods: 1.1.2
       on-finished: 2.4.1
-      once: 1.4.0
       parseurl: 1.3.3
+      path-to-regexp: 0.1.12
       proxy-addr: 2.0.7
-      qs: 6.14.0
+      qs: 6.13.0
       range-parser: 1.2.1
-      router: 2.2.0
-      send: 1.2.0
-      serve-static: 2.2.0
-      statuses: 2.0.2
-      type-is: 2.0.1
+      safe-buffer: 5.2.1
+      send: 0.19.0
+      serve-static: 1.16.2
+      setprototypeof: 1.2.0
+      statuses: 2.0.1
+      type-is: 1.6.18
+      utils-merge: 1.0.1
       vary: 1.1.2
     transitivePeerDependencies:
       - supports-color
@@ -8812,14 +8865,15 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
-  finalhandler@2.1.0:
+  finalhandler@1.3.1:
     dependencies:
-      debug: 4.4.3
+      debug: 2.6.9
       encodeurl: 2.0.0
       escape-html: 1.0.3
       on-finished: 2.4.1
       parseurl: 1.3.3
-      statuses: 2.0.2
+      statuses: 2.0.1
+      unpipe: 1.0.0
     transitivePeerDependencies:
       - supports-color
 
@@ -8893,7 +8947,7 @@ snapshots:
 
   frac@1.1.2: {}
 
-  fresh@2.0.0: {}
+  fresh@0.5.2: {}
 
   fs-constants@1.0.0: {}
 
@@ -9114,14 +9168,6 @@ snapshots:
     dependencies:
       safer-buffer: 2.1.2
 
-  iconv-lite@0.6.3:
-    dependencies:
-      safer-buffer: 2.1.2
-
-  iconv-lite@0.7.0:
-    dependencies:
-      safer-buffer: 2.1.2
-
   ieee754@1.2.1: {}
 
   ignore@5.3.2: {}
@@ -9183,6 +9229,20 @@ snapshots:
       strip-ansi: 6.0.1
       wrap-ansi: 6.2.0
 
+  ioredis@5.8.2:
+    dependencies:
+      '@ioredis/commands': 1.4.0
+      cluster-key-slot: 1.1.2
+      debug: 4.4.3
+      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
+    transitivePeerDependencies:
+      - supports-color
+
   ipaddr.js@1.9.1: {}
 
   is-accessor-descriptor@1.0.1:
@@ -9230,8 +9290,6 @@ snapshots:
     dependencies:
       isobject: 3.0.1
 
-  is-promise@4.0.0: {}
-
   is-stream@1.1.0: {}
 
   is-stream@2.0.1: {}
@@ -9754,6 +9812,8 @@ snapshots:
 
   lodash.includes@4.3.0: {}
 
+  lodash.isarguments@3.1.0: {}
+
   lodash.isboolean@3.0.3: {}
 
   lodash.isequal@4.5.0: {}
@@ -9824,15 +9884,13 @@ snapshots:
 
   media-typer@0.3.0: {}
 
-  media-typer@1.1.0: {}
-
   memfs@3.5.3:
     dependencies:
       fs-monkey: 1.1.0
 
   memory-pager@1.5.0: {}
 
-  merge-descriptors@2.0.0: {}
+  merge-descriptors@1.0.3: {}
 
   merge-stream@2.0.0: {}
 
@@ -9853,9 +9911,7 @@ snapshots:
     dependencies:
       mime-db: 1.52.0
 
-  mime-types@3.0.1:
-    dependencies:
-      mime-db: 1.54.0
+  mime@1.6.0: {}
 
   mime@2.6.0: {}
 
@@ -9915,6 +9971,8 @@ snapshots:
       bson: 6.10.4
       mongodb-connection-string-url: 3.0.2
 
+  ms@2.0.0: {}
+
   ms@2.1.3: {}
 
   multer@2.0.2:
@@ -9935,7 +9993,7 @@ snapshots:
 
   natural-compare@1.4.0: {}
 
-  negotiator@1.0.0: {}
+  negotiator@0.6.3: {}
 
   neo-async@2.6.2: {}
 
@@ -10109,12 +10167,12 @@ snapshots:
       lru-cache: 10.4.3
       minipass: 7.1.2
 
+  path-to-regexp@0.1.12: {}
+
   path-to-regexp@3.3.0: {}
 
   path-to-regexp@6.3.0: {}
 
-  path-to-regexp@8.3.0: {}
-
   path-type@4.0.0: {}
 
   pause@0.0.1: {}
@@ -10270,6 +10328,10 @@ snapshots:
       pngjs: 5.0.0
       yargs: 15.4.1
 
+  qs@6.13.0:
+    dependencies:
+      side-channel: 1.1.0
+
   qs@6.14.0:
     dependencies:
       side-channel: 1.1.0
@@ -10286,11 +10348,11 @@ snapshots:
 
   range-parser@1.2.1: {}
 
-  raw-body@3.0.1:
+  raw-body@2.5.2:
     dependencies:
       bytes: 3.1.2
       http-errors: 2.0.0
-      iconv-lite: 0.7.0
+      iconv-lite: 0.4.24
       unpipe: 1.0.0
 
   react-is@18.3.1: {}
@@ -10333,6 +10395,12 @@ snapshots:
 
   real-require@0.2.0: {}
 
+  redis-errors@1.2.0: {}
+
+  redis-parser@3.0.0:
+    dependencies:
+      redis-errors: 1.2.0
+
   reflect-metadata@0.2.2: {}
 
   regex-not@1.0.2:
@@ -10385,16 +10453,6 @@ snapshots:
     dependencies:
       glob: 7.2.3
 
-  router@2.2.0:
-    dependencies:
-      debug: 4.4.3
-      depd: 2.0.0
-      is-promise: 4.0.0
-      parseurl: 1.3.3
-      path-to-regexp: 8.3.0
-    transitivePeerDependencies:
-      - supports-color
-
   run-async@2.4.1: {}
 
   run-async@3.0.0: {}
@@ -10456,19 +10514,21 @@ snapshots:
 
   semver@7.7.3: {}
 
-  send@1.2.0:
+  send@0.19.0:
     dependencies:
-      debug: 4.4.3
-      encodeurl: 2.0.0
+      debug: 2.6.9
+      depd: 2.0.0
+      destroy: 1.2.0
+      encodeurl: 1.0.2
       escape-html: 1.0.3
       etag: 1.8.1
-      fresh: 2.0.0
+      fresh: 0.5.2
       http-errors: 2.0.0
-      mime-types: 3.0.1
+      mime: 1.6.0
       ms: 2.1.3
       on-finished: 2.4.1
       range-parser: 1.2.1
-      statuses: 2.0.2
+      statuses: 2.0.1
     transitivePeerDependencies:
       - supports-color
 
@@ -10476,12 +10536,12 @@ snapshots:
     dependencies:
       randombytes: 2.1.0
 
-  serve-static@2.2.0:
+  serve-static@1.16.2:
     dependencies:
       encodeurl: 2.0.0
       escape-html: 1.0.3
       parseurl: 1.3.3
-      send: 1.2.0
+      send: 0.19.0
     transitivePeerDependencies:
       - supports-color
 
@@ -10596,9 +10656,9 @@ snapshots:
     dependencies:
       escape-string-regexp: 2.0.0
 
-  statuses@2.0.1: {}
+  standard-as-callback@2.1.0: {}
 
-  statuses@2.0.2: {}
+  statuses@2.0.1: {}
 
   stream-wormhole@1.1.0: {}
 
@@ -10901,12 +10961,6 @@ snapshots:
       media-typer: 0.3.0
       mime-types: 2.1.35
 
-  type-is@2.0.1:
-    dependencies:
-      content-type: 1.0.5
-      media-typer: 1.1.0
-      mime-types: 3.0.1
-
   typedarray@0.0.6: {}
 
   typescript@5.7.2: {}

+ 18 - 0
prisma/mysql/migrations/20251124165457_add_redis_cache_sync_action/migration.sql

@@ -0,0 +1,18 @@
+-- CreateTable
+CREATE TABLE `CacheSyncAction` (
+    `id` BIGINT NOT NULL AUTO_INCREMENT,
+    `entityType` VARCHAR(50) NOT NULL,
+    `entityId` BIGINT NULL,
+    `operation` VARCHAR(50) NOT NULL,
+    `status` VARCHAR(20) NOT NULL,
+    `attempts` INTEGER NOT NULL DEFAULT 0,
+    `nextAttemptAt` BIGINT NULL,
+    `lastError` VARCHAR(500) NULL,
+    `payload` JSON NULL,
+    `createdAt` BIGINT NOT NULL,
+    `updatedAt` BIGINT NOT NULL,
+
+    INDEX `CacheSyncAction_status_nextAttemptAt_idx`(`status`, `nextAttemptAt`),
+    INDEX `CacheSyncAction_entityType_entityId_idx`(`entityType`, `entityId`),
+    PRIMARY KEY (`id`)
+) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

+ 31 - 0
prisma/mysql/schema/cache-sync-action.prisma

@@ -0,0 +1,31 @@
+model CacheSyncAction {
+  id           BigInt   @id @default(autoincrement()) @db.BigInt
+
+  // e.g. 'AD', 'AD_POOL', 'CHANNEL', 'CATEGORY', 'VIDEO_LIST'
+  entityType   String   @db.VarChar(50)
+
+  // optional: when operation targets a specific entity
+  entityId     BigInt?  @db.BigInt
+
+  // e.g. 'REFRESH', 'INVALIDATE', 'REBUILD_POOL', 'REFRESH_ALL'
+  operation    String   @db.VarChar(50)
+
+  // 'PENDING' | 'SUCCESS' | 'FAILED' | 'GAVE_UP'
+  status       String   @db.VarChar(20)
+
+  attempts     Int      @default(0)
+
+  // epoch millis as BigInt (your preference)
+  nextAttemptAt BigInt? @db.BigInt
+
+  lastError    String?  @db.VarChar(500)
+
+  // for extra info, like { "type": "BANNER" }
+  payload      Json?
+
+  createdAt    BigInt   @db.BigInt
+  updatedAt    BigInt   @db.BigInt
+
+  @@index([status, nextAttemptAt])
+  @@index([entityType, entityId])
+}

+ 0 - 156
structures.txt

@@ -5,168 +5,26 @@
 │       │   ├── app.config.ts
 │       │   ├── app.module.ts
 │       │   ├── config
-│       │   │   └── env.validation.ts
 │       │   ├── global.d.ts
 │       │   ├── main.ts
 │       │   └── mgnt-backend
-│       │       ├── core
-│       │       │   ├── auth
-│       │       │   │   ├── auth.constants.ts
-│       │       │   │   ├── auth.controller.ts
-│       │       │   │   ├── auth.dto.ts
-│       │       │   │   ├── auth.interface.ts
-│       │       │   │   ├── auth.module.ts
-│       │       │   │   ├── auth.service.ts
-│       │       │   │   ├── config
-│       │       │   │   │   └── jwt.config.ts
-│       │       │   │   ├── decorators
-│       │       │   │   │   └── public.decorator.ts
-│       │       │   │   ├── dto
-│       │       │   │   │   └── 2fa.dto.ts
-│       │       │   │   ├── guards
-│       │       │   │   │   ├── jwt-auth.guard.ts
-│       │       │   │   │   ├── local-auth.guard.ts
-│       │       │   │   │   ├── mfa-stage.guard.ts
-│       │       │   │   │   └── rbac.guard.ts
-│       │       │   │   ├── strategies
-│       │       │   │   │   ├── jwt.strategy.ts
-│       │       │   │   │   └── local.strategy.ts
-│       │       │   │   ├── totp.helper.ts
-│       │       │   │   └── twofa.service.ts
-│       │       │   ├── core.module.ts
-│       │       │   ├── logging
-│       │       │   │   ├── login-log
-│       │       │   │   │   ├── login-log.controller.ts
-│       │       │   │   │   ├── login-log.module.ts
-│       │       │   │   │   └── login-log.service.ts
-│       │       │   │   ├── operation-log
-│       │       │   │   │   ├── operation-log.controller.ts
-│       │       │   │   │   ├── operation-log.module.ts
-│       │       │   │   │   └── operation-log.service.ts
-│       │       │   │   └── quota-log
-│       │       │   │       ├── quota-log.controller.ts
-│       │       │   │       ├── quota-log.module.ts
-│       │       │   │       └── quota-log.service.ts
-│       │       │   ├── menu
-│       │       │   │   ├── menu.constants.ts
-│       │       │   │   ├── menu.controller.ts
-│       │       │   │   ├── menu.dto.ts
-│       │       │   │   ├── menu.interface.ts
-│       │       │   │   ├── menu.module.ts
-│       │       │   │   └── menu.service.ts
-│       │       │   ├── role
-│       │       │   │   ├── role.constants.ts
-│       │       │   │   ├── role.controller.ts
-│       │       │   │   ├── role.dto.ts
-│       │       │   │   ├── role.module.ts
-│       │       │   │   └── role.service.ts
-│       │       │   └── user
-│       │       │       ├── user.constants.ts
-│       │       │       ├── user.controller.ts
-│       │       │       ├── user.dto.ts
-│       │       │       ├── user.module.ts
-│       │       │       └── user.service.ts
-│       │       ├── feature
-│       │       │   ├── ads
-│       │       │   │   ├── ads.controller.ts
-│       │       │   │   ├── ads.dto.ts
-│       │       │   │   ├── ads.module.ts
-│       │       │   │   └── ads.service.ts
-│       │       │   ├── category
-│       │       │   │   ├── category.controller.ts
-│       │       │   │   ├── category.dto.ts
-│       │       │   │   ├── category.module.ts
-│       │       │   │   └── category.service.ts
-│       │       │   ├── channel
-│       │       │   │   ├── channel.controller.ts
-│       │       │   │   ├── channel.dto.ts
-│       │       │   │   ├── channel.module.ts
-│       │       │   │   └── channel.service.ts
-│       │       │   ├── common
-│       │       │   │   ├── mongo-id.dto.ts
-│       │       │   │   └── status.enum.ts
-│       │       │   ├── feature.module.ts
-│       │       │   ├── mgnt-http-service
-│       │       │   │   ├── mgnt-http-service.config.ts
-│       │       │   │   ├── mgnt-http-service.module.ts
-│       │       │   │   └── mgnt-http.service.ts
-│       │       │   ├── oss
-│       │       │   │   ├── oss.config.ts
-│       │       │   │   ├── oss.controller.ts
-│       │       │   │   ├── oss.module.ts
-│       │       │   │   └── oss.service.ts
-│       │       │   ├── s3
-│       │       │   │   ├── s3.config.ts
-│       │       │   │   ├── s3.controller.ts
-│       │       │   │   ├── s3.module.ts
-│       │       │   │   └── s3.service.ts
-│       │       │   ├── sync-videomedia
-│       │       │   │   ├── sync-videomedia.controller.ts
-│       │       │   │   ├── sync-videomedia.module.ts
-│       │       │   │   └── sync-videomedia.service.ts
-│       │       │   ├── system-params
-│       │       │   │   ├── system-param.dto.ts
-│       │       │   │   ├── system-params.controller.ts
-│       │       │   │   ├── system-params.module.ts
-│       │       │   │   └── system-params.service.ts
-│       │       │   ├── tag
-│       │       │   │   ├── tag.controller.ts
-│       │       │   │   ├── tag.dto.ts
-│       │       │   │   ├── tag.module.ts
-│       │       │   │   └── tag.service.ts
-│       │       │   └── video-media
-│       │       │       ├── video-media.controller.ts
-│       │       │       ├── video-media.dto.ts
-│       │       │       ├── video-media.module.ts
-│       │       │       └── video-media.service.ts
-│       │       └── mgnt-backend.module.ts
 │       └── tsconfig.json
-├── ARCHITECTURE_FLOW.md
-├── BEFORE_AFTER.md
-├── box-mgnt-note.md
-├── box-nestjs-monorepo-init.md
-├── DEPLOYMENT_CHECKLIST.md
-├── DEVELOPER_GUIDE.md
-├── IMPLEMENTATION_SUMMARY.md
 ├── libs
 │   ├── common
 │   │   ├── package.json
 │   │   ├── src
 │   │   │   ├── common.module.ts
 │   │   │   ├── config
-│   │   │   │   └── pino.config.ts
 │   │   │   ├── crypto
-│   │   │   │   └── aes-gcm.ts
 │   │   │   ├── decorators
-│   │   │   │   ├── auth-user.decorator.ts
-│   │   │   │   └── operation-log.decorator.ts
 │   │   │   ├── dto
-│   │   │   │   ├── page-list.dto.ts
-│   │   │   │   └── page-list-response.dto.ts
 │   │   │   ├── filters
-│   │   │   │   ├── all-exceptions.filter.ts
-│   │   │   │   ├── http-exception.filter.ts
-│   │   │   │   └── index.ts
 │   │   │   ├── guards
-│   │   │   │   ├── index.ts
-│   │   │   │   ├── mfa.guard.ts
-│   │   │   │   └── rate-limit.guard.ts
 │   │   │   ├── interceptors
-│   │   │   │   ├── correlation.interceptor.ts
-│   │   │   │   ├── logging.interceptor.ts
-│   │   │   │   ├── operation-log.interceptor.ts
-│   │   │   │   └── response.interceptor.ts
 │   │   │   ├── interfaces
-│   │   │   │   ├── api-response.interface.ts
-│   │   │   │   ├── index.ts
-│   │   │   │   ├── operation-logger.interface.ts
-│   │   │   │   └── response.interface.ts
 │   │   │   ├── services
-│   │   │   │   └── exception.service.ts
 │   │   │   ├── types
-│   │   │   │   └── fastify.d.ts
 │   │   │   └── utils
-│   │   │       └── image-lib.ts
 │   │   └── tsconfig.json
 │   ├── core
 │   │   ├── package.json
@@ -176,16 +34,12 @@
 │       ├── package.json
 │       ├── src
 │       │   ├── prisma
-│       │   │   ├── mongo-prisma.service.ts
-│       │   │   ├── mysql-prisma.service.ts
-│       │   │   └── prisma.module.ts
 │       │   ├── shared.module.ts
 │       │   └── utils.service.ts
 │       └── tsconfig.json
 ├── logs
 │   ├── error.log
 │   └── info.log
-├── mongo-db-seeds.md
 ├── nest-cli.json
 ├── package.json
 ├── pnpm-lock.yaml
@@ -205,7 +59,6 @@
 │   └── mysql
 │       ├── migrations
 │       │   ├── 20251121082348_init_db
-│       │   │   └── migration.sql
 │       │   └── migration_lock.toml
 │       ├── schema
 │       │   ├── api-permission.prisma
@@ -219,18 +72,9 @@
 │       │   ├── role-menu.prisma
 │       │   ├── role.prisma
 │       │   ├── seeds
-│       │   │   ├── menu-seeds.ts
-│       │   │   ├── seed-menu.ts
-│       │   │   ├── SEED_REVIEW.md
-│       │   │   └── seed-user.ts
 │       │   ├── user.prisma
 │       │   └── user-role.prisma
 │       └── seed.ts
-├── REFACTOR_README.md
-├── REFACTOR_SUMMARY.md
-├── structures.txt
 ├── tsconfig.base.json
 ├── tsconfig.json
 └── tsconfig.seed.json
-
-60 directories, 174 files