optimize: sử dụng redis để cache cho các response từ server

This commit is contained in:
2026-06-16 20:17:17 +07:00
parent 2fabdb79df
commit 37b5d14d7e
10 changed files with 857 additions and 39 deletions
+10
View File
@@ -0,0 +1,10 @@
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Cache } from 'cache-manager';
import { HttpAdapterHost } from '@nestjs/core';
export declare class CompressCacheInterceptor implements NestInterceptor {
private cacheManager;
private readonly httpAdapterHost;
constructor(cacheManager: Cache, httpAdapterHost: HttpAdapterHost);
intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>>;
}
+122
View File
@@ -0,0 +1,122 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CompressCacheInterceptor = void 0;
const common_1 = require("@nestjs/common");
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const cache_manager_1 = require("@nestjs/cache-manager");
const zlib = __importStar(require("zlib"));
const util_1 = require("util");
const core_1 = require("@nestjs/core");
const gzip = (0, util_1.promisify)(zlib.gzip);
const gunzip = (0, util_1.promisify)(zlib.gunzip);
const COMPRESSION_THRESHOLD = 100;
let CompressCacheInterceptor = class CompressCacheInterceptor {
constructor(cacheManager, httpAdapterHost) {
this.cacheManager = cacheManager;
this.httpAdapterHost = httpAdapterHost;
}
async intercept(context, next) {
const httpAdapter = this.httpAdapterHost.httpAdapter;
const request = context.getArgByIndex(0);
const response = context.getArgByIndex(1);
if (httpAdapter.getRequestMethod(request) !== 'GET') {
return next.handle();
}
const cacheKey = httpAdapter.getRequestUrl(request);
let cachedData = await this.cacheManager.get(cacheKey);
if (cachedData) {
try {
if (Buffer.isBuffer(cachedData) && cachedData.length > 2 && cachedData[0] === 0x1f && cachedData[1] === 0x8b) {
const decompressed = await gunzip(cachedData);
const jsonString = decompressed.toString('utf8');
httpAdapter.setHeader(response, 'Content-Type', 'application/json');
httpAdapter.setHeader(response, 'X-Cache', 'HIT (Compressed)');
return (0, rxjs_1.of)(JSON.parse(jsonString));
}
else {
const jsonString = cachedData.toString();
httpAdapter.setHeader(response, 'Content-Type', 'application/json');
httpAdapter.setHeader(response, 'X-Cache', 'HIT (Uncompressed)');
return (0, rxjs_1.of)(JSON.parse(jsonString));
}
}
catch (e) {
console.error(`[Cache] Lỗi giải nén dữ liệu cache cho key ${cacheKey}:`, e);
await this.cacheManager.del(cacheKey);
}
}
httpAdapter.setHeader(response, 'X-Cache', 'MISS');
return next.handle().pipe((0, operators_1.tap)(async (data) => {
if (!data)
return;
const jsonString = JSON.stringify(data);
const ttl = 60000;
if (jsonString.length > COMPRESSION_THRESHOLD) {
try {
const compressed = await gzip(Buffer.from(jsonString, 'utf8'));
await this.cacheManager.set(cacheKey, compressed, ttl);
}
catch (e) {
console.error(`[Cache] Lỗi nén dữ liệu cho key ${cacheKey}:`, e);
await this.cacheManager.set(cacheKey, jsonString, ttl);
}
}
else {
await this.cacheManager.set(cacheKey, jsonString, ttl);
}
}));
}
};
exports.CompressCacheInterceptor = CompressCacheInterceptor;
exports.CompressCacheInterceptor = CompressCacheInterceptor = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
__metadata("design:paramtypes", [Object, core_1.HttpAdapterHost])
], CompressCacheInterceptor);
//# sourceMappingURL=compress-cache.interceptor.js.map
@@ -0,0 +1 @@
{"version":3,"file":"compress-cache.interceptor.js","sourceRoot":"","sources":["../../../src/common/compress-cache.interceptor.ts"],"names":[],"mappings":"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"}
+3 -1
View File
@@ -5,12 +5,14 @@ import { PrismaService } from '../prisma/prisma.service';
import { ParticipantRole } from '@prisma/client';
import { Reflector } from '@nestjs/core';
import { CanActivate, ExecutionContext } from '@nestjs/common';
import { Cache } from 'cache-manager';
export declare const ROLES_KEY = "roles";
export declare const Roles: (...roles: ParticipantRole[]) => import("@nestjs/common").CustomDecorator<string>;
export declare class TourRoleGuard implements CanActivate {
private reflector;
private prisma;
constructor(reflector: Reflector, prisma: PrismaService);
private cacheManager;
constructor(reflector: Reflector, prisma: PrismaService, cacheManager: Cache);
canActivate(context: ExecutionContext): Promise<boolean>;
}
export declare class CommentGateway implements OnGatewayConnection {
+139 -17
View File
@@ -55,6 +55,8 @@ const envPath = path.resolve(process.cwd(), '..', '.env');
dotenv.config({ path: envPath });
require("reflect-metadata");
const fs = __importStar(require("fs"));
const zlib = __importStar(require("zlib"));
const util_1 = require("util");
const sharp_1 = __importDefault(require("sharp"));
const core_1 = require("@nestjs/core");
const common_1 = require("@nestjs/common");
@@ -70,7 +72,17 @@ const jwt_auth_guard_1 = require("./auth/jwt-auth.guard");
const jwt_strategy_1 = require("./auth/jwt.strategy");
const core_2 = require("@nestjs/core");
const common_2 = require("@nestjs/common");
const cache_manager_1 = require("@nestjs/cache-manager");
const cache_manager_redis_yet_1 = require("cache-manager-redis-yet");
const compress_cache_interceptor_1 = require("./common/compress-cache.interceptor");
const gzip = (0, util_1.promisify)(zlib.gzip);
const gunzip = (0, util_1.promisify)(zlib.gunzip);
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
const CACHE_TTL = {
DEFAULT: 600000,
RESOURCE_TO_TOUR: 3600000,
USER_ROLE: 300000,
};
async function bootstrap() {
if (!process.env.DATABASE_URL) {
throw new Error('❌ DATABASE_URL không tồn tại trong biến môi trường! Kiểm tra file .env tại: ' + envPath);
@@ -94,9 +106,10 @@ exports.ROLES_KEY = 'roles';
const Roles = (...roles) => (0, common_2.SetMetadata)(exports.ROLES_KEY, roles);
exports.Roles = Roles;
let TourRoleGuard = class TourRoleGuard {
constructor(reflector, prisma) {
constructor(reflector, prisma, cacheManager) {
this.reflector = reflector;
this.prisma = prisma;
this.cacheManager = cacheManager;
}
async canActivate(context) {
const requiredRoles = this.reflector.getAllAndOverride(exports.ROLES_KEY, [
@@ -107,14 +120,73 @@ let TourRoleGuard = class TourRoleGuard {
const rolesToCheck = requiredRoles && requiredRoles.length > 0 ? requiredRoles : defaultRoles;
const request = context.switchToHttp().getRequest();
const user = request.user;
const tourId = request.params.tourId || request.params.id;
let tourId = request.params.tourId;
const resourceId = request.params.id || request.params.legId || request.params.locationId;
if (!tourId && resourceId) {
const resCacheKey = `res-to-tour:${resourceId}`;
const compressedData = await this.cacheManager.get(resCacheKey);
if (compressedData) {
try {
const decompressed = await gunzip(compressedData);
tourId = decompressed.toString();
}
catch (e) {
console.error('Lỗi giải nén cache:', e);
}
}
else {
const isTour = await this.prisma.tour.findUnique({ where: { id: resourceId }, select: { id: true } });
if (isTour) {
tourId = resourceId;
}
else {
const leg = await this.prisma.leg.findUnique({ where: { id: resourceId }, select: { tourId: true } });
if (leg) {
tourId = leg.tourId;
}
else {
const loc = await this.prisma.location.findUnique({
where: { id: resourceId },
include: { leg: { select: { tourId: true } } }
});
if (loc) {
tourId = loc.leg.tourId;
}
else {
const photo = await this.prisma.photo.findUnique({ where: { id: resourceId }, select: { tourId: true } });
if (photo)
tourId = photo.tourId;
}
}
}
if (tourId) {
try {
const compressed = await gzip(Buffer.from(tourId));
await this.cacheManager.set(resCacheKey, compressed, CACHE_TTL.RESOURCE_TO_TOUR);
}
catch (e) {
await this.cacheManager.set(resCacheKey, tourId, CACHE_TTL.RESOURCE_TO_TOUR);
}
}
}
}
if (!user || !tourId) {
if (!resourceId && !request.params.tourId)
return true;
return false;
}
const participation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: user.id } },
});
if (!participation || !rolesToCheck.some(role => participation.role === role)) {
const roleCacheKey = `user-role:${user.id}:${tourId}`;
let role = await this.cacheManager.get(roleCacheKey);
if (!role) {
const participation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: user.id } },
});
if (!participation)
return false;
role = participation.role;
await this.cacheManager.set(roleCacheKey, role, CACHE_TTL.USER_ROLE);
}
if (!rolesToCheck.some(r => role === r)) {
throw new common_1.ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
}
return true;
@@ -123,7 +195,9 @@ let TourRoleGuard = class TourRoleGuard {
exports.TourRoleGuard = TourRoleGuard;
exports.TourRoleGuard = TourRoleGuard = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [core_2.Reflector, prisma_service_1.PrismaService])
__param(2, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
__metadata("design:paramtypes", [core_2.Reflector,
prisma_service_1.PrismaService, Object])
], TourRoleGuard);
let AppController = class AppController {
getHello() {
@@ -212,6 +286,7 @@ let PublicTourController = class PublicTourController {
this.prisma = prisma;
}
async getPublicTourDetails(id) {
console.log(`[PublicTour] Đang truy vấn chi tiết Tour ID: ${id}`);
const tour = await this.prisma.tour.findUnique({
where: { id },
include: {
@@ -240,12 +315,15 @@ let PublicTourController = class PublicTourController {
},
},
});
if (!tour)
if (!tour) {
console.warn(`[PublicTour] Không tìm thấy Tour với ID: ${id} trong Database`);
throw new common_1.NotFoundException(`Không tìm thấy Tour`);
}
return tour;
}
};
__decorate([
(0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor),
(0, common_1.Get)(':id/public'),
__param(0, (0, common_1.Param)('id', common_1.ParseUUIDPipe)),
__metadata("design:type", Function),
@@ -257,8 +335,9 @@ PublicTourController = __decorate([
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], PublicTourController);
let TourController = class TourController {
constructor(prisma) {
constructor(prisma, cacheManager) {
this.prisma = prisma;
this.cacheManager = cacheManager;
}
async createTour(body, req) {
const { title, description, startDate, endDate, adultCount, childCount, childDiscount, tags } = body;
@@ -445,10 +524,32 @@ let TourController = class TourController {
});
}
async deleteTour(id) {
const photos = await this.prisma.photo.findMany({
where: { tourId: id }
const tour = await this.prisma.tour.findUnique({
where: { id },
include: {
participants: true,
photos: true,
legs: {
include: { locations: true }
}
}
});
for (const photo of photos) {
if (!tour)
throw new common_1.NotFoundException('Không tìm thấy tour');
for (const participant of tour.participants) {
await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
}
await this.cacheManager.del(`res-to-tour:${id}`);
for (const leg of tour.legs) {
await this.cacheManager.del(`res-to-tour:${leg.id}`);
for (const loc of leg.locations) {
await this.cacheManager.del(`res-to-tour:${loc.id}`);
}
}
for (const photo of tour.photos) {
await this.cacheManager.del(`res-to-tour:${photo.id}`);
}
for (const photo of tour.photos) {
if (photo.imageUrl) {
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
if (fs.existsSync(displayFilePath)) {
@@ -527,6 +628,7 @@ let TourController = class TourController {
async addMember(tourId, body, req) {
const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'];
const role = validRoles.includes(body.role) ? body.role : 'MEMBER';
await this.cacheManager.del(`user-role:${body.userId}:${tourId}`);
const participation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: body.userId } },
});
@@ -627,6 +729,7 @@ let TourController = class TourController {
if (joinRequest.status !== 'PENDING') {
throw new common_1.BadRequestException('Yêu cầu này đã được xử lý trước đó.');
}
await this.cacheManager.del(`user-role:${joinRequest.userId}:${tourId}`);
const existing = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: joinRequest.userId } },
});
@@ -685,6 +788,7 @@ let TourController = class TourController {
if (!participation) {
throw new common_1.NotFoundException('Thành viên này không có trong tour');
}
await this.cacheManager.del(`user-role:${userId}:${tourId}`);
await this.prisma.tourParticipant.delete({
where: { tourId_userId: { tourId, userId } },
});
@@ -809,6 +913,7 @@ __decorate([
__metadata("design:returntype", Promise)
], TourController.prototype, "deleteTour", null);
__decorate([
(0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.Get)('explore'),
__param(0, (0, common_1.Req)()),
@@ -817,6 +922,7 @@ __decorate([
__metadata("design:returntype", Promise)
], TourController.prototype, "getPublicTours", null);
__decorate([
(0, common_1.UseInterceptors)(compress_cache_interceptor_1.CompressCacheInterceptor),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
(0, common_1.Get)(':id'),
@@ -903,7 +1009,8 @@ __decorate([
], TourController.prototype, "uploadPhotos", null);
TourController = __decorate([
(0, common_1.Controller)('tours'),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
__param(1, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
__metadata("design:paramtypes", [prisma_service_1.PrismaService, Object])
], TourController);
let LocationController = class LocationController {
constructor(prisma) {
@@ -1020,7 +1127,7 @@ __decorate([
LegController = __decorate([
(0, common_1.Controller)('legs'),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], LegController);
function calculateDistance(lat1, lon1, lat2, lon2) {
@@ -1123,6 +1230,7 @@ __decorate([
RoutingController = __decorate([
(0, common_1.Controller)('routing'),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], RoutingController);
let PhotoController = class PhotoController {
@@ -1166,7 +1274,7 @@ __decorate([
PhotoController = __decorate([
(0, common_1.Controller)('photos'),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], PhotoController);
let UserController = class UserController {
@@ -1364,7 +1472,8 @@ __decorate([
__metadata("design:returntype", Promise)
], CommentController.prototype, "getComments", null);
__decorate([
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard),
(0, exports.Roles)(client_1.ParticipantRole.OWNER, client_1.ParticipantRole.MANAGER, client_1.ParticipantRole.MEMBER, client_1.ParticipantRole.MEMBER_NO_FINANCE, client_1.ParticipantRole.VIEWER_ONLY),
(0, common_1.UseGuards)(jwt_auth_guard_1.JwtAuthGuard, TourRoleGuard),
(0, common_1.Post)(':locationId/comments'),
__param(0, (0, common_1.Param)('locationId', common_1.ParseUUIDPipe)),
__param(1, (0, common_1.Body)()),
@@ -1383,6 +1492,15 @@ let AppModule = class AppModule {
AppModule = __decorate([
(0, common_1.Module)({
imports: [
cache_manager_1.CacheModule.registerAsync({
isGlobal: true,
useFactory: async () => ({
store: await (0, cache_manager_redis_yet_1.redisStore)({
url: process.env.REDIS_URL || 'redis://localhost:6379',
ttl: CACHE_TTL.DEFAULT,
}),
}),
}),
jwt_1.JwtModule.register({
secret: process.env.JWT_SECRET || 'super-secret',
signOptions: { expiresIn: '1d' },
@@ -1393,5 +1511,9 @@ AppModule = __decorate([
exports: [prisma_service_1.PrismaService]
})
], AppModule);
bootstrap();
bootstrap().catch(err => {
console.error('💥 Lỗi khởi động Server:');
console.error(err);
process.exit(1);
});
//# sourceMappingURL=main.js.map
+1 -1
View File
File diff suppressed because one or more lines are too long
+4
View File
@@ -19,6 +19,7 @@
"typescript": "^5.5.3"
},
"dependencies": {
"@nestjs/cache-manager": "^3.1.3",
"@nestjs/common": "^11.1.27",
"@nestjs/core": "^11.1.27",
"@nestjs/jwt": "^11.0.2",
@@ -29,10 +30,13 @@
"@prisma/adapter-pg": "^5.16.2",
"@prisma/client": "^5.16.2",
"bcrypt": "^6.0.0",
"cache-manager": "^7.2.8",
"cache-manager-redis-yet": "^5.1.5",
"dotenv": "^17.4.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.12.0",
"redis": "^6.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"sharp": "^0.35.1",
@@ -0,0 +1,90 @@
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, Inject } from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import * as zlib from 'zlib';
import { promisify } from 'util';
import { HttpAdapterHost } from '@nestjs/core';
// Promisify các hàm nén/giải nén
const gzip = promisify(zlib.gzip);
const gunzip = promisify(zlib.gunzip);
// Ngưỡng nén: Chỉ nén nếu chuỗi JSON lớn hơn ngưỡng này (bytes)
// Nén dữ liệu quá nhỏ có thể làm tăng kích thước do overhead của header nén
const COMPRESSION_THRESHOLD = 100;
@Injectable()
export class CompressCacheInterceptor implements NestInterceptor {
constructor(
@Inject(CACHE_MANAGER) private cacheManager: Cache,
private readonly httpAdapterHost: HttpAdapterHost, // Để truy cập request/response
) {}
async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
const httpAdapter = this.httpAdapterHost.httpAdapter;
const request = context.getArgByIndex(0);
const response = context.getArgByIndex(1);
// Chỉ áp dụng cho các request GET
if (httpAdapter.getRequestMethod(request) !== 'GET') {
return next.handle();
}
const cacheKey = httpAdapter.getRequestUrl(request);
let cachedData = await this.cacheManager.get<Buffer>(cacheKey);
if (cachedData) {
try {
// Kiểm tra xem dữ liệu có phải là Buffer và có Gzip header (0x1f 0x8b) không
if (Buffer.isBuffer(cachedData) && cachedData.length > 2 && cachedData[0] === 0x1f && cachedData[1] === 0x8b) {
const decompressed = await gunzip(cachedData);
const jsonString = decompressed.toString('utf8');
httpAdapter.setHeader(response, 'Content-Type', 'application/json');
httpAdapter.setHeader(response, 'X-Cache', 'HIT (Compressed)');
return of(JSON.parse(jsonString));
} else {
// Dữ liệu không nén (lưu dưới dạng string hoặc buffer thường)
const jsonString = cachedData.toString();
httpAdapter.setHeader(response, 'Content-Type', 'application/json');
httpAdapter.setHeader(response, 'X-Cache', 'HIT (Uncompressed)');
return of(JSON.parse(jsonString));
}
} catch (e) {
console.error(`[Cache] Lỗi giải nén dữ liệu cache cho key ${cacheKey}:`, e);
// Nếu giải nén lỗi, coi như cache miss và xóa cache bị lỗi
await this.cacheManager.del(cacheKey);
}
}
// Cache miss hoặc giải nén lỗi, tiếp tục xử lý request
// Đặt header MISS ngay lập tức trước khi chạy logic Controller
httpAdapter.setHeader(response, 'X-Cache', 'MISS');
return next.handle().pipe(
tap(async (data) => { // Sử dụng tap để thực hiện side effect (lưu cache) mà không thay đổi dữ liệu gốc
if (!data) return;
const jsonString = JSON.stringify(data);
const ttl = 60000; // TTL mặc định 1 phút (có thể cấu hình từ CACHE_TTL.DEFAULT)
if (jsonString.length > COMPRESSION_THRESHOLD) {
try {
const compressed = await gzip(Buffer.from(jsonString, 'utf8'));
await this.cacheManager.set(cacheKey, compressed, ttl);
// Không setHeader ở đây vì response có thể đã gửi xong
} catch (e) {
console.error(`[Cache] Lỗi nén dữ liệu cho key ${cacheKey}:`, e);
// Nếu nén lỗi, lưu dữ liệu không nén làm fallback
await this.cacheManager.set(cacheKey, jsonString, ttl);
}
} else {
// Dữ liệu quá nhỏ, lưu không nén
await this.cacheManager.set(cacheKey, jsonString, ttl);
}
})
);
}
}
+167 -20
View File
@@ -7,9 +7,11 @@ dotenv.config({ path: envPath });
import 'reflect-metadata';
import * as fs from 'fs';
import * as zlib from 'zlib';
import { promisify } from 'util';
import sharp from 'sharp';
import { NestFactory } from '@nestjs/core';
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, Query, ForbiddenException, Injectable, UseInterceptors, UploadedFiles } from '@nestjs/common';
import { Module, Controller, Get, Post, Body, Param, Patch, Delete, ParseUUIDPipe, NotFoundException, BadRequestException, UnauthorizedException, UseGuards, Req, Query, ForbiddenException, Injectable, UseInterceptors, UploadedFiles, Inject } from '@nestjs/common';
import { NestExpressApplication } from '@nestjs/platform-express';
import { FilesInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
@@ -24,10 +26,26 @@ import { JwtAuthGuard } from './auth/jwt-auth.guard';
import { JwtStrategy } from './auth/jwt.strategy';
import { Reflector } from '@nestjs/core';
import { SetMetadata, CanActivate, ExecutionContext } from '@nestjs/common';
import { CacheModule, CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { redisStore } from 'cache-manager-redis-yet';
import { HttpAdapterHost } from '@nestjs/core';
import { CompressCacheInterceptor } from './common/compress-cache.interceptor';
// Promisify các hàm nén để sử dụng async/await
const gzip = promisify(zlib.gzip);
const gunzip = promisify(zlib.gunzip);
// Khai báo vị trí thư mục upload cụ thể
const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
// Cấu hình TTL (mili giây) cho từng loại dữ liệu
const CACHE_TTL = {
DEFAULT: 600000, // 10 phút mặc định
RESOURCE_TO_TOUR: 3600000, // 1 giờ cho ánh xạ tài nguyên -> tour
USER_ROLE: 300000, // 5 phút cho quyền hạn người dùng
};
async function bootstrap() {
if (!process.env.DATABASE_URL) {
throw new Error('❌ DATABASE_URL không tồn tại trong biến môi trường! Kiểm tra file .env tại: ' + envPath);
@@ -66,7 +84,11 @@ export const Roles = (...roles: ParticipantRole[]) => SetMetadata(ROLES_KEY, rol
// This guard checks if the user is a participant of the tour and has one of the required roles.
@Injectable()
export class TourRoleGuard implements CanActivate {
constructor(private reflector: Reflector, private prisma: PrismaService) {}
constructor(
private reflector: Reflector,
private prisma: PrismaService,
@Inject(CACHE_MANAGER) private cacheManager: Cache
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<ParticipantRole[]>(ROLES_KEY, [
@@ -80,18 +102,80 @@ export class TourRoleGuard implements CanActivate {
const request = context.switchToHttp().getRequest();
const user = request.user; // User object from JwtAuthGuard
// Kiểm tra cả 'tourId' và 'id' để tương thích với các route khác nhau
const tourId = request.params.tourId || request.params.id;
if (!user || !tourId) {
return false; // User or tourId not available
let tourId = request.params.tourId;
const resourceId = request.params.id || request.params.legId || request.params.locationId;
// Nếu không có tourId trực tiếp, tìm tourId thông qua các tài nguyên liên quan
if (!tourId && resourceId) {
const resCacheKey = `res-to-tour:${resourceId}`;
const compressedData = await this.cacheManager.get<Buffer>(resCacheKey);
if (compressedData) {
try {
const decompressed = await gunzip(compressedData);
tourId = decompressed.toString();
} catch (e) {
console.error('Lỗi giải nén cache:', e);
}
} else {
// Thử xem resourceId có phải là tourId không
const isTour = await this.prisma.tour.findUnique({ where: { id: resourceId }, select: { id: true } });
if (isTour) {
tourId = resourceId;
} else {
// Thử xem resourceId có phải là legId không
const leg = await this.prisma.leg.findUnique({ where: { id: resourceId }, select: { tourId: true } });
if (leg) {
tourId = leg.tourId;
} else {
// Thử xem resourceId có phải là locationId không
const loc = await this.prisma.location.findUnique({
where: { id: resourceId },
include: { leg: { select: { tourId: true } } }
});
if (loc) {
tourId = loc.leg.tourId;
} else {
// Thử xem resourceId có phải là photoId không
const photo = await this.prisma.photo.findUnique({ where: { id: resourceId }, select: { tourId: true } });
if (photo) tourId = photo.tourId;
}
}
}
// Cache ánh xạ tài nguyên -> tour trong 1 giờ để giảm tải query ngược
if (tourId) {
try {
const compressed = await gzip(Buffer.from(tourId));
await this.cacheManager.set(resCacheKey, compressed, CACHE_TTL.RESOURCE_TO_TOUR);
} catch (e) {
await this.cacheManager.set(resCacheKey, tourId, CACHE_TTL.RESOURCE_TO_TOUR);
}
}
}
}
const participation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: user.id } },
});
if (!user || !tourId) {
// Nếu đây là các route công khai hoặc không liên quan đến Tour, cho phép đi qua
// nhưng ở đây chúng ta đang áp dụng guard cho các route cần phân quyền Tour
if (!resourceId && !request.params.tourId) return true;
return false;
}
if (!participation || !rolesToCheck.some(role => participation.role === role)) {
// Cache vai trò người dùng trong tour (5 phút)
const roleCacheKey = `user-role:${user.id}:${tourId}`;
let role = await this.cacheManager.get<ParticipantRole>(roleCacheKey);
if (!role) {
const participation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: user.id } },
});
if (!participation) return false;
role = participation.role;
await this.cacheManager.set(roleCacheKey, role, CACHE_TTL.USER_ROLE);
}
if (!rolesToCheck.some(r => role === r)) {
throw new ForbiddenException('Bạn không có quyền thực hiện hành động này trong tour này.');
}
return true;
@@ -167,8 +251,10 @@ class AuthController {
class PublicTourController {
constructor(private prisma: PrismaService) {}
@UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
@Get(':id/public')
async getPublicTourDetails(@Param('id', ParseUUIDPipe) id: string) {
console.log(`[PublicTour] Đang truy vấn chi tiết Tour ID: ${id}`);
const tour = await this.prisma.tour.findUnique({
where: { id },
include: {
@@ -198,14 +284,20 @@ class PublicTourController {
},
});
if (!tour) throw new NotFoundException(`Không tìm thấy Tour`);
if (!tour) {
console.warn(`[PublicTour] Không tìm thấy Tour với ID: ${id} trong Database`);
throw new NotFoundException(`Không tìm thấy Tour`);
}
return tour;
}
}
@Controller('tours')
class TourController {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
@Inject(CACHE_MANAGER) private cacheManager: Cache
) {}
@UseGuards(JwtAuthGuard)
@Post()
@@ -449,13 +541,43 @@ class TourController {
@UseGuards(JwtAuthGuard, TourRoleGuard)
@Delete(':id')
async deleteTour(@Param('id', ParseUUIDPipe) id: string) {
// 1. Lấy danh sách ảnh thuộc tour để có đường dn file 2K
const photos = await this.prisma.photo.findMany({
where: { tourId: id }
// 1. Lấy thông tin chi tiết Tour cùng các tài nguyên liên quan để dn dẹp cache và file
const tour = await this.prisma.tour.findUnique({
where: { id },
include: {
participants: true,
photos: true,
legs: {
include: { locations: true }
}
}
});
if (!tour) throw new NotFoundException('Không tìm thấy tour');
// --- BẮT ĐẦU DỌN DẸP CACHE ---
// a. Xóa cache vai trò của tất cả thành viên trong tour này
for (const participant of tour.participants) {
await this.cacheManager.del(`user-role:${participant.userId}:${id}`);
}
// b. Xóa cache mapping tài nguyên (Leg, Location, Photo) về Tour này
await this.cacheManager.del(`res-to-tour:${id}`); // Bản thân tour
for (const leg of tour.legs) {
await this.cacheManager.del(`res-to-tour:${leg.id}`);
for (const loc of leg.locations) {
await this.cacheManager.del(`res-to-tour:${loc.id}`);
}
}
for (const photo of tour.photos) {
await this.cacheManager.del(`res-to-tour:${photo.id}`);
}
// --- KẾT THÚC DỌN DẸP CACHE ---
// 2. Xóa các file vật lý (ảnh 2K) trong thư mục uploads/tours
for (const photo of photos) {
for (const photo of tour.photos) {
if (photo.imageUrl) {
const displayFilePath = path.join(process.cwd(), photo.imageUrl.replace(/^\//, ''));
if (fs.existsSync(displayFilePath)) {
@@ -477,6 +599,7 @@ class TourController {
}
// getPublicTours does not need TourRoleGuard as it's for any logged-in user to see their tours
@UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
@UseGuards(JwtAuthGuard)
@Get('explore')
async getPublicTours(@Req() req: any) {
@@ -507,6 +630,7 @@ class TourController {
});
}
@UseInterceptors(CompressCacheInterceptor) // Áp dụng Interceptor nén và cache
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY) // Any participant can view tour details
@UseGuards(JwtAuthGuard, TourRoleGuard)
@Get(':id')
@@ -551,6 +675,9 @@ class TourController {
const validRoles = ['OWNER', 'MANAGER', 'MEMBER', 'MEMBER_NO_FINANCE', 'VIEWER_ONLY'] as const;
const role = validRoles.includes(body.role as any) ? body.role as 'OWNER' | 'MANAGER' | 'MEMBER' | 'MEMBER_NO_FINANCE' | 'VIEWER_ONLY' : 'MEMBER';
// Xóa cache khi thay đổi quyền hạn hoặc thêm thành viên mới
await this.cacheManager.del(`user-role:${body.userId}:${tourId}`);
const participation = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: body.userId } },
});
@@ -672,6 +799,8 @@ class TourController {
throw new BadRequestException('Yêu cầu này đã được xử lý trước đó.');
}
await this.cacheManager.del(`user-role:${joinRequest.userId}:${tourId}`);
const existing = await this.prisma.tourParticipant.findUnique({
where: { tourId_userId: { tourId, userId: joinRequest.userId } },
});
@@ -743,6 +872,9 @@ class TourController {
if (!participation) {
throw new NotFoundException('Thành viên này không có trong tour');
}
await this.cacheManager.del(`user-role:${userId}:${tourId}`);
await this.prisma.tourParticipant.delete({
where: { tourId_userId: { tourId, userId } },
});
@@ -862,7 +994,7 @@ class LocationController {
@Controller('legs')
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Leg operations
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, TourRoleGuard)
class LegController {
constructor(private prisma: PrismaService) {}
@@ -910,6 +1042,7 @@ function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: numbe
@Controller('routing')
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER) // Default roles for Routing operations
@UseGuards(JwtAuthGuard, TourRoleGuard)
class RoutingController {
constructor(private prisma: PrismaService) {}
@@ -1024,7 +1157,7 @@ class RoutingController {
@Controller('photos')
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE) // All members can delete their own photos
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, TourRoleGuard)
class PhotoController {
constructor(private prisma: PrismaService) {}
@@ -1204,7 +1337,8 @@ class CommentController {
});
}
@UseGuards(JwtAuthGuard)
@Roles(ParticipantRole.OWNER, ParticipantRole.MANAGER, ParticipantRole.MEMBER, ParticipantRole.MEMBER_NO_FINANCE, ParticipantRole.VIEWER_ONLY)
@UseGuards(JwtAuthGuard, TourRoleGuard)
@Post(':locationId/comments')
async addComment(
@Param('locationId', ParseUUIDPipe) locationId: string,
@@ -1239,6 +1373,15 @@ class CommentController {
@Module({
imports: [
CacheModule.registerAsync({
isGlobal: true,
useFactory: async () => ({
store: await redisStore({
url: process.env.REDIS_URL || 'redis://localhost:6379',
ttl: CACHE_TTL.DEFAULT, // Cấu hình TTL mặc định cho toàn bộ store
}),
}),
}),
JwtModule.register({
secret: process.env.JWT_SECRET || 'super-secret',
signOptions: { expiresIn: '1d' },
@@ -1251,4 +1394,8 @@ class CommentController {
class AppModule {}
bootstrap();
bootstrap().catch(err => {
console.error('💥 Lỗi khởi động Server:');
console.error(err);
process.exit(1);
});
+320
View File
@@ -22,6 +22,7 @@
"backend": {
"version": "0.0.1",
"dependencies": {
"@nestjs/cache-manager": "^3.1.3",
"@nestjs/common": "^11.1.27",
"@nestjs/core": "^11.1.27",
"@nestjs/jwt": "^11.0.2",
@@ -32,10 +33,13 @@
"@prisma/adapter-pg": "^5.16.2",
"@prisma/client": "^5.16.2",
"bcrypt": "^6.0.0",
"cache-manager": "^7.2.8",
"cache-manager-redis-yet": "^5.1.5",
"dotenv": "^17.4.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.12.0",
"redis": "^6.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"sharp": "^0.35.1",
@@ -405,6 +409,16 @@
"url": "https://github.com/sponsors/Borewit"
}
},
"node_modules/@cacheable/utils": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz",
"integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==",
"license": "MIT",
"dependencies": {
"hashery": "^1.5.1",
"keyv": "^5.6.0"
}
},
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
@@ -1878,6 +1892,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@keyv/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
"license": "MIT"
},
"node_modules/@lukeed/csprng": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
@@ -1906,6 +1926,19 @@
"@emnapi/runtime": "^1.7.1"
}
},
"node_modules/@nestjs/cache-manager": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.1.3.tgz",
"integrity": "sha512-HMtiOfHz75NZX7mJn1VnZGLSachVI04TnUc5wvEogIaKwk5BDQHgtP5htxreizjv7oxKalJbuTyxtiF6bE+bgQ==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0",
"@nestjs/core": "^9.0.0 || ^10.0.0 || ^11.0.0",
"cache-manager": ">=6",
"keyv": ">=5",
"rxjs": "^7.8.1"
}
},
"node_modules/@nestjs/cli": {
"version": "11.0.23",
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.23.tgz",
@@ -2218,6 +2251,65 @@
"react-dom": "^18.0.0"
}
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/graph": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz",
"integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz",
"integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz",
"integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz",
"integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
@@ -3617,6 +3709,74 @@
"node": ">= 0.8"
}
},
"node_modules/cache-manager": {
"version": "7.2.8",
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-7.2.8.tgz",
"integrity": "sha512-0HDaDLBBY/maa/LmUVAr70XUOwsiQD+jyzCBjmUErYZUKdMS9dT59PqW59PpVqfGM7ve6H0J6307JTpkCYefHQ==",
"license": "MIT",
"dependencies": {
"@cacheable/utils": "^2.3.3",
"keyv": "^5.5.5"
}
},
"node_modules/cache-manager-redis-yet": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/cache-manager-redis-yet/-/cache-manager-redis-yet-5.1.5.tgz",
"integrity": "sha512-NYDxrWBoLXxxVPw4JuBriJW0f45+BVOAsgLiozRo4GoJQyoKPbueQWYStWqmO73/AeHJeWrV7Hzvk6vhCGHlqA==",
"deprecated": "With cache-manager v6 we now are using Keyv",
"license": "MIT",
"dependencies": {
"@redis/bloom": "^1.2.0",
"@redis/client": "^1.6.0",
"@redis/graph": "^1.1.1",
"@redis/json": "^1.0.7",
"@redis/search": "^1.2.0",
"@redis/time-series": "^1.1.0",
"cache-manager": "^5.7.6",
"redis": "^4.7.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/cache-manager-redis-yet/node_modules/cache-manager": {
"version": "5.7.6",
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz",
"integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.1",
"lodash.clonedeep": "^4.5.0",
"lru-cache": "^10.2.2",
"promise-coalesce": "^1.1.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/cache-manager-redis-yet/node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/cache-manager-redis-yet/node_modules/redis": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz",
"integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==",
"license": "MIT",
"workspaces": [
"./packages/*"
],
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.6.1",
"@redis/graph": "1.1.1",
"@redis/json": "1.0.7",
"@redis/search": "1.2.0",
"@redis/time-series": "1.1.0"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -3837,6 +3997,15 @@
"node": ">=0.8"
}
},
"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/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -4496,6 +4665,12 @@
"node": ">= 0.6"
}
},
"node_modules/eventemitter3": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -4808,6 +4983,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -4960,6 +5144,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hashery": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
"integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
"license": "MIT",
"dependencies": {
"hookified": "^1.15.0"
},
"engines": {
"node": ">=20"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
@@ -4972,6 +5168,12 @@
"node": ">= 0.4"
}
},
"node_modules/hookified": {
"version": "1.15.1",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
"integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
"license": "MIT"
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
@@ -5312,6 +5514,15 @@
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
"license": "MIT",
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
@@ -5635,6 +5846,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
"license": "MIT"
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -6431,6 +6648,15 @@
"node": ">=0.10.0"
}
},
"node_modules/promise-coalesce": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.5.0.tgz",
"integrity": "sha512-cTJ30U+ur1LD7pMPyQxiKIwxjtAjLsyU7ivRhVWZrX9BNIXtf78pc37vSMc8Vikx7DVzEKNk2SEJ5KWUpSG2ig==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=16"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -6585,6 +6811,94 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/redis": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redis/-/redis-6.0.0.tgz",
"integrity": "sha512-n9Thfc39OXleEoPT2k5gwKsqY+HfCww3YS71ofcr9KKbkn89bpjU9dToIlD+JRdM3/GYQkwMtVgTxLyed+LptQ==",
"license": "MIT",
"dependencies": {
"@redis/bloom": "6.0.0",
"@redis/client": "6.0.0",
"@redis/json": "6.0.0",
"@redis/search": "6.0.0",
"@redis/time-series": "6.0.0"
},
"engines": {
"node": ">= 20.0.0"
}
},
"node_modules/redis/node_modules/@redis/bloom": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-6.0.0.tgz",
"integrity": "sha512-P0n5NkV9IIdT6nYXOfMHG83sho8pE7Nay7yw27wOGVLv4DthgvzebpGz6m7VuMTizeJmw3LPw2Xek5wFUhGpVw==",
"license": "MIT",
"engines": {
"node": ">= 20.0.0"
},
"peerDependencies": {
"@redis/client": "^6.0.0"
}
},
"node_modules/redis/node_modules/@redis/client": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-6.0.0.tgz",
"integrity": "sha512-NS4iIT25r24sAjNQ2nSRdCW5jPJoV0rxkBee27oTeR+RXaOu89cjIsrww5rPBaYVGVdL1QCx9uz9141gZiSKdQ==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2"
},
"engines": {
"node": ">= 20.0.0"
},
"peerDependencies": {
"@node-rs/xxhash": "^1.1.0",
"@opentelemetry/api": ">=1 <2"
},
"peerDependenciesMeta": {
"@node-rs/xxhash": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
}
}
},
"node_modules/redis/node_modules/@redis/json": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-6.0.0.tgz",
"integrity": "sha512-F+eqFfgPcy57Zs1KW7UtLnBtRk6lxAUIoe7dyZerpm6e+ssYXG/dWJrbrHFYs0b7tt6QBtYpVuukBuM9XqhUAg==",
"license": "MIT",
"engines": {
"node": ">= 20.0.0"
},
"peerDependencies": {
"@redis/client": "^6.0.0"
}
},
"node_modules/redis/node_modules/@redis/search": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-6.0.0.tgz",
"integrity": "sha512-VHuCJ2W0YWFixGZh/l//8JiyOsD4gN+NhjdRAGIoUe0UQ4mtq1NyY2ZJ973XT+vYhaU21XdK8r8oNrd5n7wbzQ==",
"license": "MIT",
"engines": {
"node": ">= 20.0.0"
},
"peerDependencies": {
"@redis/client": "^6.0.0"
}
},
"node_modules/redis/node_modules/@redis/time-series": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-6.0.0.tgz",
"integrity": "sha512-QWhkYsg+3lhBrBf+cbzybtV8LQcSrk7iXIgTaGU+pHNFTkql7TpVRE24ROS6M2ybVIV6O/zxTqfxgxxYiqyw0Q==",
"license": "MIT",
"engines": {
"node": ">= 20.0.0"
},
"peerDependencies": {
"@redis/client": "^6.0.0"
}
},
"node_modules/reflect-metadata": {
"version": "0.1.14",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz",
@@ -8092,6 +8406,12 @@
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",