diff --git a/.env.example b/.env.example index f8fd148..578fca2 100644 --- a/.env.example +++ b/.env.example @@ -18,4 +18,8 @@ API_URL= FRONTEND_URL= # Application Environment -NODE_ENV= \ No newline at end of file +NODE_ENV= + +# Redis Variables +REDIS_HOST= +REDIS_PORT= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9d23201..f207a31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", @@ -16,6 +17,8 @@ "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", "bcryptjs": "^2.4.3", + "cache-manager": "^5.7.6", + "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cors": "^2.8.5", @@ -1707,6 +1710,18 @@ "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", "license": "MIT" }, + "node_modules/@nestjs/cache-manager": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz", + "integrity": "sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.2.tgz", @@ -2139,6 +2154,71 @@ "url": "https://opencollective.com/unts" } }, + "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.0", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", + "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@redis/client/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/@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/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -3554,6 +3634,58 @@ "node": ">= 0.8" } }, + "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-store": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cache-manager-redis-store/-/cache-manager-redis-store-3.0.1.tgz", + "integrity": "sha512-o560kw+dFqusC9lQJhcm6L2F2fMKobJ5af+FoR2PdnMVdpQ3f3Bz6qzvObTGyvoazQJxjQNWgMQeChP4vRTuXQ==", + "license": "MIT", + "dependencies": { + "redis": "^4.3.1" + }, + "engines": { + "node": ">= 16.18.0" + } + }, + "node_modules/cache-manager-redis-yet": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/cache-manager-redis-yet/-/cache-manager-redis-yet-5.1.4.tgz", + "integrity": "sha512-2mXZjo+txfH2m+mSTHTITNq8c5SssU2nP7NutzrocO3Mw/SbjHcDo+mriI3ZuR63ov/oUUIaF9iF+MzDqVzMoQ==", + "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/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/call-bind": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", @@ -3907,6 +4039,15 @@ "node": ">=0.10.0" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4831,6 +4972,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -5367,21 +5514,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5400,6 +5532,15 @@ "is-property": "^1.0.2" } }, + "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/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7139,6 +7280,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "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", @@ -8341,6 +8488,15 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, + "node_modules/promise-coalesce": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz", + "integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16" + } + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -8537,6 +8693,23 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/redis": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", + "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "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" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", diff --git a/package.json b/package.json index cac3835..46ab03f 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "db:reset": "npm run db:drop && npm run db:create && npm run db:seed" }, "dependencies": { + "@nestjs/cache-manager": "^2.2.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", @@ -36,6 +37,8 @@ "@nestjs/swagger": "^7.4.0", "@nestjs/typeorm": "^10.0.2", "bcryptjs": "^2.4.3", + "cache-manager": "^5.7.6", + "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cors": "^2.8.5", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index da77fef..9302b09 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -6,9 +6,14 @@ import { CourseModule } from '../modules/course/infra/modules/course.module'; import { UserActivitiesAnsweredModule } from '../modules/user-activities-answered/infra/modules/user-activities-answered.module'; import { UserCoursesConcludedModule } from '../modules/user-courses-concluded/infra/modules/user-courses-concluded.module'; import { ActivityModule } from '../modules/activity/infra/modules/activity.module'; +import { CacheModule } from '@nestjs/cache-manager'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { RedisOptions } from '../shared/config/redis.config'; @Module({ imports: [ + ConfigModule.forRoot({ isGlobal: true }), + CacheModule.registerAsync(RedisOptions), DatabaseModule, UserModule, UserScoreModule, diff --git a/src/modules/user/infra/services/user.service.ts b/src/modules/user/infra/services/user.service.ts index 04217a2..be6a066 100644 --- a/src/modules/user/infra/services/user.service.ts +++ b/src/modules/user/infra/services/user.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { UserRepository } from '../db/repositories/user.repository'; import { CreateUserRequestDTO, @@ -27,10 +27,13 @@ import { EditProfileRequestDTO, EditProfileResponseDTO, } from '../../domain/dtos/requests/EditProfile.request.dto'; +import { Cache } from '@nestjs/cache-manager'; @Injectable() export class UserService { constructor( + @Inject('CACHE_MANAGER') + private cacheManager: Cache, private readonly userRepository: UserRepository, private readonly userScoreRepository: UserScoreRepository, private readonly userCoursesConcludedRepository: UserCourseConcludedRepository, @@ -154,12 +157,25 @@ export class UserService { async homeData( user_id: number, ): Promise { + const cachedData = await this.cacheManager.get(`homedata:${user_id}`); + + if (cachedData) return cachedData + const user = await this.userRepository.findById(user_id); if (!user) throw new UserNotFoundException(); const userScore = await this.userScoreRepository.findByUserId(user_id); + await this.cacheManager.set(`homedata:${user_id}`, { + user: { + id: user.id_user, + username: user.username, + score: userScore.total_score, + role: user.role, + } + }) + return { user: { id: user.id_user, @@ -174,6 +190,10 @@ export class UserService { async getProfile( id: number, ): Promise { + const cachedData = await this.cacheManager.get(`profile:${id}`); + + if (cachedData) return cachedData; + const user = await this.userRepository.findById(id); if (!user) { @@ -185,6 +205,14 @@ export class UserService { const userConcludedCourses = await this.userCoursesConcludedRepository.countUserConcludedCourses(id); + await this.cacheManager.set(`profile:${id}`, { + username: user.username, + email: user.email, + user_score: userScore.total_score, + courses_completed: userConcludedCourses, + member_since: String(user.created_at), + }) + return { username: user.username, email: user.email, diff --git a/src/shared/config/app.config.ts b/src/shared/config/app.config.ts index d26bb00..952f528 100644 --- a/src/shared/config/app.config.ts +++ b/src/shared/config/app.config.ts @@ -24,6 +24,9 @@ const appConfigurationsSchema = z.object({ .enum(['development', 'production', 'test', 'local']) .default('development'), SSL: z.boolean().default(false), + + REDIS_HOST: z.string().min(1), + REDIS_PORT: z.number(), }); let appConfigurations: z.infer = {}; @@ -53,6 +56,8 @@ try { JWT_KEY: process.env.JWT_KEY, API_PORT: parseInt(process.env.API_PORT), SSL: process.env.NODE_ENV === 'production' ? true : false, + REDIS_HOST: process.env.REDIS_HOST, + REDIS_PORT: parseInt(process.env.REDIS_PORT), }); } catch (error) { if (error instanceof ZodError) { diff --git a/src/shared/config/redis.config.ts b/src/shared/config/redis.config.ts new file mode 100644 index 0000000..1816a4b --- /dev/null +++ b/src/shared/config/redis.config.ts @@ -0,0 +1,22 @@ +import { CacheModuleAsyncOptions } from "@nestjs/cache-manager"; +import { ConfigModule } from "@nestjs/config"; +import { redisStore } from "cache-manager-redis-store"; +import { appConfigurations } from "./app.config"; + +export const RedisOptions: CacheModuleAsyncOptions = { + isGlobal: true, + imports: [ConfigModule], + useFactory: async () => { + const store = await redisStore({ + ttl: 60 * 10000, + socket: { + host: appConfigurations.REDIS_HOST, + port: appConfigurations.REDIS_PORT, + }, + }); + return { + store: () => store, + }; + }, + inject: [], + }; \ No newline at end of file