From 74cd121b2b4b77022c9ee94d20bc655cd455a084 Mon Sep 17 00:00:00 2001 From: feyishola Date: Mon, 24 Feb 2025 20:11:46 +0100 Subject: [PATCH] implemented redisdb --- backend/package-lock.json | 320 ++++++++++++++++++ backend/package.json | 3 + backend/src/app.module.ts | 14 +- .../leaderboard/leaderboard-entry.entity.ts | 16 + .../src/leaderboard/leaderboard.controller.ts | 2 +- .../providers/leaderboard.service.ts | 57 +++- backend/src/redis/redis.module.ts | 9 + backend/src/redis/redis.service.spec.ts | 18 + backend/src/redis/redis.service.ts | 31 ++ backend/src/songs/songs.module.ts | 3 +- backend/src/songs/songs.service.ts | 84 ++++- 11 files changed, 536 insertions(+), 21 deletions(-) create mode 100644 backend/src/leaderboard/leaderboard-entry.entity.ts create mode 100644 backend/src/redis/redis.module.ts create mode 100644 backend/src/redis/redis.service.spec.ts create mode 100644 backend/src/redis/redis.service.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index d9c4f7962..213f42a63 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@nestjs/cache-manager": "^3.0.0", "@nestjs/common": "^11.0.5", "@nestjs/config": "^4.0.0", "@nestjs/core": "^11.0.5", @@ -22,8 +23,10 @@ "@nestjs/websockets": "^11.0.10", "@types/bcrypt": "^5.0.2", "bcrypt": "^5.1.1", + "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "ioredis": "^5.5.0", "joi": "^17.13.3", "pg": "^8.13.1", "reflect-metadata": "^0.2.2", @@ -1322,6 +1325,12 @@ } } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1956,6 +1965,41 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/serialize": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.3.tgz", + "integrity": "sha512-qnEovoOp5Np2JDGonIDL6Ayihw0RhnRh6vxPuHo4RDn1UOzwEo4AeIfpL6UGIrsceWrCMiVPgwRjbHu4vYFc3g==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer": "^6.0.3" + } + }, + "node_modules/@keyv/serialize/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -1991,6 +2035,18 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, + "node_modules/@nestjs/cache-manager": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-3.0.0.tgz", + "integrity": "sha512-csKvxHSQWfC0OiDo0bNEhLqrmYDopHEvRyC81MxV9xFj1AO+rOKocpHa4M1ZGH//6uKFIPGN9oiR0mvZY77APA==", + "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", + "rxjs": "^7.8.1" + } + }, "node_modules/@nestjs/cli": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.4.tgz", @@ -2168,6 +2224,66 @@ } } }, + "node_modules/@nestjs/microservices": { + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-11.0.10.tgz", + "integrity": "sha512-G2R+kyc8qjQsNyP3fyQFAwv6wZ1qdKQ1xmvb1o9EZdSWYpwfWDdBYuZGT4zXw/iRiCMCYk8JcjMbPVD+ezrCmg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.8.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "amqp-connection-manager": "*", + "amqplib": "*", + "cache-manager": "*", + "ioredis": "*", + "kafkajs": "*", + "mqtt": "*", + "nats": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + }, + "amqp-connection-manager": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "cache-manager": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "kafkajs": { + "optional": true + }, + "mqtt": { + "optional": true + }, + "nats": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "11.0.10", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.10.tgz", @@ -2412,6 +2528,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/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -3898,6 +4079,38 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-6.4.0.tgz", + "integrity": "sha512-eUmPyVqQYzWCt7hx1QrYzQ7oC3MGKM1etxxe8zuq1o7IB4NzdBeWcUGDSWYahaI8fkd538SEZRGadyZWQfvOzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "keyv": "^5.2.3" + } + }, + "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/node_modules/keyv": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz", + "integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@keyv/serialize": "^1.0.2" + } + }, "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", @@ -4287,6 +4500,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/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4654,6 +4876,15 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "license": "MIT" }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -6098,6 +6329,15 @@ "node": ">=8" } }, + "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", @@ -6512,6 +6752,30 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", + "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -7775,12 +8039,24 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -9174,6 +9450,44 @@ "url": "https://paulmillr.com/funding/" } }, + "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/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -9966,6 +10280,12 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 5bc645b56..ab3064ebd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,6 +20,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/cache-manager": "^3.0.0", "@nestjs/common": "^11.0.5", "@nestjs/config": "^4.0.0", "@nestjs/core": "^11.0.5", @@ -33,8 +34,10 @@ "@nestjs/websockets": "^11.0.10", "@types/bcrypt": "^5.0.2", "bcrypt": "^5.1.1", + "cache-manager-redis-store": "^3.0.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "ioredis": "^5.5.0", "joi": "^17.13.3", "pg": "^8.13.1", "reflect-metadata": "^0.2.2", diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 91ffa5e54..5eb85f196 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; @@ -25,7 +25,9 @@ import { GameGateway } from './websocket-game comms/providers/gamegateway'; import { GameModule } from './websocket-game comms/game.module'; import { AchievementModule } from './achievement/achievement.module'; import { SocialModule } from './social/social.module'; -import { AchievementModule } from './achievement/achievement.module'; +// import { AchievementModule } from './achievement/achievement.module'; +import { CacheModule } from '@nestjs/cache-manager'; +import * as redisStore from 'cache-manager-redis-store'; @Module({ imports: [ @@ -45,6 +47,14 @@ import { AchievementModule } from './achievement/achievement.module'; autoLoadEntities: true, synchronize: process.env.NODE_ENV === 'development', }), + CacheModule.register({ + store: redisStore, + socket: { + host: process.env.REDIS_HOST || 'localhost', + port: Number(process.env.REDIS_PORT) || 6379, + }, + ttl: 3600, + }), SongsModule, ChatRoomModule, ScoringModule, diff --git a/backend/src/leaderboard/leaderboard-entry.entity.ts b/backend/src/leaderboard/leaderboard-entry.entity.ts new file mode 100644 index 000000000..1caec2d9f --- /dev/null +++ b/backend/src/leaderboard/leaderboard-entry.entity.ts @@ -0,0 +1,16 @@ +import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; + +@Entity('leaderboard_entries') +export class LeaderboardEntry { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ unique: true }) + playerId: string; + + @Column() + playerName: string; + + @Column({ type: 'int', default: 0 }) + score: number; +} diff --git a/backend/src/leaderboard/leaderboard.controller.ts b/backend/src/leaderboard/leaderboard.controller.ts index efb0848fc..d6e8aee5c 100644 --- a/backend/src/leaderboard/leaderboard.controller.ts +++ b/backend/src/leaderboard/leaderboard.controller.ts @@ -29,6 +29,6 @@ export class LeaderboardController { @ApiResponse({ status: 200, description: 'Player rank successfully retrieved' }) @ApiResponse({ status: 404, description: 'Player not found' }) getPlayerRank() { - return this.leaderboardService.getPlayerRank(); + return this.leaderboardService.getPlayerRank('player123'); } } \ No newline at end of file diff --git a/backend/src/leaderboard/providers/leaderboard.service.ts b/backend/src/leaderboard/providers/leaderboard.service.ts index 747041b52..4e490dfc9 100644 --- a/backend/src/leaderboard/providers/leaderboard.service.ts +++ b/backend/src/leaderboard/providers/leaderboard.service.ts @@ -1,15 +1,58 @@ +// import { Injectable } from '@nestjs/common'; + +// // Service responsible for leaderboard operations. +// @Injectable() +// export class LeaderboardService { +// // Retrieve the global leaderboard. +// getLeaderboard() { +// // Implement get leaderboard logic +// } + +// // Retrieve the rank of a specific player. +// getPlayerRank() { +// // Implement get player rank logic +// } +// } + + import { Injectable } from '@nestjs/common'; +import { RedisService } from 'src/redis/redis.service'; +import { Repository } from 'typeorm'; +import { InjectRepository } from '@nestjs/typeorm'; +import { LeaderboardEntry } from '../leaderboard-entry.entity'; -// Service responsible for leaderboard operations. @Injectable() export class LeaderboardService { - // Retrieve the global leaderboard. - getLeaderboard() { - // Implement get leaderboard logic + constructor( + @InjectRepository(LeaderboardEntry) + private leaderboardRepository: Repository, + private redisService: RedisService + ) {} + + // Helper method for caching + private async getCachedData(key: string, fetchFunction: () => Promise, ttl = 3600): Promise { + const cachedData = await this.redisService.get(key); + if (cachedData) return JSON.parse(cachedData); + + const freshData = await fetchFunction(); + await this.redisService.set(key, JSON.stringify(freshData), ttl); + + return freshData; + } + + // Retrieve the global leaderboard + async getLeaderboard() { + return this.getCachedData('leaderboard:global', () => + this.leaderboardRepository.find({ order: { score: 'DESC' }, take: 100 }) + ); } - // Retrieve the rank of a specific player. - getPlayerRank() { - // Implement get player rank logic + // Retrieve the rank of a specific player + async getPlayerRank(playerId: string) { + return this.getCachedData(`leaderboard:rank:${playerId}`, async () => { + const leaderboard = await this.getLeaderboard(); // Fetch cached leaderboard + const rank = leaderboard.findIndex(entry => entry.playerId === playerId) + 1; + return rank > 0 ? { playerId, rank } : null; + }); } } diff --git a/backend/src/redis/redis.module.ts b/backend/src/redis/redis.module.ts new file mode 100644 index 000000000..ef7c808cb --- /dev/null +++ b/backend/src/redis/redis.module.ts @@ -0,0 +1,9 @@ +import { Module, Global } from '@nestjs/common'; +import { RedisService } from './redis.service'; + +@Global() +@Module({ + providers: [RedisService], + exports: [RedisService], +}) +export class RedisModule {} diff --git a/backend/src/redis/redis.service.spec.ts b/backend/src/redis/redis.service.spec.ts new file mode 100644 index 000000000..9300ac3eb --- /dev/null +++ b/backend/src/redis/redis.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RedisService } from './redis.service'; + +describe('RedisService', () => { + let service: RedisService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RedisService], + }).compile(); + + service = module.get(RedisService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/redis/redis.service.ts b/backend/src/redis/redis.service.ts new file mode 100644 index 000000000..da01c7a3a --- /dev/null +++ b/backend/src/redis/redis.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@nestjs/common'; +import { Redis } from 'ioredis'; + +@Injectable() +export class RedisService { + private client: Redis; + + constructor() { + this.client = new Redis({ + host: '127.0.0.1', + port: 6379, + }); + } + + async get(key: string): Promise { + return this.client.get(key); + } + + async set(key: string, value: string, ttl?: number): Promise { + if (ttl) { + await this.client.set(key, value, 'EX', ttl); + } else { + await this.client.set(key, value); + } + } + + async del(key: string): Promise { + await this.client.del(key); + } +} + diff --git a/backend/src/songs/songs.module.ts b/backend/src/songs/songs.module.ts index 8fe9c6bfc..c94fe9d43 100644 --- a/backend/src/songs/songs.module.ts +++ b/backend/src/songs/songs.module.ts @@ -4,9 +4,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { SongsService } from './songs.service'; import { SongsController } from './songs.controller'; import { Song } from './entities/song.entity'; +import { RedisService } from 'src/redis/redis.service'; @Module({ - imports: [TypeOrmModule.forFeature([Song])], + imports: [TypeOrmModule.forFeature([Song]), RedisService], controllers: [SongsController], providers: [SongsService], exports: [SongsService], diff --git a/backend/src/songs/songs.service.ts b/backend/src/songs/songs.service.ts index d706d1bc5..5f0a8f654 100644 --- a/backend/src/songs/songs.service.ts +++ b/backend/src/songs/songs.service.ts @@ -4,59 +4,123 @@ import { Repository } from 'typeorm'; import { CreateSongDto } from './dto/create-song.dto'; import { UpdateSongDto } from './dto/update-song.dto'; import { Song } from './entities/song.entity'; +import { RedisService } from 'src/redis/redis.service'; @Injectable() export class SongsService { constructor( @InjectRepository(Song) private songsRepository: Repository, + // injecting the redis sercvice + private redisService: RedisService ) {} - create(createSongDto: CreateSongDto) { + async create(createSongDto: CreateSongDto) { const song = this.songsRepository.create(createSongDto); - return this.songsRepository.save(song); + const savedSong = await this.songsRepository.save(song); + await this.redisService.del('songs:all'); + return savedSong; } - findAll() { - return this.songsRepository.find(); + + async findAll() { + const cacheKey = 'songs:all'; + + const cachedSongs = await this.redisService.get(cacheKey); + if (cachedSongs) { + return JSON.parse(cachedSongs); + } + + const songs = await this.songsRepository.find(); + + await this.redisService.set(cacheKey, JSON.stringify(songs), 3600); + + return songs; } async findOne(id: string) { + const cacheKey = `song:${id}`; + + const cachedSong = await this.redisService.get(cacheKey); + if (cachedSong) { + return JSON.parse(cachedSong); + } + const song = await this.songsRepository.findOne({ where: { id } }); if (!song) { throw new NotFoundException(`Song with ID ${id} not found`); } + + await this.redisService.set(cacheKey, JSON.stringify(song), 3600); + return song; } async update(id: string, updateSongDto: UpdateSongDto) { const song = await this.findOne(id); Object.assign(song, updateSongDto); - return this.songsRepository.save(song); + const updatedSong = await this.songsRepository.save(song); + + // Invalidate caches + await this.redisService.del(`song:${id}`); + await this.redisService.del('songs:all'); + + return updatedSong; } async remove(id: string) { const song = await this.findOne(id); - return this.songsRepository.remove(song); + await this.songsRepository.remove(song); + + await this.redisService.del(`song:${id}`); + await this.redisService.del('songs:all'); + + return { message: `Song with ID ${id} deleted successfully` }; } async findByGenre(genre: string) { - return this.songsRepository.find({ where: { genre } }); + const cacheKey = `songs:genre:${genre}`; + + const cachedSongs = await this.redisService.get(cacheKey); + if (cachedSongs) { + return JSON.parse(cachedSongs); + } + + const songs = await this.songsRepository.find({ where: { genre } }); + + await this.redisService.set(cacheKey, JSON.stringify(songs), 3600); + + return songs; } async searchSongs(query: string) { - return this.songsRepository + const cacheKey = `songs:search:${query}`; + + const cachedResults = await this.redisService.get(cacheKey); + if (cachedResults) { + return JSON.parse(cachedResults); + } + + const songs = await this.songsRepository .createQueryBuilder('song') .where('song.title ILIKE :query OR song.artist ILIKE :query', { query: `%${query}%`, }) .getMany(); + + await this.redisService.set(cacheKey, JSON.stringify(songs), 1800); + + return songs; } async updatePlayCount(id: string) { const song = await this.findOne(id); song.playCount += 1; - return this.songsRepository.save(song); + const updatedSong = await this.songsRepository.save(song); + + await this.redisService.set(`song:${id}`, JSON.stringify(updatedSong), 3600); + + return updatedSong; } async findByDifficulty(level: number) { @@ -70,4 +134,4 @@ export class SongsService { .take(1) .getOne(); } -} \ No newline at end of file +}