From a2b746ccc9de1893a037c07f209fbdc0b4fdb68b Mon Sep 17 00:00:00 2001 From: fox3000foxy <40730498+fox3000foxy@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:19:13 +0100 Subject: [PATCH 1/2] first phase --- package.json | 166 +++++----- pnpm-lock.yaml | 122 ++++++- src/controllers/AuthenticatorController.ts | 3 +- src/controllers/BuyOrderController.ts | 6 +- src/controllers/GameController.ts | 27 +- src/controllers/GameGiftController.ts | 6 +- src/controllers/InventoryController.ts | 6 +- src/controllers/ItemController.ts | 16 +- src/controllers/LobbyController.ts | 12 +- src/controllers/LogController.ts | 6 +- src/controllers/MarketListingController.ts | 10 +- src/controllers/OAuth2Controller.ts | 6 +- src/controllers/StripeController.ts | 2 +- src/controllers/StudioController.ts | 12 +- src/controllers/TradeController.ts | 12 +- src/controllers/UserController.ts | 12 +- src/repositories/BadgeRepository.ts | 112 +++++-- src/repositories/BuyOrderRepository.ts | 99 ++++-- src/repositories/GameGiftRepository.ts | 122 +++++-- src/repositories/GameRepository.ts | 335 ++++++++++++++++---- src/repositories/GameViewRepository.ts | 179 ++++++++--- src/repositories/InventoryRepository.ts | 300 +++++++++++++----- src/repositories/ItemRepository.ts | 110 ++++--- src/repositories/LobbyRepository.ts | 56 ++-- src/repositories/MarketListingRepository.ts | 270 +++++++++++----- src/repositories/OAuth2Repository.ts | 110 ++++--- src/repositories/StudioRepository.ts | 46 ++- src/repositories/TradeRepository.ts | 93 ++++-- src/repositories/UserRepository.ts | 306 +++++++++++++++--- src/services/DatabaseService.ts | 100 ++---- src/services/StudioService.ts | 7 +- src/services/TradeService.ts | 85 ++++- src/services/UserService.ts | 246 +++++++------- 33 files changed, 2126 insertions(+), 874 deletions(-) diff --git a/package.json b/package.json index ff89cf5..12c5769 100644 --- a/package.json +++ b/package.json @@ -1,82 +1,84 @@ -{ - "name": "croissant-api", - "version": "1.0.0", - "description": "template", - "main": "dist/index.js", - "scripts": { - "start": "node dist/index.js", - "lint": "npx eslint src --ext .ts", - "lint:fix": "npx eslint src --ext .ts --fix", - "format": "npx prettier --write src", - "format:check": "npx prettier --check src", - "dev": "npx ts-node-dev src/index.ts", - "build": "npx tsc", - "test": "npx tsc && npx jest --detectOpenHandles --forceExit", - "test-watch": "npx tsc && npx jest --watch --detectOpenHandles --forceExit", - "migrate:logs": "npx ts-node src/database/migrate-logs.ts" - }, - "dependencies": { - "@peculiar/webcrypto": "^1.5.0", - "@simplewebauthn/server": "^13.1.2", - "bcryptjs": "^3.0.2", - "commander": "^13.1.0", - "compression": "^1.8.1", - "cors": "^2.8.5", - "croissant-api": "file:", - "crypto": "^1.0.1", - "diacritics": "^1.3.0", - "dotenv": "^16.5.0", - "ejs": "^3.1.10", - "express": "^4.21.2", - "express-rate-limit": "^8.1.0", - "express-session": "^1.18.2", - "express-static-gzip": "^3.0.0", - "inversify": "^6.2.2", - "inversify-express-utils": "^6.5.0", - "jest": "^29.7.0", - "jsonwebtoken": "^9.0.2", - "knex": "^3.1.0", - "mysql": "^2.18.1", - "node-fetch": "^2.7.0", - "nodemailer": "^7.0.7", - "passport": "^0.7.0", - "passport-steam": "^1.0.18", - "qrcode": "^1.5.4", - "reflect-metadata": "^0.2.2", - "sharp": "^0.34.1", - "socket.io": "^4.8.1", - "sqlite": "^5.1.1", - "sqlite3": "^5.1.7", - "stripe": "^18.3.0", - "time2fa": "^1.4.2", - "uuid": "^11.1.0", - "yup": "^1.6.1" - }, - "devDependencies": { - "@eslint/js": "^9.25.0", - "@types/compression": "^1.8.1", - "@types/cors": "^2.8.17", - "@types/csv-parse": "^1.2.5", - "@types/diacritics": "^1.3.3", - "@types/ejs": "^3.1.5", - "@types/express": "^5.0.1", - "@types/express-session": "^1.18.2", - "@types/jsonwebtoken": "^9.0.10", - "@types/node": "^14.18.63", - "@types/node-fetch": "^2.6.12", - "@types/nodemailer": "^6.4.17", - "@types/passport": "^1.0.17", - "@types/passport-steam": "^1.0.6", - "@types/qrcode": "^1.5.5", - "@types/sqlite3": "^5.1.0", - "@types/yup": "^0.32.0", - "eslint": "^9.25.0", - "prettier": "^3.5.3", - "supertest": "^7.1.0", - "ts-node": "^10.9.2", - "typescript": "5.3", - "typescript-eslint": "^8.30.1" - }, - "author": "fox3000foxy", - "license": "MIT" -} +{ + "name": "croissant-api", + "version": "1.0.0", + "description": "template", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "lint": "npx eslint src --ext .ts", + "lint:fix": "npx eslint src --ext .ts --fix", + "format": "npx prettier --write src", + "format:check": "npx prettier --check src", + "dev": "npx ts-node-dev src/index.ts", + "build": "npx tsc", + "test": "npx tsc && npx jest --detectOpenHandles --forceExit", + "test-watch": "npx tsc && npx jest --watch --detectOpenHandles --forceExit", + "migrate:logs": "npx ts-node src/database/migrate-logs.ts" + }, + "dependencies": { + "@peculiar/webcrypto": "^1.5.0", + "@simplewebauthn/server": "^13.1.2", + "bcryptjs": "^3.0.2", + "commander": "^13.1.0", + "compression": "^1.8.1", + "cors": "^2.8.5", + "croissant-api": "file:", + "crypto": "^1.0.1", + "csv-parse": "^6.1.0", + "diacritics": "^1.3.0", + "dotenv": "^16.5.0", + "ejs": "^3.1.10", + "express": "^4.21.2", + "express-rate-limit": "^8.1.0", + "express-session": "^1.18.2", + "express-static-gzip": "^3.0.0", + "inversify": "^6.2.2", + "inversify-express-utils": "^6.5.0", + "jest": "^29.7.0", + "jsonwebtoken": "^9.0.2", + "knex": "^3.1.0", + "mongodb": "^7.1.0", + "mysql": "^2.18.1", + "node-fetch": "^2.7.0", + "nodemailer": "^7.0.7", + "passport": "^0.7.0", + "passport-steam": "^1.0.18", + "qrcode": "^1.5.4", + "reflect-metadata": "^0.2.2", + "sharp": "^0.34.1", + "socket.io": "^4.8.1", + "sqlite": "^5.1.1", + "sqlite3": "^5.1.7", + "stripe": "^18.3.0", + "time2fa": "^1.4.2", + "uuid": "^11.1.0", + "yup": "^1.6.1" + }, + "devDependencies": { + "@eslint/js": "^9.25.0", + "@types/compression": "^1.8.1", + "@types/cors": "^2.8.17", + "@types/csv-parse": "^1.2.5", + "@types/diacritics": "^1.3.3", + "@types/ejs": "^3.1.5", + "@types/express": "^5.0.1", + "@types/express-session": "^1.18.2", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^14.18.63", + "@types/node-fetch": "^2.6.12", + "@types/nodemailer": "^6.4.17", + "@types/passport": "^1.0.17", + "@types/passport-steam": "^1.0.6", + "@types/qrcode": "^1.5.5", + "@types/sqlite3": "^5.1.0", + "@types/yup": "^0.32.0", + "eslint": "^9.25.0", + "prettier": "^3.5.3", + "supertest": "^7.1.0", + "ts-node": "^10.9.2", + "typescript": "5.3", + "typescript-eslint": "^8.30.1" + }, + "author": "fox3000foxy", + "license": "MIT" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30bf428..3deb0ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,10 +28,13 @@ importers: version: 2.8.6 croissant-api: specifier: 'file:' - version: file:(@types/node@14.18.63)(encoding@0.1.13)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.3.3)) + version: file:(@types/node@14.18.63)(encoding@0.1.13)(socks@2.8.7)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.3.3)) crypto: specifier: ^1.0.1 version: 1.0.1 + csv-parse: + specifier: ^6.1.0 + version: 6.1.0 diacritics: specifier: ^1.3.0 version: 1.3.0 @@ -68,6 +71,9 @@ importers: knex: specifier: ^3.1.0 version: 3.1.0(mysql@2.18.1)(sqlite3@5.1.7) + mongodb: + specifier: ^7.1.0 + version: 7.1.0(socks@2.8.7) mysql: specifier: ^2.18.1 version: 2.18.1 @@ -662,6 +668,9 @@ packages: '@levischuck/tiny-cbor@0.2.11': resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + '@mongodb-js/saslprep@1.4.6': + resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==} + '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} @@ -869,6 +878,12 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/webidl-conversions@7.0.3': + resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} + + '@types/whatwg-url@13.0.0': + resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} + '@types/yargs-parser@21.0.3': resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1113,6 +1128,10 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + bson@7.2.0: + resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==} + engines: {node: '>=20.19.0'} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} @@ -2196,6 +2215,9 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} @@ -2298,6 +2320,37 @@ packages: engines: {node: '>=10'} hasBin: true + mongodb-connection-string-url@7.0.1: + resolution: {integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==} + engines: {node: '>=20.19.0'} + + mongodb@7.1.0: + resolution: {integrity: sha512-kMfnKunbolQYwCIyrkxNJFB4Ypy91pYqua5NargS/f8ODNSJxT03ZU3n1JqL4mCzbSih8tvmMEMLpKTT7x5gCg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@aws-sdk/credential-providers': ^3.806.0 + '@mongodb-js/zstd': ^7.0.0 + gcp-metadata: ^7.0.1 + kerberos: ^7.0.0 + mongodb-client-encryption: '>=7.0.0 <7.1.0' + snappy: ^7.3.2 + socks: ^2.8.6 + peerDependenciesMeta: + '@aws-sdk/credential-providers': + optional: true + '@mongodb-js/zstd': + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} @@ -2762,6 +2815,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + sparse-bitfield@3.0.3: + resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -2905,6 +2961,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -3024,6 +3084,14 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -3678,6 +3746,10 @@ snapshots: '@levischuck/tiny-cbor@0.2.11': {} + '@mongodb-js/saslprep@1.4.6': + dependencies: + sparse-bitfield: 3.0.3 + '@noble/hashes@1.8.0': {} '@npmcli/fs@1.1.1': @@ -3984,6 +4056,12 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/webidl-conversions@7.0.3': {} + + '@types/whatwg-url@13.0.0': + dependencies: + '@types/webidl-conversions': 7.0.3 + '@types/yargs-parser@21.0.3': {} '@types/yargs@17.0.35': @@ -4309,6 +4387,8 @@ snapshots: dependencies: node-int64: 0.4.0 + bson@7.2.0: {} + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} @@ -4478,7 +4558,7 @@ snapshots: create-require@1.1.1: {} - croissant-api@file:(@types/node@14.18.63)(encoding@0.1.13)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.3.3)): + croissant-api@file:(@types/node@14.18.63)(encoding@0.1.13)(socks@2.8.7)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.3.3)): dependencies: '@peculiar/webcrypto': 1.5.0 '@simplewebauthn/server': 13.2.3 @@ -4499,6 +4579,7 @@ snapshots: jest: 29.7.0(@types/node@14.18.63)(ts-node@10.9.2(@types/node@14.18.63)(typescript@5.3.3)) jsonwebtoken: 9.0.3 knex: 3.1.0(mysql@2.18.1)(sqlite3@5.1.7) + mongodb: 7.1.0(socks@2.8.7) mysql: 2.18.1 node-fetch: 2.7.0(encoding@0.1.13) nodemailer: 7.0.13 @@ -4515,6 +4596,8 @@ snapshots: uuid: 11.1.0 yup: 1.7.1 transitivePeerDependencies: + - '@aws-sdk/credential-providers' + - '@mongodb-js/zstd' - '@types/node' - babel-plugin-macros - better-sqlite3 @@ -4522,10 +4605,15 @@ snapshots: - bufferutil - debug - encoding + - gcp-metadata + - kerberos + - mongodb-client-encryption - mysql2 - node-notifier - pg - pg-native + - snappy + - socks - supports-color - tedious - ts-node @@ -5651,6 +5739,8 @@ snapshots: media-typer@0.3.0: {} + memory-pager@1.5.0: {} + merge-descriptors@1.0.3: {} merge-stream@2.0.0: {} @@ -5740,6 +5830,19 @@ snapshots: mkdirp@1.0.4: {} + mongodb-connection-string-url@7.0.1: + dependencies: + '@types/whatwg-url': 13.0.0 + whatwg-url: 14.2.0 + + mongodb@7.1.0(socks@2.8.7): + dependencies: + '@mongodb-js/saslprep': 1.4.6 + bson: 7.2.0 + mongodb-connection-string-url: 7.0.1 + optionalDependencies: + socks: 2.8.7 + ms@2.0.0: {} ms@2.1.2: {} @@ -6277,6 +6380,10 @@ snapshots: source-map@0.6.1: {} + sparse-bitfield@3.0.3: + dependencies: + memory-pager: 1.5.0 + sprintf-js@1.0.3: {} sqlite3@5.1.7: @@ -6434,6 +6541,10 @@ snapshots: tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.4.0(typescript@5.3.3): dependencies: typescript: 5.3.3 @@ -6552,6 +6663,13 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/src/controllers/AuthenticatorController.ts b/src/controllers/AuthenticatorController.ts index d026755..5a9de3f 100644 --- a/src/controllers/AuthenticatorController.ts +++ b/src/controllers/AuthenticatorController.ts @@ -76,7 +76,8 @@ export class Authenticator { @httpPost('/:action', LoggedCheck.middleware) async handleAuthenticatorActions(req: AuthenticatedRequest, res: Response) { - const action = req.params.action; + const actionParam = req.params.action; + const action = Array.isArray(actionParam) ? actionParam[0] : actionParam; const user = req.user; try { switch (action) { diff --git a/src/controllers/BuyOrderController.ts b/src/controllers/BuyOrderController.ts index 404eb0c..60536c2 100644 --- a/src/controllers/BuyOrderController.ts +++ b/src/controllers/BuyOrderController.ts @@ -67,7 +67,7 @@ export class BuyOrderController { @httpPut('/:id/cancel', LoggedCheck.middleware) public async cancelBuyOrder(req: AuthenticatedRequest, res: Response) { const buyerId = req.user.user_id; - const orderId = req.params.id; + const orderId = req.params.id as string; try { await this.buyOrderService.cancelBuyOrder(orderId, buyerId); await this.logAction(req, 'cancelBuyOrder', 200); @@ -80,7 +80,7 @@ export class BuyOrderController { @httpGet('/user/:userId', LoggedCheck.middleware) public async getBuyOrdersByUser(req: AuthenticatedRequest, res: Response) { - const userId = req.params.userId; + const userId = req.params.userId as string; if (userId !== req.user.user_id) { await this.logAction(req, 'getBuyOrdersByUser', 403); return res.status(403).send({ message: 'Forbidden' }); @@ -97,7 +97,7 @@ export class BuyOrderController { @httpGet('/item/:itemId') public async getActiveBuyOrdersForItem(req: AuthenticatedRequest, res: Response) { - const itemId = req.params.itemId; + const itemId = req.params.itemId as string; try { const orders = await this.buyOrderService.getBuyOrders({ itemId, status: 'active' }, 'price DESC, created_at ASC'); await this.logAction(req, 'getActiveBuyOrdersForItem', 200); diff --git a/src/controllers/GameController.ts b/src/controllers/GameController.ts index 9681f53..53b247d 100644 --- a/src/controllers/GameController.ts +++ b/src/controllers/GameController.ts @@ -1,9 +1,10 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ +import crypto from 'crypto'; import { Request, Response } from 'express'; import rateLimit from 'express-rate-limit'; import { inject } from 'inversify'; -import { controller, httpGet, httpPost, httpPut, httpHead } from 'inversify-express-utils'; +import { controller, httpGet, httpHead, httpPost, httpPut } from 'inversify-express-utils'; import fetch from 'node-fetch'; import { v4 } from 'uuid'; import { AuthenticatedRequest, LoggedCheck } from '../middlewares/LoggedCheck'; @@ -12,7 +13,6 @@ import { IGameViewService } from '../services/GameViewService'; import { ILogService } from '../services/LogService'; import { IUserService } from '../services/UserService'; import { createGameBodySchema, gameIdParamSchema, updateGameBodySchema } from '../validators/GameValidator'; -import crypto from 'crypto'; function handleError(res: Response, error: unknown, message: string, status = 500) { const msg = error instanceof Error ? error.message : String(error); @@ -171,7 +171,7 @@ export class Games { return; } try { - const { gameId } = req.params; + const { gameId } = req.params as { gameId: string }; const game = await this.gameService.getGameWithBadgesAndViews(gameId); if (!game) { await this.createLog(req, 'getGame', 'games', 404); @@ -192,7 +192,8 @@ export class Games { return; } try { - const { gameId } = req.params; + const { gameId } = req.params as { gameId: string }; + const userId = req.user.user_id; const game = await this.gameService.getGameForOwner(gameId, userId); if (!game) { @@ -237,7 +238,7 @@ export class Games { return; } try { - const game = await this.gameService.getGame(req.params.gameId); + const game = await this.gameService.getGame(req.params.gameId as string); if (!game) { await this.createLog(req, 'updateGame', 'games', 404, req.user?.user_id); return res.status(404).send({ message: 'Game not found' }); @@ -246,8 +247,8 @@ export class Games { await this.createLog(req, 'updateGame', 'games', 403, req.user?.user_id); return res.status(403).send({ message: 'You are not the owner of this game' }); } - await this.gameService.updateGame(req.params.gameId, req.body); - const updatedGame = await this.gameService.getGame(req.params.gameId); + await this.gameService.updateGame(req.params.gameId as string, req.body); + const updatedGame = await this.gameService.getGame(req.params.gameId as string); await this.createLog(req, 'updateGame', 'games', 200, req.user.user_id); res.status(200).send(updatedGame); } catch (error) { @@ -258,7 +259,7 @@ export class Games { @httpPost(':gameId/buy', LoggedCheck.middleware, buyGameRateLimit) public async buyGame(req: AuthenticatedRequest, res: Response) { - const { gameId } = req.params; + const { gameId } = req.params as { gameId: string }; const userId = req.user.user_id; try { const game = await this.gameService.getGame(gameId); @@ -303,7 +304,7 @@ export class Games { @httpPost('/transfer-ownership/:gameId', LoggedCheck.middleware, transferOwnershipRateLimit) public async transferOwnership(req: AuthenticatedRequest, res: Response) { - const { gameId } = req.params; + const { gameId } = req.params as { gameId: string }; const { newOwnerId } = req.body; const userId = req.user.user_id; if (!gameId || !newOwnerId) { @@ -341,7 +342,7 @@ export class Games { await this.createLog(req, 'transferGame', 'games', 400, req.user?.user_id); return; } - const { gameId } = req.params; + const { gameId } = req.params as { gameId: string }; const { targetUserId } = req.body; const fromUserId = req.user.user_id; if (!targetUserId || fromUserId === targetUserId) { @@ -374,7 +375,7 @@ export class Games { await this.createLog(req, 'canTransferGame', 'games', 400, req.user?.user_id); return; } - const { gameId } = req.params; + const { gameId } = req.params as { gameId: string }; const { targetUserId } = req.query; const fromUserId = req.user.user_id; if (!targetUserId) { @@ -393,7 +394,7 @@ export class Games { @httpGet('/:gameId/download', LoggedCheck.middleware) public async downloadGame(req: AuthenticatedRequest, res: Response) { - const { gameId } = req.params; + const { gameId } = req.params as { gameId: string }; const userId = req.user.user_id; try { const game = await this.gameService.getGame(gameId); @@ -448,7 +449,7 @@ export class Games { @httpHead('/:gameId/download', LoggedCheck.middleware) public async headDownloadGame(req: AuthenticatedRequest, res: Response) { - const { gameId } = req.params; + const { gameId } = req.params as { gameId: string }; const userId = req.user.user_id; try { const game = await this.gameService.getGame(gameId); diff --git a/src/controllers/GameGiftController.ts b/src/controllers/GameGiftController.ts index 8af9cfc..cfaa73f 100644 --- a/src/controllers/GameGiftController.ts +++ b/src/controllers/GameGiftController.ts @@ -114,7 +114,7 @@ export class GameGifts { return res.status(404).send({ message: 'Unknown action' }); } } catch (error) { - await this.createLog(req, action, 'gifts', 500, userId); + await this.createLog(req, action as string, 'gifts', 500, userId); handleError(res, error, `Error in ${action}`); } } @@ -172,7 +172,7 @@ export class GameGifts { const { giftCode } = req.params; try { - const gift = await this.giftService.getGift(giftCode); + const gift = await this.giftService.getGift(giftCode as string); if (!gift) { await this.createLog(req, 'getGiftInfo', 'gifts', 404, req.user.user_id); return res.status(404).send({ message: 'Gift not found' }); @@ -220,7 +220,7 @@ export class GameGifts { return res.status(400).send({ message: 'Gift is no longer active' }); } - await this.giftService.revokeGift(giftId, userId); + await this.giftService.revokeGift(giftId as string, userId); const game = await this.gameService.getGame(gift.gameId); if (game) { diff --git a/src/controllers/InventoryController.ts b/src/controllers/InventoryController.ts index 17557fa..783b367 100644 --- a/src/controllers/InventoryController.ts +++ b/src/controllers/InventoryController.ts @@ -123,10 +123,10 @@ export class Inventories { @httpGet('/:userId') public async getInventory(req: Request, res: Response) { if (!(await validateOr400(userIdParamSchema, { userId: req.params.userId }, res))) { - await this.createLog(req, 'getInventory', 'inventory', 400, req.params.userId); + await this.createLog(req, 'getInventory', 'inventory', 400, req.params.userId as string); return; } - const userId = req.params.userId; + const userId = req.params.userId as string; try { const inventory = await this.inventoryService.getInventory(userId); await this.createLog(req, 'getInventory', 'inventory', 200, userId); @@ -154,7 +154,7 @@ export class Inventories { }) @httpGet('/:userId/item/:itemId/amount') public async getItemAmount(req: Request, res: Response) { - const { userId, itemId } = req.params; + const { userId, itemId } = req.params as { userId: string; itemId: string }; try { const correctedUserId = await this.inventoryService.getCorrectedUserId(userId); const repo = this.inventoryService.getInventoryRepository(); diff --git a/src/controllers/ItemController.ts b/src/controllers/ItemController.ts index 9ae9f0c..11a3bc3 100644 --- a/src/controllers/ItemController.ts +++ b/src/controllers/ItemController.ts @@ -244,7 +244,7 @@ export class Items { return; } try { - const { itemId } = req.params; + const { itemId } = req.params as { itemId: string }; const item = await this.itemService.getItem(itemId); if (!item || item.deleted) { await this.createLog(req, 'items', 404, undefined, { itemId }); @@ -334,7 +334,7 @@ export class Items { await this.createLog(req, 'items', 400); return; } - const { itemId } = req.params; + const { itemId } = req.params as { itemId: string }; const { name, description, price, iconHash, showInStore } = req.body; try { await this.itemService.updateItem(itemId, { @@ -376,7 +376,7 @@ export class Items { await this.createLog(req, 'items', 400); return; } - const { itemId } = req.params; + const { itemId } = req.params as { itemId: string }; try { await this.itemService.deleteItem(itemId); await this.createLog(req, 'items', 200, undefined, { itemId, action: 'deleted' }); @@ -389,7 +389,7 @@ export class Items { @httpPost('/buy/:itemId', LoggedCheck.middleware, buyItemRateLimit) public async buyItem(req: AuthenticatedRequest, res: Response) { - const { itemId } = req.params; + const { itemId } = req.params as { itemId: string }; const { amount } = req.body; if (!itemId || isNaN(amount)) { await this.createLog(req, 'inventory', 400, req.user?.user_id, { reason: 'invalid_input', itemId, amount }); @@ -429,7 +429,7 @@ export class Items { @httpPost('/sell/:itemId', LoggedCheck.middleware, sellItemRateLimit) public async sellItem(req: AuthenticatedRequest, res: Response) { - const { itemId } = req.params; + const { itemId } = req.params as { itemId: string }; const { amount, purchasePrice, dataItemIndex } = req.body; if (!itemId || isNaN(amount)) { await this.createLog(req, 'inventory', 400, req.user?.user_id, { reason: 'invalid_input', itemId, amount }); @@ -485,7 +485,7 @@ export class Items { @httpPost('/consume/:itemId', OwnerCheck.middleware, consumeItemRateLimit) public async consumeItem(req: AuthenticatedRequestWithOwner, res: Response) { - const { itemId } = req.params; + const { itemId } = req.params as { itemId: string }; const { amount, uniqueId, userId } = req.body; if (!itemId || !userId) { await this.createLog(req, 'inventory', 400, req.user?.user_id, { reason: 'missing_required_fields', itemId, userId }); @@ -535,7 +535,7 @@ export class Items { @httpPost('/drop/:itemId', LoggedCheck.middleware, dropItemRateLimit) public async dropItem(req: AuthenticatedRequest, res: Response) { - const { itemId } = req.params; + const { itemId } = req.params as { itemId: string }; const { amount, uniqueId, dataItemIndex } = req.body; if (!itemId) { await this.createLog(req, 'inventory', 400, req.user?.user_id, { reason: 'missing_itemId' }); @@ -580,7 +580,7 @@ export class Items { @httpPost('/transfer-ownership/:itemId', OwnerCheck.middleware, transferOwnershipRateLimit) public async transferOwnership(req: AuthenticatedRequestWithOwner, res: Response) { - const { itemId } = req.params; + const { itemId } = req.params as { itemId: string }; const { newOwnerId } = req.body; if (!itemId || !newOwnerId) { await this.createLog(req, 'items', 400, req.user?.user_id, { reason: 'invalid_input', itemId, newOwnerId }); diff --git a/src/controllers/LobbyController.ts b/src/controllers/LobbyController.ts index 9e414d7..c17c6ac 100644 --- a/src/controllers/LobbyController.ts +++ b/src/controllers/LobbyController.ts @@ -127,7 +127,7 @@ export class Lobbies { return; } try { - const lobbyId = req.params.lobbyId; + const lobbyId = req.params.lobbyId as string; const lobby = await this.lobbyService.getLobby(lobbyId); if (!lobby) { await this.createLog(req, 'getLobby', 'lobbies', 404); @@ -177,11 +177,11 @@ export class Lobbies { @httpGet('/user/:userId') public async getUserLobby(req: Request, res: Response) { if (!(await validateOr400(userIdParamSchema, req.params, res))) { - await this.createLog(req, 'getUserLobby', 'lobbies', 400, req.params.userId); + await this.createLog(req, 'getUserLobby', 'lobbies', 400, req.params.userId as string); return; } try { - const { userId } = req.params; + const { userId } = req.params as { userId: string }; const lobby = await this.lobbyService.getUserLobby(userId); if (!lobby) { await this.createLog(req, 'getUserLobby', 'lobbies', 404, userId); @@ -190,7 +190,7 @@ export class Lobbies { await this.createLog(req, 'getUserLobby', 'lobbies', 200, userId); res.send(lobby); } catch (error) { - await this.createLog(req, 'getUserLobby', 'lobbies', 500, req.params.userId); + await this.createLog(req, 'getUserLobby', 'lobbies', 500, req.params.userId as string); handleError(res, error, 'Error fetching user lobby'); } } @@ -212,7 +212,7 @@ export class Lobbies { } try { await this.lobbyService.leaveAllLobbies(req.user.user_id); - await this.lobbyService.joinLobby(req.params.lobbyId, req.user.user_id); + await this.lobbyService.joinLobby(req.params.lobbyId as string, req.user.user_id); await this.createLog(req, 'joinLobby', 'lobbies', 200, req.user.user_id); res.status(200).send({ message: 'Joined lobby' }); } catch (error) { @@ -237,7 +237,7 @@ export class Lobbies { return; } try { - await this.lobbyService.leaveLobby(req.params.lobbyId, req.user.user_id); + await this.lobbyService.leaveLobby(req.params.lobbyId as string, req.user.user_id); await this.createLog(req, 'leaveLobby', 'lobbies', 200, req.user.user_id); res.status(200).send({ message: 'Left lobby' }); } catch (error) { diff --git a/src/controllers/LogController.ts b/src/controllers/LogController.ts index 76779fe..8bebc84 100644 --- a/src/controllers/LogController.ts +++ b/src/controllers/LogController.ts @@ -37,7 +37,7 @@ export class LogController { } try { - const controller = req.params.controller; + const controller = req.params.controller as string; const limit = parseInt(req.query.limit as string) || 100; const logs = await this.logService.getLogsByController(controller, limit); @@ -54,7 +54,7 @@ export class LogController { } try { - const userId = req.params.userId; + const userId = req.params.userId as string; const limit = parseInt(req.query.limit as string) || 100; const logs = await this.logService.getLogsByUser(userId, limit); @@ -71,7 +71,7 @@ export class LogController { } try { - const tableName = req.params.tableName; + const tableName = req.params.tableName as string; const limit = parseInt(req.query.limit as string) || 100; const logs = await this.logService.getLogsByTable(tableName, limit); diff --git a/src/controllers/MarketListingController.ts b/src/controllers/MarketListingController.ts index a37462a..335a91c 100644 --- a/src/controllers/MarketListingController.ts +++ b/src/controllers/MarketListingController.ts @@ -65,8 +65,8 @@ export class MarketListingController { @httpPut('/:id/cancel', LoggedCheck.middleware) public async cancelMarketListing(req: AuthenticatedRequest, res: Response) { - const sellerId = req.user.user_id; - const listingId = req.params.id; + const sellerId = req.user.user_id as string; + const listingId = req.params.id as string; try { await this.marketListingService.cancelMarketListing(listingId, sellerId); @@ -97,7 +97,7 @@ export class MarketListingController { @httpGet('/item/:itemId') public async getActiveListingsForItem(req: AuthenticatedRequest, res: Response) { - const itemId = req.params.itemId; + const itemId = req.params.itemId as string; try { const listings = await this.marketListingService.getActiveListingsForItem(itemId); await this.createLog(req, 'getActiveListingsForItem', 'market_listings', 200, req.user?.user_id); @@ -110,7 +110,7 @@ export class MarketListingController { @httpGet('/:id') public async getMarketListingById(req: AuthenticatedRequest, res: Response) { - const listingId = req.params.id; + const listingId = req.params.id as string; try { const listing = await this.marketListingService.getMarketListingById(listingId); if (!listing) { @@ -161,7 +161,7 @@ export class MarketListingController { await this.createLog(req, 'buyMarketListing', 'market_listings', 401, undefined); return res.status(401).send({ message: 'Unauthorized' }); } - const listingId = req.params.id; + const listingId = req.params.id as string; try { const listing = await this.marketListingService.getMarketListingById(listingId); if (!listing) { diff --git a/src/controllers/OAuth2Controller.ts b/src/controllers/OAuth2Controller.ts index 74b091b..addff36 100644 --- a/src/controllers/OAuth2Controller.ts +++ b/src/controllers/OAuth2Controller.ts @@ -88,7 +88,7 @@ export class OAuth2 { async getAppByClientId(req: Request, res: Response) { const { client_id } = req.params; try { - const app = await this.oauth2Service.getAppByClientId(client_id); + const app = await this.oauth2Service.getAppByClientId(client_id as string); if (!app) { await this.createLog(req, 'oauth2_apps', 404, undefined, { client_id }); return res.status(404).send({ message: 'App not found' }); @@ -193,7 +193,7 @@ export class OAuth2 { const { client_id } = req.params; const { name, redirect_urls } = req.body; try { - await this.oauth2Service.updateApp(client_id, req.user.user_id, { + await this.oauth2Service.updateApp(client_id as string, req.user.user_id, { name, redirect_urls, }); @@ -227,7 +227,7 @@ export class OAuth2 { async deleteApp(req: AuthenticatedRequest, res: Response) { const { client_id } = req.params; try { - await this.oauth2Service.deleteApp(client_id, req.user.user_id); + await this.oauth2Service.deleteApp(client_id as string, req.user.user_id); await this.createLog(req, 'oauth2_apps', 204, req.user.user_id, { client_id }); res.status(204).send(); } catch (error) { diff --git a/src/controllers/StripeController.ts b/src/controllers/StripeController.ts index 2480c9b..2df675d 100644 --- a/src/controllers/StripeController.ts +++ b/src/controllers/StripeController.ts @@ -82,7 +82,7 @@ export class StripeController { throw new Error('Stripe API key is not set in environment variables'); } this.stripe = new Stripe(STRIPE_API_KEY, { - apiVersion: '2025-06-30.basil', + apiVersion: '2025-08-27.basil', }); } diff --git a/src/controllers/StudioController.ts b/src/controllers/StudioController.ts index cc659d1..b5ecd4f 100644 --- a/src/controllers/StudioController.ts +++ b/src/controllers/StudioController.ts @@ -118,7 +118,7 @@ export class Studios { @httpGet('/:studioId') async getStudio(req: Request, res: Response) { try { - const studio = await this.getStudioOrError(req.params.studioId, req, res); + const studio = await this.getStudioOrError(req.params.studioId as string, req, res); if (!studio) return; await this.createLog(req, 'studios', 200); res.send(studio); @@ -190,11 +190,11 @@ export class Studios { const { userId } = req.body; if (!userId) return res.status(400).send({ message: 'Missing userId' }); try { - const user = await this.studioService.getUser(userId); + const user = await this.studioService.getUser(userId as string); if (!user) return res.status(404).send({ message: 'User not found' }); - const studio = await this.checkStudioAdmin(req, res, studioId); + const studio = await this.checkStudioAdmin(req, res, studioId as string); if (!studio) return; - await this.studioService.addUserToStudio(studioId, user); + await this.studioService.addUserToStudio(studioId as string, user); await this.createLog(req, 'studio_users', 200); res.send({ message: 'User added to studio' }); } catch (error) { @@ -218,10 +218,10 @@ export class Studios { const { userId } = req.body; if (!userId) return res.status(400).send({ message: 'Missing userId' }); try { - const studio = await this.checkStudioAdmin(req, res, studioId); + const studio = await this.checkStudioAdmin(req, res, studioId as string); if (!studio) return; if (studio.admin_id === userId) return res.status(403).send({ message: 'Cannot remove the studio admin' }); - await this.studioService.removeUserFromStudio(studioId, userId); + await this.studioService.removeUserFromStudio(studioId as string, userId); await this.createLog(req, 'studio_users', 200); res.send({ message: 'User removed from studio' }); } catch (error) { diff --git a/src/controllers/TradeController.ts b/src/controllers/TradeController.ts index fb0dbfd..a45e6ba 100644 --- a/src/controllers/TradeController.ts +++ b/src/controllers/TradeController.ts @@ -72,7 +72,7 @@ export class Trades { } try { - const trade = await this.tradeService.startOrGetPendingTrade(fromUserId, toUserId); + const trade = await this.tradeService.startOrGetPendingTrade(fromUserId, toUserId as string); await this.createLog(req, 'trades', 200, { trade_id: trade.id, target_user_id: toUserId, @@ -130,7 +130,7 @@ export class Trades { const id = req.params.id; try { - const trade = await this.tradeService.getFormattedTradeById(id); + const trade = await this.tradeService.getFormattedTradeById(id as string); if (!trade) { await this.createLog(req, 'trades', 404, { trade_id: id }); @@ -272,7 +272,7 @@ export class Trades { } try { - await this.tradeService.addItemToTrade(tradeId, req.user.user_id, tradeItem); + await this.tradeService.addItemToTrade(tradeId as string, req.user.user_id, tradeItem); await this.createLog(req, 'trade_items', 200, { trade_id: tradeId, action: 'add_item', @@ -335,7 +335,7 @@ export class Trades { } try { - await this.tradeService.removeItemFromTrade(tradeId, req.user.user_id, tradeItem); + await this.tradeService.removeItemFromTrade(tradeId as string, req.user.user_id, tradeItem); await this.createLog(req, 'trade_items', 200, { trade_id: tradeId, action: 'remove_item', @@ -371,7 +371,7 @@ export class Trades { const tradeId = req.params.id; try { - await this.tradeService.approveTrade(tradeId, req.user.user_id); + await this.tradeService.approveTrade(tradeId as string, req.user.user_id); await this.createLog(req, 'trades', 200, { trade_id: tradeId, action: 'approve', @@ -401,7 +401,7 @@ export class Trades { const tradeId = req.params.id; try { - await this.tradeService.cancelTrade(tradeId, req.user.user_id); + await this.tradeService.cancelTrade(tradeId as string, req.user.user_id); await this.createLog(req, 'trades', 200, { trade_id: tradeId, action: 'cancel', diff --git a/src/controllers/UserController.ts b/src/controllers/UserController.ts index 7b908c6..a794702 100644 --- a/src/controllers/UserController.ts +++ b/src/controllers/UserController.ts @@ -657,14 +657,14 @@ export class Users { return this.sendError(res, 400, 'Invalid userId'); } const { userId } = req.params; - const userWithData = await this.userService.getUserWithPublicProfile(userId); + const userWithData = await this.userService.getUserWithPublicProfile(userId as string); if (!userWithData || ('disabled' in userWithData && userWithData['disabled'])) { await this.createLog(req, 'getUser', 'users', 404); return this.sendError(res, 404, 'User not found'); } await this.createLog(req, 'getUser', 'users', 200); - const studios = await this.studioService.getUserStudios(userId); + const studios = await this.studioService.getUserStudios(userId as string); res.send({ ...this.mapUserSearch(userWithData), studios: studios.map(s => { @@ -716,7 +716,7 @@ export class Users { }); } try { - await this.userService.disableAccount(userId, adminUserId); + await this.userService.disableAccount(userId as string, adminUserId); await this.createLog(req, 'disableAccount', 'users', 200, adminUserId); res.status(200).send({ message: 'Account disabled' }); } catch (error) { @@ -740,7 +740,7 @@ export class Users { return res.status(400).send({ message: 'Vous ne pouvez pas réactiver votre propre compte.' }); } try { - await this.userService.reenableAccount(userId, adminUserId); + await this.userService.reenableAccount(userId as string, adminUserId); await this.createLog(req, 'reenableAccount', 'users', 200, adminUserId); res.status(200).send({ message: 'Account re-enabled' }); } catch (error) { @@ -759,12 +759,12 @@ export class Users { } try { await userIdParamValidator.validate(req.params); - } catch { + } catch (error) { await this.createLog(req, 'adminGetUser', 'users', 400, req.user?.user_id); return this.sendError(res, 400, 'Invalid userId'); } const { userId } = req.params; - const userWithData = await this.userService.adminGetUserWithProfile(userId); + const userWithData = await this.userService.adminGetUserWithProfile(userId as string); if (!userWithData) { await this.createLog(req, 'adminGetUser', 'users', 404, req.user?.user_id); return this.sendError(res, 404, 'User not found'); diff --git a/src/repositories/BadgeRepository.ts b/src/repositories/BadgeRepository.ts index 96e3b18..ee7a5cd 100644 --- a/src/repositories/BadgeRepository.ts +++ b/src/repositories/BadgeRepository.ts @@ -5,42 +5,112 @@ export class BadgeRepository { constructor(private databaseService: IDatabaseService) {} async getBadgeTypes(): Promise { - const result = await this.databaseService.read('SELECT * FROM badge_types ORDER BY id'); - return result; + // const result = await this.databaseService.read('SELECT * FROM badge_types ORDER BY id'); + + // MongoDB query to get badge types + const db = await this.databaseService.getDb(); + const result = await db.collection('badge_types').find().sort({ id: 1 }).toArray(); + + // Convert MongoDB documents to BadgeType interface if necessary + const badgeTypes: BadgeType[] = result.map(doc => ({ + id: doc.id, + name: doc.name, + display_name: doc.display_name, + color: doc.color, + icon: doc.icon, + duration_days: doc.duration_days + })); + return badgeTypes; } async getActiveBadgesForGame(gameId: string): Promise { - const result = await this.databaseService.read( - `SELECT - b.id, b.name, b.display_name, b.color, b.icon, gb.expires_at - FROM game_badges gb - JOIN badge_types b ON gb.badge_id = b.id - WHERE gb.game_id = ? AND gb.expires_at > NOW() - ORDER BY gb.created_at DESC`, - [gameId] - ); - return result; + // const result = await this.databaseService.read( + // `SELECT + // b.id, b.name, b.display_name, b.color, b.icon, gb.expires_at + // FROM game_badges gb + // JOIN badge_types b ON gb.badge_id = b.id + // WHERE gb.game_id = ? AND gb.expires_at > NOW() + // ORDER BY gb.created_at DESC`, + // [gameId] + // ); + // MongoDB query to get active badges for a game + const db = await this.databaseService.getDb(); + const result = await db.collection('game_badges').aggregate([ + { $match: { gameId: gameId, expires_at: { $gt: new Date() } } }, + { $lookup: { + from: 'badge_types', + localField: 'badgeId', + foreignField: 'id', + as: 'badge_info' + } + }, + { $unwind: '$badge_info' }, + { $project: { + id: '$badge_info.id', + name: '$badge_info.name', + display_name: '$badge_info.display_name', + color: '$badge_info.color', + icon: '$badge_info.icon', + expires_at: '$expires_at' + } + }, + { $sort: { created_at: -1 } } + ]).toArray(); + + // Convert MongoDB documents to Badge interface if necessary + const badges: Badge[] = result.map(doc => ({ + id: doc.id, + name: doc.name, + display_name: doc.display_name, + color: doc.color, + icon: doc.icon, + expires_at: doc.expires_at + })); + return badges; } async addBadgeToGame(gameId: string, badgeId: number, durationDays: number): Promise { const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + durationDays); - await this.databaseService.request( - `INSERT INTO game_badges (game_id, badge_id, created_at, expires_at) - VALUES (?, ?, NOW(), ?) - ON DUPLICATE KEY UPDATE - created_at = NOW(), expires_at = ?`, - [gameId, badgeId, expiresAt, expiresAt] + // await this.databaseService.request( + // `INSERT INTO game_badges (game_id, badge_id, created_at, expires_at) + // VALUES (?, ?, NOW(), ?) + // ON DUPLICATE KEY UPDATE + // created_at = NOW(), expires_at = ?`, + // [gameId, badgeId, expiresAt, expiresAt] + // ); + // MongoDB query to add badge to game + const db = await this.databaseService.getDb(); + await db.collection('game_badges').updateOne( + { gameId: gameId, badgeId: badgeId }, + { $set: { created_at: new Date(), expires_at: expiresAt } }, + { upsert: true } ); } async removeExpiredBadges(): Promise { - await this.databaseService.request('DELETE FROM game_badges WHERE expires_at < NOW()'); + // await this.databaseService.request('DELETE FROM game_badges WHERE expires_at < NOW()'); + // MongoDB query to remove expired badges + const db = await this.databaseService.getDb(); + await db.collection('game_badges').deleteMany({ expires_at: { $lt: new Date() } }); } async getBadgeTypeByName(name: string): Promise { - const result = await this.databaseService.read('SELECT * FROM badge_types WHERE name = ?', [name]); - return result.length > 0 ? result[0] : null; + // const result = await this.databaseService.read('SELECT * FROM badge_types WHERE name = ?', [name]); + const db = await this.databaseService.getDb(); + const result = await db.collection('badge_types').find({ name: name }).toArray(); + + // Convert MongoDB document to BadgeType interface if necessary + const badgeTypes: BadgeType[] = result.map(doc => ({ + id: doc.id, + name: doc.name, + display_name: doc.display_name, + color: doc.color, + icon: doc.icon, + duration_days: doc.duration_days + })); + + return badgeTypes.length > 0 ? badgeTypes[0] : null; } } diff --git a/src/repositories/BuyOrderRepository.ts b/src/repositories/BuyOrderRepository.ts index 09ed9fd..3a33bbc 100644 --- a/src/repositories/BuyOrderRepository.ts +++ b/src/repositories/BuyOrderRepository.ts @@ -5,48 +5,97 @@ export class BuyOrderRepository { constructor(private databaseService: IDatabaseService) {} async insertBuyOrder(order: BuyOrder): Promise { - await this.databaseService.request( - `INSERT INTO buy_orders (id, buyer_id, item_id, price, status, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - [order.id, order.buyer_id, order.item_id, order.price, order.status, order.created_at, order.updated_at] - ); + // await this.databaseService.request( + // `INSERT INTO buy_orders (id, buyer_id, item_id, price, status, created_at, updated_at) + // VALUES (?, ?, ?, ?, ?, ?, ?)`, + // [order.id, order.buyer_id, order.item_id, order.price, order.status, order.created_at, order.updated_at] + // ); + + // MongoDB query to insert a buy order + const db = await this.databaseService.getDb(); + await db.collection('buy_orders').insertOne({ + id: order.id, + buyer_id: order.buyer_id, + item_id: order.item_id, + price: order.price, + status: order.status, + created_at: new Date(order.created_at), + updated_at: new Date(order.updated_at), + fulfilled_at: order.fulfilled_at ? new Date(order.fulfilled_at) : null + }); } async updateBuyOrderStatusToCancelled(orderId: string, buyerId: string, updatedAt: string): Promise { - await this.databaseService.request( - `UPDATE buy_orders - SET status = 'cancelled', updated_at = ? - WHERE id = ? AND buyer_id = ? AND status = 'active'`, - [updatedAt, orderId, buyerId] + // await this.databaseService.request( + // `UPDATE buy_orders + // SET status = 'cancelled', updated_at = ? + // WHERE id = ? AND buyer_id = ? AND status = 'active'`, + // [updatedAt, orderId, buyerId] + // ); + // MongoDB query to update buy order status to cancelled + const db = await this.databaseService.getDb(); + await db.collection('buy_orders').updateOne( + { id: orderId, buyer_id: buyerId, status: 'active' }, + { $set: { status: 'cancelled', updated_at: new Date(updatedAt) } } ); } async getBuyOrders(filters: { userId?: string; itemId?: string; status?: string; minPrice?: number } = {}, orderBy: string = 'created_at DESC', limit?: number): Promise { - let query = `SELECT * FROM buy_orders WHERE 1=1`; - const params = []; + // let query = `SELECT * FROM buy_orders WHERE 1=1`; + // const params = []; + + // if (filters.userId) { + // query += ` AND buyer_id = ?`; + // params.push(filters.userId); + // } + // if (filters.itemId) { + // query += ` AND item_id = ?`; + // params.push(filters.itemId); + // } + // if (filters.status) { + // query += ` AND status = ?`; + // params.push(filters.status); + // } + // if (filters.minPrice !== undefined) { + // query += ` AND price >= ?`; + // params.push(filters.minPrice); + // } + // query += ` ORDER BY ${orderBy}`; + // if (limit) { + // query += ` LIMIT ${limit}`; + // } + + // return await this.databaseService.read(query, params); + // MongoDB query to get buy orders with filters + const db = await this.databaseService.getDb(); + const mongoQuery: any = {}; if (filters.userId) { - query += ` AND buyer_id = ?`; - params.push(filters.userId); + mongoQuery.buyer_id = filters.userId; } if (filters.itemId) { - query += ` AND item_id = ?`; - params.push(filters.itemId); + mongoQuery.item_id = filters.itemId; } if (filters.status) { - query += ` AND status = ?`; - params.push(filters.status); + mongoQuery.status = filters.status; } if (filters.minPrice !== undefined) { - query += ` AND price >= ?`; - params.push(filters.minPrice); + mongoQuery.price = { $gte: filters.minPrice }; } - query += ` ORDER BY ${orderBy}`; - if (limit) { - query += ` LIMIT ${limit}`; - } + const result = await db.collection('buy_orders').find(mongoQuery).toArray(); - return await this.databaseService.read(query, params); + // Convert MongoDB documents to BuyOrder interface if necessary + const buyOrders: BuyOrder[] = result.map(doc => ({ + id: doc.id, + buyer_id: doc.buyer_id, + item_id: doc.item_id, + price: doc.price, + status: doc.status, + created_at: doc.created_at.toISOString(), + updated_at: doc.updated_at.toISOString(), + fulfilled_at: doc.fulfilled_at ? doc.fulfilled_at.toISOString() : null + })); + return buyOrders; } } diff --git a/src/repositories/GameGiftRepository.ts b/src/repositories/GameGiftRepository.ts index 104ae28..8fc2403 100644 --- a/src/repositories/GameGiftRepository.ts +++ b/src/repositories/GameGiftRepository.ts @@ -5,61 +5,119 @@ export class GameGiftRepository { constructor(private databaseService: IDatabaseService) {} async insertGift(gift: GameGift): Promise { - await this.databaseService.request( - `INSERT INTO game_gifts (id, gameId, fromUserId, giftCode, createdAt, isActive, message) - VALUES (?, ?, ?, ?, ?, ?, ?)`, - [gift.id, gift.gameId, gift.fromUserId, gift.giftCode, gift.createdAt.toISOString(), 1, gift.message || null] - ); + // await this.databaseService.request( + // `INSERT INTO game_gifts (id, gameId, fromUserId, giftCode, createdAt, isActive, message) + // VALUES (?, ?, ?, ?, ?, ?, ?)`, + // [gift.id, gift.gameId, gift.fromUserId, gift.giftCode, gift.createdAt.toISOString(), 1, gift.message || null] + // ); + // MongoDB query to insert a game gift + const db = await this.databaseService.getDb(); + await db.collection('game_gifts').insertOne({ + id: gift.id, + gameId: gift.gameId, + fromUserId: gift.fromUserId, + toUserId: gift.toUserId || null, + giftCode: gift.giftCode, + createdAt: new Date(gift.createdAt), + claimedAt: gift.claimedAt ? new Date(gift.claimedAt) : null, + isActive: gift.isActive, + message: gift.message || null + }); } async updateGiftClaim(giftCode: string, userId: string, claimedAt: Date): Promise { - await this.databaseService.request(`UPDATE game_gifts SET toUserId = ?, claimedAt = ?, isActive = 0 WHERE giftCode = ?`, [userId, claimedAt.toISOString(), giftCode]); + // await this.databaseService.request(`UPDATE game_gifts SET toUserId = ?, claimedAt = ?, isActive = 0 WHERE giftCode = ?`, [userId, claimedAt.toISOString(), giftCode]); + const db = await this.databaseService.getDb(); + await db.collection('game_gifts').updateOne( + { giftCode: giftCode }, + { $set: { toUserId: userId, claimedAt: new Date(claimedAt), isActive: false } } + ); } async updateGiftStatus(giftId: string, isActive: boolean): Promise { - await this.databaseService.request(`UPDATE game_gifts SET isActive = ? WHERE id = ?`, [isActive ? 1 : 0, giftId]); + // await this.databaseService.request(`UPDATE game_gifts SET isActive = ? WHERE id = ?`, [isActive ? 1 : 0, giftId]); + const db = await this.databaseService.getDb(); + await db.collection('game_gifts').updateOne( + { id: giftId }, + { $set: { isActive: isActive } } + ); } // Méthode générique pour récupérer les gifts selon des filtres async getGifts(filters: { giftCode?: string; giftId?: string; fromUserId?: string; toUserId?: string; isActive?: boolean } = {}, orderBy: string = 'createdAt DESC'): Promise { - let query = `SELECT * FROM game_gifts WHERE 1=1`; - const params = []; + // let query = `SELECT * FROM game_gifts WHERE 1=1`; + // const params = []; + + // if (filters.giftCode) { + // query += ` AND giftCode = ?`; + // params.push(filters.giftCode); + // } + // if (filters.giftId) { + // query += ` AND id = ?`; + // params.push(filters.giftId); + // } + // if (filters.fromUserId) { + // query += ` AND fromUserId = ?`; + // params.push(filters.fromUserId); + // } + // if (filters.toUserId) { + // query += ` AND toUserId = ?`; + // params.push(filters.toUserId); + // } + // if (filters.isActive !== undefined) { + // query += ` AND isActive = ?`; + // params.push(filters.isActive ? 1 : 0); + // } + // query += ` ORDER BY ${orderBy}`; + + // const rows = await this.databaseService.read(query, params); + // return rows.map(row => ({ + // id: row.id, + // gameId: row.gameId, + // fromUserId: row.fromUserId, + // toUserId: row.toUserId, + // giftCode: row.giftCode, + // createdAt: new Date(row.createdAt), + // claimedAt: row.claimedAt ? new Date(row.claimedAt) : undefined, + // isActive: Boolean(row.isActive), + // message: row.message, + // })); + + // MongoDB query to get gifts with filters + const db = await this.databaseService.getDb(); + const mongoQuery: any = {}; if (filters.giftCode) { - query += ` AND giftCode = ?`; - params.push(filters.giftCode); + mongoQuery.giftCode = filters.giftCode; } if (filters.giftId) { - query += ` AND id = ?`; - params.push(filters.giftId); + mongoQuery.id = filters.giftId; } if (filters.fromUserId) { - query += ` AND fromUserId = ?`; - params.push(filters.fromUserId); + mongoQuery.fromUserId = filters.fromUserId; } if (filters.toUserId) { - query += ` AND toUserId = ?`; - params.push(filters.toUserId); + mongoQuery.toUserId = filters.toUserId; } if (filters.isActive !== undefined) { - query += ` AND isActive = ?`; - params.push(filters.isActive ? 1 : 0); + mongoQuery.isActive = filters.isActive; } - query += ` ORDER BY ${orderBy}`; - - const rows = await this.databaseService.read(query, params); - return rows.map(row => ({ - id: row.id, - gameId: row.gameId, - fromUserId: row.fromUserId, - toUserId: row.toUserId, - giftCode: row.giftCode, - createdAt: new Date(row.createdAt), - claimedAt: row.claimedAt ? new Date(row.claimedAt) : undefined, - isActive: Boolean(row.isActive), - message: row.message, + const result = await db.collection('game_gifts').find(mongoQuery).toArray(); + + // Convert MongoDB documents to GameGift interface if necessary + const gifts: GameGift[] = result.map(doc => ({ + id: doc.id, + gameId: doc.gameId, + fromUserId: doc.fromUserId, + toUserId: doc.toUserId, + giftCode: doc.giftCode, + createdAt: doc.createdAt.toISOString(), + claimedAt: doc.claimedAt ? doc.claimedAt.toISOString() : null, + isActive: Boolean(doc.isActive), + message: doc.message || null })); + return gifts; } // Surcharges utilisant la méthode générique diff --git a/src/repositories/GameRepository.ts b/src/repositories/GameRepository.ts index 55473cd..d411048 100644 --- a/src/repositories/GameRepository.ts +++ b/src/repositories/GameRepository.ts @@ -5,30 +5,76 @@ export class GameRepository { constructor(private databaseService: IDatabaseService) {} async getGames(filters: { gameId?: string; ownerId?: string; showInStore?: boolean; search?: string } = {}, select: string = '*', orderBy: string = '', limit?: number): Promise { - let query = `SELECT ${select} FROM games WHERE 1=1`; - const params = []; + // let query = `SELECT ${select} FROM games WHERE 1=1`; + // const params = []; + // if (filters.gameId) { + // query += ` AND gameId = ?`; + // params.push(filters.gameId); + // } + // if (filters.ownerId) { + // query += ` AND owner_id = ?`; + // params.push(filters.ownerId); + // } + // if (filters.showInStore !== undefined) { + // query += ` AND showInStore = ?`; + // params.push(filters.showInStore ? 1 : 0); + // } + // if (filters.search) { + // const searchTerm = `%${filters.search.toLowerCase()}%`; + // query += ` AND (LOWER(name) LIKE ? OR LOWER(description) LIKE ? OR LOWER(genre) LIKE ?)`; + // params.push(searchTerm, searchTerm, searchTerm); + // } + // if (orderBy) query += ` ORDER BY ${orderBy}`; + // if (limit) query += ` LIMIT ${limit}`; + + // return await this.databaseService.read(query, params); + // MongoDB query to get games with filters + const db = await this.databaseService.getDb(); + const mongoQuery: any = {}; if (filters.gameId) { - query += ` AND gameId = ?`; - params.push(filters.gameId); + mongoQuery.gameId = filters.gameId; } if (filters.ownerId) { - query += ` AND owner_id = ?`; - params.push(filters.ownerId); + mongoQuery.owner_id = filters.ownerId; } if (filters.showInStore !== undefined) { - query += ` AND showInStore = ?`; - params.push(filters.showInStore ? 1 : 0); + mongoQuery.showInStore = filters.showInStore; } if (filters.search) { - const searchTerm = `%${filters.search.toLowerCase()}%`; - query += ` AND (LOWER(name) LIKE ? OR LOWER(description) LIKE ? OR LOWER(genre) LIKE ?)`; - params.push(searchTerm, searchTerm, searchTerm); + const searchTerm = new RegExp(filters.search, 'i'); + mongoQuery.$or = [ + { name: searchTerm }, + { description: searchTerm }, + { genre: searchTerm } + ]; } - if (orderBy) query += ` ORDER BY ${orderBy}`; - if (limit) query += ` LIMIT ${limit}`; + + const result = await db.collection('games').find(mongoQuery).toArray(); - return await this.databaseService.read(query, params); + // Convert MongoDB documents to Game interface if necessary + const games: Game[] = result.map(doc => ({ + id: doc.id, + gameId: doc.gameId, + name: doc.name, + description: doc.description, + price: doc.price, + owner_id: doc.owner_id, + showInStore: Boolean(doc.showInStore), + iconHash: doc.iconHash || null, + splashHash: doc.splashHash || null, + bannerHash: doc.bannerHash || null, + genre: doc.genre || null, + release_date: doc.release_date || null, + developer: doc.developer || null, + publisher: doc.publisher || null, + platforms: doc.platforms || [], + rating: doc.rating || 0, + website: doc.website || null, + trailer_link: doc.trailer_link || null, + multiplayer: Boolean(doc.multiplayer) + })); + return games; } async getGame(gameId: string): Promise { @@ -46,28 +92,137 @@ export class GameRepository { } async getGameForOwner(gameId: string, userId: string): Promise { - const rows = await this.databaseService.read( - `SELECT g.*, - CASE - WHEN g.owner_id = ? OR go.ownerId IS NOT NULL - THEN 1 ELSE 0 - END as can_download - FROM games g - LEFT JOIN game_owners go ON g.gameId = go.gameId AND go.ownerId = ? - WHERE g.gameId = ?`, - [userId, userId, gameId] - ); - return rows.length > 0 ? rows[0] : null; + // const rows = await this.databaseService.read( + // `SELECT g.*, + // CASE + // WHEN g.owner_id = ? OR go.ownerId IS NOT NULL + // THEN 1 ELSE 0 + // END as can_download + // FROM games g + // LEFT JOIN game_owners go ON g.gameId = go.gameId AND go.ownerId = ? + // WHERE g.gameId = ?`, + // [userId, userId, gameId] + // ); + // return rows.length > 0 ? rows[0] : null; + // MongoDB query to get game for owner + const db = await this.databaseService.getDb(); + const result = await db.collection('games').aggregate([ + { $match: { gameId: gameId } }, + { $lookup: { + from: 'game_owners', + let: { gameId: '$gameId' }, + pipeline: [ + { $match: { $expr: { $and: [ + { $eq: ['$gameId', '$$gameId'] }, + { $eq: ['$ownerId', userId] } + ] } } } + ], + as: 'owners' + } + }, + { $addFields: { + can_download: { + $cond: { + if: { $or: [ + { $eq: ['$owner_id', userId] }, + { $gt: [{ $size: '$owners' }, 0] } + ] }, + then: true, + else: false + } + } + } + }, + { $project: { + gameId: 1, + name: 1, + description: 1, + price: 1, + owner_id: 1, + showInStore: 1, + iconHash: 1, + splashHash: 1, + bannerHash: 1, + genre: 1, + release_date: 1, + developer: 1, + publisher: 1, + platforms: 1, + rating: 1, + website: 1, + trailer_link: 1, + multiplayer: 1, + can_download: 1 + } + } + ]).toArray(); + const games: Game = result.map(doc => ({ + id: doc._id.toString(), + gameId: doc.gameId, + name: doc.name, + description: doc.description, + price: doc.price, + owner_id: doc.owner_id, + showInStore: Boolean(doc.showInStore), + iconHash: doc.iconHash || null, + splashHash: doc.splashHash || null, + bannerHash: doc.bannerHash || null, + genre: doc.genre || null, + release_date: doc.release_date || null, + developer: doc.developer || null, + publisher: doc.publisher || null, + platforms: doc.platforms || [], + rating: doc.rating || 0, + website: doc.website || null, + trailer_link: doc.trailer_link || null, + multiplayer: Boolean(doc.multiplayer), + can_download : Boolean(doc.can_download) + }))[0] || null; + return games; } async getUserGames(userId: string): Promise { - return await this.databaseService.read( - `SELECT g.* - FROM games g - INNER JOIN game_owners go ON g.gameId = go.gameId - WHERE go.ownerId = ?`, - [userId] - ); + // return await this.databaseService.read( + // `SELECT g.* + // FROM games g + // INNER JOIN game_owners go ON g.gameId = go.gameId + // WHERE go.ownerId = ?`, + // [userId] + // ); + // MongoDB query to get user games + const db = await this.databaseService.getDb(); + const result = await db.collection('games').aggregate([ + { $lookup: { + from: 'game_owners', + localField: 'gameId', + foreignField: 'gameId', + as: 'owners' + } + }, + { $match: { 'owners.ownerId': userId } } + ]).toArray(); + const games: Game[] = result.map(doc => ({ + id: doc._id.toString(), + gameId: doc.gameId, + name: doc.name, + description: doc.description, + price: doc.price, + owner_id: doc.owner_id, + showInStore: Boolean(doc.showInStore), + iconHash: doc.iconHash || null, + splashHash: doc.splashHash || null, + bannerHash: doc.bannerHash || null, + genre: doc.genre || null, + release_date: doc.release_date || null, + developer: doc.developer || null, + publisher: doc.publisher || null, + platforms: doc.platforms || [], + rating: doc.rating || 0, + website: doc.website || null, + trailer_link: doc.trailer_link || null, + multiplayer: Boolean(doc.multiplayer) + })); + return games; } async listGames(): Promise { @@ -87,13 +242,47 @@ export class GameRepository { } async getUserOwnedGames(userId: string): Promise { - return await this.databaseService.read( - `SELECT g.* - FROM games g - INNER JOIN game_owners go ON g.gameId = go.gameId - WHERE go.ownerId = ?`, - [userId] - ); + // return await this.databaseService.read( + // `SELECT g.* + // FROM games g + // INNER JOIN game_owners go ON g.gameId = go.gameId + // WHERE go.ownerId = ?`, + // [userId] + // ); + // MongoDB query to get user owned games + const db = await this.databaseService.getDb(); + const result = await db.collection('games').aggregate([ + { $lookup: { + from: 'game_owners', + localField: 'gameId', + foreignField: 'gameId', + as: 'owners' + } + }, + { $match: { 'owners.ownerId': userId } } + ]).toArray(); + const games: Game[] = result.map(doc => ({ + id: doc._id.toString(), + gameId: doc.gameId, + name: doc.name, + description: doc.description, + price: doc.price, + owner_id: doc.owner_id, + showInStore: Boolean(doc.showInStore), + iconHash: doc.iconHash || null, + splashHash: doc.splashHash || null, + bannerHash: doc.bannerHash || null, + genre: doc.genre || null, + release_date: doc.release_date || null, + developer: doc.developer || null, + publisher: doc.publisher || null, + platforms: doc.platforms || [], + rating: doc.rating || 0, + website: doc.website || null, + trailer_link: doc.trailer_link || null, + multiplayer: Boolean(doc.multiplayer) + })); + return games; } async searchGames(query: string): Promise { @@ -105,31 +294,71 @@ export class GameRepository { } async createGame(game: Omit): Promise { - await this.databaseService.request( - `INSERT INTO games ( - gameId, name, description, price, owner_id, showInStore, download_link, - iconHash, splashHash, bannerHash, genre, release_date, developer, - publisher, platforms, rating, website, trailer_link, multiplayer - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [game.gameId, game.name, game.description, game.price, game.owner_id, game.showInStore ? 1 : 0, game.download_link, game.iconHash ?? null, game.splashHash ?? null, game.bannerHash ?? null, game.genre ?? null, game.release_date ?? null, game.developer ?? null, game.publisher ?? null, game.platforms ?? null, game.rating ?? 0, game.website ?? null, game.trailer_link ?? null, game.multiplayer ? 1 : 0] - ); + // await this.databaseService.request( + // `INSERT INTO games ( + // gameId, name, description, price, owner_id, showInStore, download_link, + // iconHash, splashHash, bannerHash, genre, release_date, developer, + // publisher, platforms, rating, website, trailer_link, multiplayer + // ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + // [game.gameId, game.name, game.description, game.price, game.owner_id, game.showInStore ? 1 : 0, game.download_link, game.iconHash ?? null, game.splashHash ?? null, game.bannerHash ?? null, game.genre ?? null, game.release_date ?? null, game.developer ?? null, game.publisher ?? null, game.platforms ?? null, game.rating ?? 0, game.website ?? null, game.trailer_link ?? null, game.multiplayer ? 1 : 0] + // ); + // MongoDB query to create a game + const db = await this.databaseService.getDb(); + await db.collection('games').insertOne({ + gameId: game.gameId, + name: game.name, + description: game.description, + price: game.price, + owner_id: game.owner_id, + showInStore: game.showInStore, + download_link: game.download_link || null, + iconHash: game.iconHash || null, + splashHash: game.splashHash || null, + bannerHash: game.bannerHash || null, + genre: game.genre || null, + release_date: game.release_date ? new Date(game.release_date) : null, + developer: game.developer || null, + publisher: game.publisher || null, + platforms: game.platforms || null, + rating: game.rating || 0, + website: game.website || null, + trailer_link: game.trailer_link || null, + multiplayer: game.multiplayer + }); } async updateGame(gameId: string, fields: string[], values: unknown[]): Promise { values.push(gameId); - await this.databaseService.request(`UPDATE games SET ${fields.join(', ')} WHERE gameId = ?`, values); + // await this.databaseService.request(`UPDATE games SET ${fields.join(', ')} WHERE gameId = ?`, values); + // MongoDB query to update a game + const db = await this.databaseService.getDb(); + const updateFields: any = {}; + fields.forEach((field, index) => { + updateFields[field] = values[index]; + }); + await db.collection('games').updateOne({ gameId: gameId }, { $set: updateFields }); } async deleteGame(gameId: string): Promise { - await this.databaseService.request('DELETE FROM games WHERE gameId = ?', [gameId]); - await this.databaseService.request('DELETE FROM game_owners WHERE gameId = ?', [gameId]); + // await this.databaseService.request('DELETE FROM games WHERE gameId = ?', [gameId]); + // await this.databaseService.request('DELETE FROM game_owners WHERE gameId = ?', [gameId]); + // MongoDB query to delete a game and its owners + const db = await this.databaseService.getDb(); + await db.collection('games').deleteOne({ gameId: gameId }); + await db.collection('game_owners').deleteMany({ gameId: gameId }); } async addOwner(gameId: string, ownerId: string): Promise { - await this.databaseService.request('INSERT INTO game_owners (gameId, ownerId) VALUES (?, ?)', [gameId, ownerId]); + // await this.databaseService.request('INSERT INTO game_owners (gameId, ownerId) VALUES (?, ?)', [gameId, ownerId]); + // MongoDB query to add an owner to a game + const db = await this.databaseService.getDb(); + await db.collection('game_owners').insertOne({ gameId: gameId, ownerId: ownerId }); } async removeOwner(gameId: string, ownerId: string): Promise { - await this.databaseService.request('DELETE FROM game_owners WHERE gameId = ? AND ownerId = ?', [gameId, ownerId]); + // await this.databaseService.request('DELETE FROM game_owners WHERE gameId = ? AND ownerId = ?', [gameId, ownerId]); + // MongoDB query to remove an owner from a game + const db = await this.databaseService.getDb(); + await db.collection('game_owners').deleteOne({ gameId: gameId, ownerId: ownerId }); } } diff --git a/src/repositories/GameViewRepository.ts b/src/repositories/GameViewRepository.ts index 775945c..3506b80 100644 --- a/src/repositories/GameViewRepository.ts +++ b/src/repositories/GameViewRepository.ts @@ -2,65 +2,153 @@ import { GameViewStats } from '../interfaces/GameView'; import { IDatabaseService } from '../services/DatabaseService'; export class GameViewRepository { - constructor(private databaseService: IDatabaseService) {} + constructor(private databaseService: IDatabaseService) { } async addView(gameId: string, viewerCookie: string, ipAddress: string, userAgent?: string): Promise { - await this.databaseService.request( - `INSERT INTO game_views (game_id, viewer_cookie, ip_address, viewed_at, user_agent) - VALUES (?, ?, ?, NOW(), ?)`, - [gameId, viewerCookie, ipAddress, userAgent || null] - ); + // await this.databaseService.request( + // `INSERT INTO game_views (game_id, viewer_cookie, ip_address, viewed_at, user_agent) + // VALUES (?, ?, ?, NOW(), ?)`, + // [gameId, viewerCookie, ipAddress, userAgent || null] + // ); + // MongoDB query to add a game view + const db = await this.databaseService.getDb(); + await db.collection('game_views').insertOne({ + game_id: gameId, + viewer_cookie: viewerCookie, + ip_address: ipAddress, + viewed_at: new Date(), + user_agent: userAgent || null + }); } async hasViewedToday(gameId: string, viewerCookie: string): Promise { - const result = await this.databaseService.read<{ count: number }>( - `SELECT COUNT(*) as count FROM game_views - WHERE game_id = ? AND viewer_cookie = ? - AND DATE(viewed_at) = CURDATE()`, - [gameId, viewerCookie] - ); - return result.length > 0 && result[0].count > 0; + // const result = await this.databaseService.read<{ count: number }>( + // `SELECT COUNT(*) as count FROM game_views + // WHERE game_id = ? AND viewer_cookie = ? + // AND DATE(viewed_at) = CURDATE()`, + // [gameId, viewerCookie] + // ); + // return result.length > 0 && result[0].count > 0; + // MongoDB query to check if the viewer has viewed the game today + const db = await this.databaseService.getDb(); + const count = await db.collection('game_views').countDocuments({ + game_id: gameId, + viewer_cookie: viewerCookie, + viewed_at: { $gte: new Date(new Date().setHours(0, 0, 0, 0)) } + }); + return count > 0; } async getGameViewStats(gameId: string): Promise { - const result = await this.databaseService.read( - `SELECT - ? as gameId, - COUNT(*) as total_views, - COUNT(DISTINCT viewer_cookie) as unique_views, - COUNT(CASE WHEN DATE(viewed_at) = CURDATE() THEN 1 END) as views_today, - COUNT(CASE WHEN viewed_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as views_this_week, - COUNT(CASE WHEN viewed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as views_this_month - FROM game_views - WHERE game_id = ?`, - [gameId, gameId] - ); - return result.length > 0 ? result[0] : { gameId, total_views: 0, unique_views: 0, views_today: 0, views_this_week: 0, views_this_month: 0 }; + // const result = await this.databaseService.read( + // `SELECT + // ? as gameId, + // COUNT(*) as total_views, + // COUNT(DISTINCT viewer_cookie) as unique_views, + // COUNT(CASE WHEN DATE(viewed_at) = CURDATE() THEN 1 END) as views_today, + // COUNT(CASE WHEN viewed_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as views_this_week, + // COUNT(CASE WHEN viewed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as views_this_month + // FROM game_views + // WHERE game_id = ?`, + // [gameId, gameId] + // ); + // return result.length > 0 ? result[0] : { gameId, total_views: 0, unique_views: 0, views_today: 0, views_this_week: 0, views_this_month: 0 }; + // MongoDB query to get game view stats + const db = await this.databaseService.getDb(); + const result = await db.collection('game_views').aggregate([ + { $match: { game_id: gameId } }, + { + $group: { + _id: '$game_id', + total_views: { $sum: 1 }, + unique_views: { $addToSet: '$viewer_cookie' }, + views_today: { $sum: { $cond: [{ $eq: [{ $dateToString: { format: "%Y-%m-%d", date: "$viewed_at" } }, new Date().toISOString().split('T')[0]] }, 1, 0] } }, + views_this_week: { $sum: { $cond: [{ $gte: ['$viewed_at', new Date(new Date().setDate(new Date().getDate() - 7))] }, 1, 0] } }, + views_this_month: { $sum: { $cond: [{ $gte: ['$viewed_at', new Date(new Date().setDate(new Date().getDate() - 30))] }, 1, 0] } } + } + } + ]).toArray(); + const stats: GameViewStats = { + gameId, + total_views: result.length > 0 ? result[0].total_views : 0, + unique_views: result.length > 0 ? result[0].unique_views.length : 0, + views_today: result.length > 0 ? result[0].views_today : 0, + views_this_week: result.length > 0 ? result[0].views_this_week : 0, + views_this_month: result.length > 0 ? result[0].views_this_month : 0, + }; + return stats; } async getViewsForGames(gameIds: string[]): Promise> { - if (gameIds.length === 0) return {}; + // if (gameIds.length === 0) return {}; + + // const placeholders = gameIds.map(() => '?').join(','); + // const result = await this.databaseService.read( + // `SELECT + // game_id as gameId, + // COUNT(*) as total_views, + // COUNT(DISTINCT viewer_cookie) as unique_views, + // COUNT(CASE WHEN DATE(viewed_at) = CURDATE() THEN 1 END) as views_today, + // COUNT(CASE WHEN viewed_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as views_this_week, + // COUNT(CASE WHEN viewed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as views_this_month + // FROM game_views + // WHERE game_id IN (${placeholders}) + // GROUP BY game_id`, + // gameIds + // ); + + // const stats: Record = {}; + // for (const row of result) { + // stats[row.gameId] = row; + // } - const placeholders = gameIds.map(() => '?').join(','); - const result = await this.databaseService.read( - `SELECT - game_id as gameId, - COUNT(*) as total_views, - COUNT(DISTINCT viewer_cookie) as unique_views, - COUNT(CASE WHEN DATE(viewed_at) = CURDATE() THEN 1 END) as views_today, - COUNT(CASE WHEN viewed_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1 END) as views_this_week, - COUNT(CASE WHEN viewed_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) THEN 1 END) as views_this_month - FROM game_views - WHERE game_id IN (${placeholders}) - GROUP BY game_id`, - gameIds - ); + // for (const gameId of gameIds) { + // if (!stats[gameId]) { + // stats[gameId] = { + // gameId, + // total_views: 0, + // unique_views: 0, + // views_today: 0, + // views_this_week: 0, + // views_this_month: 0, + // }; + // } + // } + // return stats; + if (gameIds.length === 0) return {}; + + // MongoDB query to get view stats for multiple games + const db = await this.databaseService.getDb(); + const result = await db.collection('game_views').aggregate([ + { $match: { game_id: { $in: gameIds } } }, + { + $group: { + _id: '$game_id', + total_views: { $sum: 1 }, + unique_views: { $addToSet: '$viewer_cookie' }, + views_today: { $sum: { $cond: [{ $eq: [{ $dateToString: { format: "%Y-%m-%d", date: "$viewed_at" } }, new Date().toISOString().split('T')[0]] }, 1, 0] } }, + views_this_week: { + $sum: { + $cond: [{ $gte: ['$viewed_at', new Date(new Date().setDate(new Date().getDate() - 7))] }, 1 + , 0] + } + }, + views_this_month: { $sum: { $cond: [{ $gte: ['$viewed_at', new Date(new Date().setDate(new Date().getDate() - 30))] }, 1, 0] } } + } + } + ]).toArray(); const stats: Record = {}; for (const row of result) { - stats[row.gameId] = row; + stats[row._id] = { + gameId: row._id, + total_views: row.total_views, + unique_views: row.unique_views.length, + views_today: row.views_today, + views_this_week: row.views_this_week, + views_this_month: row.views_this_month, + }; } - for (const gameId of gameIds) { if (!stats[gameId]) { stats[gameId] = { @@ -78,6 +166,9 @@ export class GameViewRepository { } async cleanupOldViews(daysToKeep: number = 365): Promise { - await this.databaseService.request('DELETE FROM game_views WHERE viewed_at < DATE_SUB(NOW(), INTERVAL ? DAY)', [daysToKeep]); + // await this.databaseService.request('DELETE FROM game_views WHERE viewed_at < DATE_SUB(NOW(), INTERVAL ? DAY)', [daysToKeep]); + // MongoDB query to delete old views + const db = await this.databaseService.getDb(); + await db.collection('game_views').deleteMany({ viewed_at: { $lt: new Date(new Date().setDate(new Date().getDate() - daysToKeep)) } }); } } diff --git a/src/repositories/InventoryRepository.ts b/src/repositories/InventoryRepository.ts index 644e1da..8d56df8 100644 --- a/src/repositories/InventoryRepository.ts +++ b/src/repositories/InventoryRepository.ts @@ -5,78 +5,84 @@ export class InventoryRepository { constructor(private databaseService: IDatabaseService) {} async deleteNonExistingItems(userId: string): Promise { - await this.databaseService.request( - `DELETE FROM inventories - WHERE user_id = ? - AND item_id NOT IN ( - SELECT itemId FROM items WHERE deleted IS NULL OR deleted = 0 - )`, - [userId] - ); + const db = await this.databaseService.getDb(); + const existingItemIds = await db.collection('items').find({ deleted: { $in: [null, false] } }).project({ itemId: 1 }).toArray(); + const existingItemIdsSet = new Set(existingItemIds.map(item => item.itemId)); + await db.collection('inventories').deleteMany({ + user_id: userId, + item_id: { $nin: Array.from(existingItemIdsSet) } + }); } async getInventory(filters: { userId?: string; itemId?: string; sellable?: boolean; purchasePrice?: number; uniqueId?: string; minAmount?: number } = {}): Promise { - let query = ` - SELECT - inv.user_id, - inv.item_id, - inv.amount, - inv.metadata, - inv.sellable, - inv.purchasePrice, - inv.rarity, - inv.custom_url_link, - i.itemId, - i.name, - i.description, - i.iconHash, - i.price, - i.owner, - i.showInStore - FROM inventories inv - INNER JOIN items i ON inv.item_id = i.itemId AND (i.deleted IS NULL OR i.deleted = 0) - WHERE 1=1 - `; - const params = []; + const db = await this.databaseService.getDb(); + + const matchStage: any = {}; + if (filters.userId) { - query += ' AND inv.user_id = ?'; - params.push(filters.userId); + matchStage.user_id = filters.userId; } if (filters.itemId) { - query += ' AND inv.item_id = ?'; - params.push(filters.itemId); + matchStage.item_id = filters.itemId; } if (filters.sellable !== undefined) { - query += ' AND inv.sellable = ?'; - params.push(filters.sellable ? 1 : 0); + matchStage.sellable = filters.sellable; + } + if (filters.minAmount !== undefined) { + matchStage.amount = { $gte: filters.minAmount }; } + if (filters.purchasePrice !== undefined) { - query += ' AND (inv.purchasePrice = ? OR (inv.purchasePrice IS NULL AND ? IS NULL))'; - params.push(filters.purchasePrice, filters.purchasePrice); + matchStage.$or = [ + { purchasePrice: filters.purchasePrice }, + { purchasePrice: null } + ]; } + if (filters.uniqueId) { - query += " AND JSON_EXTRACT(inv.metadata, '$._unique_id') = ?"; - params.push(filters.uniqueId); - } - if (filters.minAmount !== undefined) { - query += ' AND inv.amount >= ?'; - params.push(filters.minAmount); + matchStage['metadata._unique_id'] = filters.uniqueId; } - const items = await this.databaseService.read(query, params); - items.map(item => ({ - user_id: item.user_id, - item_id: item.item_id, - amount: item.amount, - metadata: item.metadata, - sellable: !!item.sellable, - purchasePrice: item.purchasePrice, - name: item.name, - description: item.description, - iconHash: item.iconHash, - price: item.purchasePrice, - rarity: item.rarity, - custom_url_link: item.custom_url_link, - })); + + const pipeline = [ + { $match: matchStage }, + { + $lookup: { + from: 'items', + let: { itemId: '$item_id' }, + pipeline: [ + { + $match: { + $expr: { $eq: ['$itemId', '$$itemId'] }, + deleted: { $in: [null, false] } + } + } + ], + as: 'itemData' + } + }, + { $unwind: '$itemData' }, + { + $project: { + user_id: 1, + item_id: 1, + amount: 1, + metadata: 1, + sellable: 1, + purchasePrice: 1, + rarity: 1, + custom_url_link: 1, + itemId: '$itemData.itemId', + name: '$itemData.name', + description: '$itemData.description', + iconHash: '$itemData.iconHash', + price: '$itemData.price', + owner: '$itemData.owner', + showInStore: '$itemData.showInStore' + } + } + ]; + + const items = await db.collection('inventories').aggregate(pipeline).toArray() as unknown as InventoryItem[]; return items; } @@ -102,41 +108,87 @@ export class InventoryRepository { } async addItem(userId: string, itemId: string, amount: number, metadata: { [key: string]: unknown } | undefined, sellable: boolean, purchasePrice: number | undefined, uuidv4: () => string): Promise { + const db = await this.databaseService.getDb(); + if (metadata) { + // Insert individual items with unique metadata + const documents = []; for (let i = 0; i < amount; i++) { const uniqueMetadata = { ...metadata, _unique_id: uuidv4() }; - await this.databaseService.request('INSERT INTO inventories (user_id, item_id, amount, metadata, sellable, purchasePrice, rarity, custom_url_link) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [userId, itemId, 1, JSON.stringify(uniqueMetadata), sellable ? 1 : 0, purchasePrice, metadata['rarity'], metadata['custom_url_link']]); + documents.push({ + user_id: userId, + item_id: itemId, + amount: 1, + metadata: uniqueMetadata, + sellable, + purchasePrice, + rarity: metadata['rarity'], + custom_url_link: metadata['custom_url_link'] + }); } + await db.collection('inventories').insertMany(documents); } else { - const items = await this.getInventory({ userId, itemId, sellable, purchasePrice }); - if (items.length > 0) { - await this.databaseService.request('UPDATE inventories SET amount = amount + ? WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = ? AND (purchasePrice = ? OR (purchasePrice IS NULL AND ? IS NULL))', [amount, userId, itemId, sellable ? 1 : 0, purchasePrice, purchasePrice]); - } else { - await this.databaseService.request('INSERT INTO inventories (user_id, item_id, amount, metadata, sellable, purchasePrice) VALUES (?, ?, ?, ?, ?, ?)', [userId, itemId, amount, null, sellable ? 1 : 0, purchasePrice]); + // Try to update existing stack + const existingFilter = { + user_id: userId, + item_id: itemId, + metadata: null, + sellable, + purchasePrice: purchasePrice === undefined ? null : purchasePrice + }; + + const result = await db.collection('inventories').updateOne( + existingFilter, + { $inc: { amount } } + ); + + if (result.matchedCount === 0) { + // Insert new stack if it doesn't exist + await db.collection('inventories').insertOne({ + user_id: userId, + item_id: itemId, + amount, + metadata: null, + sellable, + purchasePrice + }); } } } async setItemAmount(userId: string, itemId: string, amount: number): Promise { + const db = await this.databaseService.getDb(); if (amount <= 0) { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ?`, [userId, itemId]); + await db.collection('inventories').deleteMany({ user_id: userId, item_id: itemId }); return; } - const items = await this.getInventory({ userId, itemId }); - if (items.length > 0) { - await this.databaseService.request(`UPDATE inventories SET amount = ? WHERE user_id = ? AND item_id = ? AND metadata IS NULL`, [amount, userId, itemId]); - } else { - await this.databaseService.request(`INSERT INTO inventories (user_id, item_id, amount, metadata, sellable, purchasePrice) VALUES (?, ?, ?, ?, ?, ?)`, [userId, itemId, amount, null, 0, null]); + const result = await db.collection('inventories').updateOne( + { user_id: userId, item_id: itemId, metadata: null }, + { $set: { amount } } + ); + if (result.matchedCount === 0) { + await db.collection('inventories').insertOne({ + user_id: userId, + item_id: itemId, + amount, + metadata: null, + sellable: false, + purchasePrice: null + }); } } async updateItemMetadata(userId: string, itemId: string, uniqueId: string, metadata: object): Promise { + const db = await this.databaseService.getDb(); const metadataWithUniqueId = { ...metadata, _unique_id: uniqueId }; - const metadataJson = JSON.stringify(metadataWithUniqueId); - await this.databaseService.request("UPDATE inventories SET metadata = ? WHERE user_id = ? AND item_id = ? AND JSON_EXTRACT(metadata, '$._unique_id') = ?", [metadataJson, userId, itemId, uniqueId]); + await db.collection('inventories').updateOne( + { user_id: userId, item_id: itemId, 'metadata._unique_id': uniqueId }, + { $set: { metadata: metadataWithUniqueId } } + ); } async removeItem(userId: string, itemId: string, amount: number, dataItemIndex?: number): Promise { + const db = await this.databaseService.getDb(); const items = await this.getInventory({ userId, itemId }); if (typeof dataItemIndex === 'number' && items[dataItemIndex]) { @@ -144,9 +196,22 @@ export class InventoryRepository { const toRemoveFromStack = Math.min(amount, item.amount); const newAmount = item.amount - toRemoveFromStack; if (newAmount <= 0) { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND sellable = ? AND amount = ? AND purchasePrice = ? LIMIT 1`, [userId, item.item_id, item.sellable ? 1 : 0, item.amount, item.purchasePrice]); + await db.collection('inventories').deleteOne({ + user_id: userId, + item_id: item.item_id, + sellable: item.sellable, + amount: item.amount, + purchasePrice: item.purchasePrice + }); } else { - await this.databaseService.request(`UPDATE inventories SET amount = ? WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = ? AND amount = ? AND purchasePrice = ? LIMIT 1`, [newAmount, userId, item.item_id, item.sellable ? 1 : 0, item.amount, item.purchasePrice]); + await db.collection('inventories').updateOne({ + user_id: userId, + item_id: item.item_id, + metadata: null, + sellable: item.sellable, + amount: item.amount, + purchasePrice: item.purchasePrice + }, { $set: { amount: newAmount } }); } return; } @@ -157,19 +222,37 @@ export class InventoryRepository { const toRemoveFromStack = Math.min(remainingToRemove, item.amount); const newAmount = item.amount - toRemoveFromStack; if (newAmount <= 0) { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = ? AND amount = ? LIMIT 1`, [userId, itemId, item.sellable ? 1 : 0, item.amount]); + await db.collection('inventories').deleteOne({ + user_id: userId, + item_id: itemId, + metadata: null, + sellable: item.sellable, + amount: item.amount + }); } else { - await this.databaseService.request(`UPDATE inventories SET amount = ? WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = ? AND amount = ? LIMIT 1`, [newAmount, userId, itemId, item.sellable ? 1 : 0, item.amount]); + await db.collection('inventories').updateOne({ + user_id: userId, + item_id: itemId, + metadata: null, + sellable: item.sellable, + amount: item.amount + }, { $set: { amount: newAmount } }); } remainingToRemove -= toRemoveFromStack; } } async removeItemByUniqueId(userId: string, itemId: string, uniqueId: string): Promise { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND JSON_EXTRACT(metadata, '$._unique_id') = ?`, [userId, itemId, uniqueId]); + const db = await this.databaseService.getDb(); + await db.collection('inventories').deleteOne({ + user_id: userId, + item_id: itemId, + 'metadata._unique_id': uniqueId + }); } async removeSellableItem(userId: string, itemId: string, amount: number): Promise { + const db = await this.databaseService.getDb(); const items = await this.getInventory({ userId, itemId, sellable: true }); let remainingToRemove = amount; for (const item of items) { @@ -177,15 +260,26 @@ export class InventoryRepository { const toRemoveFromStack = Math.min(remainingToRemove, item.amount); const newAmount = item.amount - toRemoveFromStack; if (newAmount <= 0) { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = 1`, [userId, itemId]); + await db.collection('inventories').deleteOne({ + user_id: userId, + item_id: itemId, + metadata: null, + sellable: true + }); } else { - await this.databaseService.request(`UPDATE inventories SET amount = ? WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = 1`, [newAmount, userId, itemId]); + await db.collection('inventories').updateOne({ + user_id: userId, + item_id: itemId, + metadata: null, + sellable: true + }, { $set: { amount: newAmount } }); } remainingToRemove -= toRemoveFromStack; } } async removeSellableItemWithPrice(userId: string, itemId: string, amount: number, purchasePrice: number, dataItemIndex?: number): Promise { + const db = await this.databaseService.getDb(); const items = await this.getInventory({ userId, itemId, sellable: true, purchasePrice }); if (typeof dataItemIndex === 'number' && items[dataItemIndex]) { @@ -193,9 +287,23 @@ export class InventoryRepository { const toRemoveFromStack = Math.min(amount, item.amount); const newAmount = item.amount - toRemoveFromStack; if (newAmount <= 0) { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = 1 AND amount = ? AND purchasePrice = ? LIMIT 1`, [userId, itemId, item.amount, purchasePrice]); + await db.collection('inventories').deleteOne({ + user_id: userId, + item_id: itemId, + metadata: null, + sellable: true, + amount: item.amount, + purchasePrice + }); } else { - await this.databaseService.request(`UPDATE inventories SET amount = ? WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = 1 AND amount = ? AND purchasePrice = ? LIMIT 1`, [newAmount, userId, itemId, item.amount, purchasePrice]); + await db.collection('inventories').updateOne({ + user_id: userId, + item_id: itemId, + metadata: null, + sellable: true, + amount: item.amount, + purchasePrice + }, { $set: { amount: newAmount } }); } return; } @@ -206,15 +314,37 @@ export class InventoryRepository { const toRemoveFromStack = Math.min(remainingToRemove, item.amount); const newAmount = item.amount - toRemoveFromStack; if (newAmount <= 0) { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = 1 AND amount = ? AND purchasePrice = ? LIMIT 1`, [userId, itemId, item.amount, purchasePrice]); + await db.collection('inventories').deleteOne({ + user_id: userId, + item_id: itemId, + metadata: null, + sellable: true, + amount: item.amount, + purchasePrice + }); } else { - await this.databaseService.request(`UPDATE inventories SET amount = ? WHERE user_id = ? AND item_id = ? AND metadata IS NULL AND sellable = 1 AND amount = ? AND purchasePrice = ? LIMIT 1`, [newAmount, userId, itemId, item.amount, purchasePrice]); + await db.collection('inventories').updateOne({ + user_id: userId, + item_id: itemId, + metadata: null, + sellable: true, + amount: item.amount, + purchasePrice + }, { $set: { amount: newAmount } }); } remainingToRemove -= toRemoveFromStack; } } async transferItem(fromUserId: string, toUserId: string, itemId: string, uniqueId: string): Promise { - await this.databaseService.request(`UPDATE inventories SET user_id = ? WHERE user_id = ? AND item_id = ? AND JSON_EXTRACT(metadata, '$._unique_id') = ?`, [toUserId, fromUserId, itemId, uniqueId]); + const db = await this.databaseService.getDb(); + await db.collection('inventories').updateOne( + { + user_id: fromUserId, + item_id: itemId, + 'metadata._unique_id': uniqueId + }, + { $set: { user_id: toUserId } } + ); } } diff --git a/src/repositories/ItemRepository.ts b/src/repositories/ItemRepository.ts index 223fbec..7a1dbd4 100644 --- a/src/repositories/ItemRepository.ts +++ b/src/repositories/ItemRepository.ts @@ -5,47 +5,75 @@ export class ItemRepository { constructor(private databaseService: IDatabaseService) {} async createItem(item: Omit): Promise { + const db = await this.databaseService.getDb(); const existing = await this.getItem(item.itemId); if (existing) throw new Error('ItemId already exists'); - await this.databaseService.request( - `INSERT INTO items (itemId, name, description, price, owner, iconHash, showInStore, deleted) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, - [item.itemId, item.name ?? null, item.description ?? null, item.price ?? 0, item.owner, item.iconHash ?? null, item.showInStore ? 1 : 0, item.deleted ? 1 : 0] - ); + await db.collection('items').insertOne({ + ...item, + showInStore: !!item.showInStore, + deleted: !!item.deleted + }); } - async getItems(filters: { itemId?: string; owner?: string; showInStore?: boolean; deleted?: boolean; search?: string } = {}, select: string = '*', orderBy: string = 'name', limit?: number): Promise { - let query = `SELECT ${select} FROM items WHERE 1=1`; - const params = []; - if (filters.itemId) { - query += ' AND itemId = ?'; - params.push(filters.itemId); - } - if (filters.owner) { - query += ' AND owner = ?'; - params.push(filters.owner); - } - if (filters.showInStore !== undefined) { - query += ' AND showInStore = ?'; - params.push(filters.showInStore ? 1 : 0); + async getItems( + filters: { itemId?: string; owner?: string; showInStore?: boolean; deleted?: boolean; search?: string } = {}, + select: string = '*', + orderBy: string = 'name', + limit?: number + ): Promise { + const db = await this.databaseService.getDb(); + const query: any = {}; + if (filters.itemId) query.itemId = filters.itemId; + if (filters.owner) query.owner = filters.owner; + if (filters.showInStore !== undefined) query.showInStore = !!filters.showInStore; + if (filters.deleted !== undefined) query.deleted = !!filters.deleted; + if (filters.search) { + query.name = { $regex: filters.search, $options: 'i' }; } - if (filters.deleted !== undefined) { - query += ' AND deleted = ?'; - params.push(filters.deleted ? 1 : 0); + // select: if not '*', project only those fields + let projection: any = undefined; + if (select !== '*' && select.trim().length > 0) { + projection = {}; + select.split(',').map(f => f.trim()).forEach(f => projection[f] = 1); } - if (filters.search) { - const searchTerm = `%${filters.search.toLowerCase()}%`; - query += ' AND LOWER(name) LIKE ?'; - params.push(searchTerm); + let cursor = db.collection('items').find(query, { projection }); + // orderBy + if (orderBy) { + const sort: any = {}; + sort[orderBy] = 1; + cursor = cursor.sort(sort); } - query += ` ORDER BY ${orderBy}`; - if (limit) query += ` LIMIT ${limit}`; - return this.databaseService.read(query, params); + if (limit) cursor = cursor.limit(limit); + const itemsIterations = await cursor.toArray(); + const items: Item[] = itemsIterations.map(doc => ({ + itemId: doc.itemId, + name: doc.name, + description: doc.description, + price: doc.price, + owner: doc.owner, + iconHash: doc.iconHash, + showInStore: doc.showInStore, + deleted: doc.deleted + })); + return items as Item[]; + // return cursor.toArray() as Promise; } async getItem(itemId: string): Promise { - const items = await this.getItems({ itemId }); - return items[0] || null; + const db = await this.databaseService.getDb(); + const items = await db.collection('items').find({ itemId }).toArray(); + if (items.length === 0) return null; + const doc = items[0]; + return { + itemId: doc.itemId, + name: doc.name, + description: doc.description, + price: doc.price, + owner: doc.owner, + iconHash: doc.iconHash, + showInStore: doc.showInStore, + deleted: doc.deleted + }; } async getAllItems(): Promise { @@ -60,15 +88,23 @@ export class ItemRepository { return this.getItems({ owner: userId, deleted: false }, 'itemId, name, description, owner, price, iconHash, showInStore'); } - async updateItem(itemId: string, item: Partial>, buildUpdateFields: (obj: Record, skip?: string[]) => { fields: string[]; values: unknown[] }): Promise { - const { fields, values } = buildUpdateFields(item); - if (!fields.length) return; - values.push(itemId); - await this.databaseService.request(`UPDATE items SET ${fields.join(', ')} WHERE itemId = ?`, values); + async updateItem( + itemId: string, + item: Partial>, + buildUpdateFields: (obj: Record, skip?: string[]) => { fields: string[]; values: unknown[] } + ): Promise { + const db = await this.databaseService.getDb(); + // buildUpdateFields is not needed for MongoDB, just use the item object + if (!Object.keys(item).length) return; + await db.collection('items').updateOne( + { itemId }, + { $set: item } + ); } async deleteItem(itemId: string): Promise { - await this.databaseService.request('UPDATE items SET deleted = 1 WHERE itemId = ?', [itemId]); + const db = await this.databaseService.getDb(); + await db.collection('items').updateOne({ itemId }, { $set: { deleted: true } }); } async searchItemsByName(query: string): Promise { diff --git a/src/repositories/LobbyRepository.ts b/src/repositories/LobbyRepository.ts index ac5468c..681835f 100644 --- a/src/repositories/LobbyRepository.ts +++ b/src/repositories/LobbyRepository.ts @@ -6,24 +6,19 @@ export class LobbyRepository { constructor(private databaseService: IDatabaseService) {} async getLobbies(filters: { lobbyId?: string; userId?: string } = {}): Promise { - const query = 'SELECT lobbyId, users FROM lobbies WHERE 1=1'; - const rows = await this.databaseService.read<{ lobbyId: string; users: string[] }>(query); + const db = await this.databaseService.getDb(); + const query: any = {}; + if (filters.lobbyId) query.lobbyId = filters.lobbyId; + if (filters.userId) query.users = filters.userId; + const rows = await db.collection('lobbies').find(query).toArray(); const lobbies: Lobby[] = []; for (const row of rows) { - if (filters.userId && row.users.indexOf(filters.userId) !== -1 && filters.userId) { - const users = await this.getUsersByIds(row.users); - lobbies.push({ - lobbyId: row.lobbyId, - users, - }); - } else if (filters.lobbyId && row.lobbyId === filters.lobbyId) { - const users = await this.getUsersByIds(row.users); - lobbies.push({ - lobbyId: row.lobbyId, - users, - }); - } + const users = await this.getUsersByIds(row.users); + lobbies.push({ + lobbyId: row.lobbyId, + users, + }); } return lobbies; } @@ -44,22 +39,41 @@ export class LobbyRepository { } async createLobby(lobbyId: string, users: string[] = []): Promise { - await this.databaseService.request('INSERT INTO lobbies (lobbyId, users) VALUES (?, ?)', [lobbyId, JSON.stringify(users)]); + const db = await this.databaseService.getDb(); + await db.collection('lobbies').insertOne({ lobbyId, users }); } async updateLobbyUsers(lobbyId: string, users: PublicUser[]): Promise { + const db = await this.databaseService.getDb(); const usersIds = await this.getUsersIdOnly(users); - await this.databaseService.request('UPDATE lobbies SET users = ? WHERE lobbyId = ?', [JSON.stringify(usersIds), lobbyId]); + await db.collection('lobbies').updateOne( + { lobbyId }, + { $set: { users: usersIds } } + ); } async deleteLobby(lobbyId: string): Promise { - await this.databaseService.request('DELETE FROM lobbies WHERE lobbyId = ?', [lobbyId]); + const db = await this.databaseService.getDb(); + await db.collection('lobbies').deleteOne({ lobbyId }); } private async getUsersByIds(userIds: string[]): Promise { - if (userIds.length === 0) return []; - - return await this.databaseService.read(`SELECT user_id, username, verified, admin FROM users WHERE user_id IN (${userIds.map(() => '?').join(',')}) AND disabled = 0`, userIds); + if (!userIds || userIds.length === 0) return []; + const db = await this.databaseService.getDb(); + const result = await db.collection('users').find({ user_id: { $in: userIds }, disabled: 0 }).project({ user_id: 1, username: 1, verified: 1, admin: 1, badges: 1, beta_user: 1, created_at: 1, updated_at: 1, _id: 0 }).toArray(); + // const result = await this.databaseService.read(`SELECT user_id, username, verified, admin FROM users WHERE user_id IN (?) AND disabled = 0`, [userIds]); + const users: PublicUser[] = result.map(doc => ({ + user_id: doc.user_id, + username: doc.username, + verified: doc.verified, + admin: doc.admin, + badges: doc.badges || [], + beta_user: doc.beta_user || false, + created_at: doc.created_at, + updated_at: doc.updated_at, + isStudio: doc.isStudio || false, + })); + return users; } private async getUsersIdOnly(users: PublicUser[]): Promise { diff --git a/src/repositories/MarketListingRepository.ts b/src/repositories/MarketListingRepository.ts index afb16af..b736838 100644 --- a/src/repositories/MarketListingRepository.ts +++ b/src/repositories/MarketListingRepository.ts @@ -6,60 +6,87 @@ export class MarketListingRepository { constructor(private databaseService: IDatabaseService) {} async insertMarketListing(listing: MarketListing): Promise { - await this.databaseService.request( - `INSERT INTO market_listings (id, seller_id, item_id, price, status, metadata, created_at, updated_at, purchasePrice) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [listing.id, listing.seller_id, listing.item_id, listing.price, listing.status, JSON.stringify(listing.metadata || {}), listing.created_at, listing.updated_at, listing.purchasePrice] - ); + const db = await this.databaseService.getDb(); + await db.collection('market_listings').insertOne({ + ...listing, + metadata: listing.metadata || {} + }); } // --- INVENTORY HELPERS --- async removeInventoryItemByUniqueId(userId: string, itemId: string, uniqueId: string): Promise { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND JSON_EXTRACT(metadata, '$._unique_id') = ?`, [userId, itemId, uniqueId]); + const db = await this.databaseService.getDb(); + await db.collection('inventories').deleteOne({ + user_id: userId, + item_id: itemId, + 'metadata._unique_id': uniqueId + }); } async updateInventoryAmountOrDelete(userId: string, itemId: string, purchasePrice: number): Promise { - const [row] = await this.databaseService.read<{ amount: number }>(`SELECT amount FROM inventories WHERE user_id = ? AND item_id = ? AND purchasePrice = ?`, [userId, itemId, purchasePrice]); + const db = await this.databaseService.getDb(); + const filter = { user_id: userId, item_id: itemId, purchasePrice }; + const row = await db.collection('inventories').findOne(filter, { projection: { amount: 1 } }); if (row && row.amount > 1) { - await this.databaseService.request(`UPDATE inventories SET amount = amount - 1 WHERE user_id = ? AND item_id = ? AND purchasePrice = ?`, [userId, itemId, purchasePrice]); + await db.collection('inventories').updateOne(filter, { $inc: { amount: -1 } }); } else { - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND purchasePrice = ?`, [userId, itemId, purchasePrice]); + await db.collection('inventories').deleteOne(filter); } } async decrementOrDeleteInventory(userId: string, itemId: string): Promise { - await this.databaseService.request(`UPDATE inventories SET amount = amount - 1 WHERE user_id = ? AND item_id = ? AND amount > 0`, [userId, itemId]); - await this.databaseService.request(`DELETE FROM inventories WHERE user_id = ? AND item_id = ? AND amount = 0`, [userId, itemId]); + const db = await this.databaseService.getDb(); + await db.collection('inventories').updateMany( + { user_id: userId, item_id: itemId, amount: { $gt: 0 } }, + { $inc: { amount: -1 } } + ); + await db.collection('inventories').deleteMany({ user_id: userId, item_id: itemId, amount: 0 }); } // --- MARKET LISTING GENERIC GETTER --- async getMarketListings(filters: { id?: string; sellerId?: string; itemId?: string; status?: string } = {}, select: string = '*', orderBy: string = 'created_at DESC', limit?: number): Promise { - let query = `SELECT ${select} FROM market_listings WHERE 1=1`; - const params = []; - if (filters.id) { - query += ' AND id = ?'; - params.push(filters.id); - } - if (filters.sellerId) { - query += ' AND seller_id = ?'; - params.push(filters.sellerId); - } - if (filters.itemId) { - query += ' AND item_id = ?'; - params.push(filters.itemId); + const db = await this.databaseService.getDb(); + const query: any = {}; + if (filters.id) query.id = filters.id; + if (filters.sellerId) query.seller_id = filters.sellerId; + if (filters.itemId) query.item_id = filters.itemId; + if (filters.status) query.status = filters.status; + let cursor = db.collection('market_listings').find(query); + // sort handling (simple implementation splitting by space) + if (orderBy) { + const parts = orderBy.split(',').map(p => p.trim()); + const sort: any = {}; + parts.forEach(p => { + const [field, dir] = p.split(' '); + sort[field] = dir && dir.toUpperCase() === 'DESC' ? -1 : 1; + }); + cursor = cursor.sort(sort); } - if (filters.status) { - query += ' AND status = ?'; - params.push(filters.status); - } - query += ` ORDER BY ${orderBy}`; - if (limit) query += ` LIMIT ${limit}`; - return this.databaseService.read(query, params); + if (limit) cursor = cursor.limit(limit); + const listings = await cursor.toArray(); + const marketListings: MarketListing[] = listings.map(doc => ({ + id: doc.id, + seller_id: doc.seller_id, + item_id: doc.item_id, + price: doc.price, + status: doc.status, + metadata: doc.metadata || {}, + created_at: doc.created_at.toISOString(), + updated_at: doc.updated_at.toISOString(), + purchasePrice: doc.purchasePrice || null, + buyer_id: doc.buyer_id || null, + sold_at: doc.sold_at ? doc.sold_at.toISOString() : null, + rarity: doc.rarity || null + })); + return marketListings; + } // --- Surcharges utilisant la méthode générique --- async getMarketListingById(listingId: string, sellerId?: string): Promise { - const listings = await this.getMarketListings({ id: listingId, sellerId, status: 'active' }); + const filters: any = { id: listingId, status: 'active' }; + if (sellerId) filters.seller_id = sellerId; + const listings = await this.getMarketListings(filters); return listings[0] || null; } @@ -69,18 +96,39 @@ export class MarketListingRepository { } async getMarketListingsByUser(userId: string): Promise { - return this.databaseService.read( - `SELECT - ml.*, - i.name as item_name, - i.description as item_description, - i.iconHash as item_icon_hash - FROM market_listings ml - JOIN items i ON ml.item_id = i.itemId - WHERE ml.seller_id = ? - ORDER BY ml.created_at DESC`, - [userId] - ); + const db = await this.databaseService.getDb(); + const pipeline = [ + { $match: { seller_id: userId } }, + { + $lookup: { + from: 'items', + localField: 'item_id', + foreignField: 'itemId', + as: 'itemData' + } + }, + { $unwind: '$itemData' }, + { $sort: { created_at: -1 } }, + { + $project: { + 'id': 1, + 'seller_id': 1, + 'item_id': 1, + 'price': 1, + 'status': 1, + 'metadata': 1, + 'created_at': 1, + 'updated_at': 1, + 'purchasePrice': 1, + 'buyer_id': 1, + 'sold_at': 1, + item_name: '$itemData.name', + item_description: '$itemData.description', + item_icon_hash: '$itemData.iconHash' + } + } + ]; + return db.collection('market_listings').aggregate(pipeline).toArray() as Promise; } async getActiveListingsForItem(itemId: string): Promise { @@ -88,62 +136,124 @@ export class MarketListingRepository { } async getEnrichedMarketListings(limit: number, offset: number): Promise { - return this.databaseService.read( - `SELECT - ml.*, - i.name as item_name, - i.description as item_description, - i.iconHash as item_icon_hash - FROM market_listings ml - JOIN items i ON ml.item_id = i.itemId - WHERE ml.status = 'active' AND (i.deleted IS NULL OR i.deleted = 0) - ORDER BY ml.created_at DESC - LIMIT ? OFFSET ?`, - [limit, offset] - ); + const db = await this.databaseService.getDb(); + const pipeline: any[] = [ + { $match: { status: 'active' } }, + { + $lookup: { + from: 'items', + localField: 'item_id', + foreignField: 'itemId', + as: 'itemData' + } + }, + { $unwind: '$itemData' }, + { $match: { 'itemData.deleted': { $in: [null, false] } } }, + { $sort: { created_at: -1 } }, + { $skip: offset }, + { $limit: limit }, + { + $project: { + id: 1, + seller_id: 1, + item_id: 1, + price: 1, + status: 1, + metadata: 1, + created_at: 1, + updated_at: 1, + purchasePrice: 1, + buyer_id: 1, + sold_at: 1, + item_name: '$itemData.name', + item_description: '$itemData.description', + item_icon_hash: '$itemData.iconHash' + } + } + ]; + return db.collection('market_listings').aggregate(pipeline).toArray() as Promise; } async searchMarketListings(searchTerm: string, limit: number): Promise { - return this.databaseService.read( - `SELECT - ml.*, - i.name as item_name, - i.description as item_description, - i.iconHash as item_icon_hash - FROM market_listings ml - JOIN items i ON ml.item_id = i.itemId - WHERE ml.status = 'active' - AND (i.deleted IS NULL OR i.deleted = 0) - AND i.name LIKE ? - ORDER BY ml.price ASC, ml.created_at ASC - LIMIT ?`, - [`%${searchTerm}%`, limit] - ); + const db = await this.databaseService.getDb(); + const pipeline: any[] = [ + { $match: { status: 'active' } }, + { + $lookup: { + from: 'items', + localField: 'item_id', + foreignField: 'itemId', + as: 'itemData' + } + }, + { $unwind: '$itemData' }, + { $match: { 'itemData.deleted': { $in: [null, false] }, 'itemData.name': { $regex: searchTerm, $options: 'i' } } }, + { $sort: { price: 1, created_at: 1 } }, + { $limit: limit }, + { + $project: { + id: 1, + seller_id: 1, + item_id: 1, + price: 1, + status: 1, + metadata: 1, + created_at: 1, + updated_at: 1, + purchasePrice: 1, + buyer_id: 1, + sold_at: 1, + item_name: '$itemData.name', + item_description: '$itemData.description', + item_icon_hash: '$itemData.iconHash' + } + } + ]; + return db.collection('market_listings').aggregate(pipeline).toArray() as Promise; } // --- UPDATE STATUS --- async updateMarketListingStatus(listingId: string, status: string, updatedAt: string): Promise { - await this.databaseService.request(`UPDATE market_listings SET status = ?, updated_at = ? WHERE id = ?`, [status, updatedAt, listingId]); + const db = await this.databaseService.getDb(); + await db.collection('market_listings').updateOne({ id: listingId }, { $set: { status, updated_at: updatedAt } }); } async updateMarketListingSold(listingId: string, buyerId: string, now: string): Promise { - await this.databaseService.request(`UPDATE market_listings SET status = 'sold', buyer_id = ?, sold_at = ?, updated_at = ? WHERE id = ?`, [buyerId, now, now, listingId]); + const db = await this.databaseService.getDb(); + await db.collection('market_listings').updateOne( + { id: listingId }, + { $set: { status: 'sold', buyer_id: buyerId, sold_at: now, updated_at: now } } + ); } async updateBuyOrderToFulfilled(buyOrderId: string, now: string): Promise { - await this.databaseService.request(`UPDATE buy_orders SET status = 'fulfilled', fulfilled_at = ?, updated_at = ? WHERE id = ?`, [now, now, buyOrderId]); + const db = await this.databaseService.getDb(); + await db.collection('buy_orders').updateOne( + { id: buyOrderId }, + { $set: { status: 'fulfilled', fulfilled_at: now, updated_at: now } } + ); } // --- INVENTORY ADD --- async addItemToInventory(inventoryItem: InventoryItem): Promise { + const db = await this.databaseService.getDb(); if (inventoryItem.metadata && inventoryItem.metadata._unique_id) { - await this.databaseService.request(`INSERT INTO inventories (user_id, item_id, amount, metadata, sellable, purchasePrice) VALUES (?, ?, ?, ?, ?, ?)`, [inventoryItem.user_id, inventoryItem.item_id, inventoryItem.amount, JSON.stringify(inventoryItem.metadata), inventoryItem.sellable, inventoryItem.purchasePrice]); + await db.collection('inventories').insertOne(inventoryItem); } else { - const existingResult = await this.databaseService.read(`SELECT * FROM inventories WHERE user_id = ? AND item_id = ? AND purchasePrice = ?`, [inventoryItem.user_id, inventoryItem.item_id, inventoryItem.purchasePrice || null]); - if (existingResult.length > 0) { - await this.databaseService.request(`UPDATE inventories SET amount = amount + ? WHERE user_id = ? AND item_id = ? AND purchasePrice = ?`, [inventoryItem.amount, inventoryItem.user_id, inventoryItem.item_id, inventoryItem.purchasePrice || null]); + const filter = { + user_id: inventoryItem.user_id, + item_id: inventoryItem.item_id, + purchasePrice: inventoryItem.purchasePrice || null, + metadata: null + }; + const existing = await db.collection('inventories').findOne(filter); + if (existing) { + await db.collection('inventories').updateOne(filter, { $inc: { amount: inventoryItem.amount } }); } else { - await this.databaseService.request(`INSERT INTO inventories (user_id, item_id, amount, metadata, sellable, purchasePrice) VALUES (?, ?, ?, ?, ?, ?)`, [inventoryItem.user_id, inventoryItem.item_id, inventoryItem.amount, null, inventoryItem.sellable, inventoryItem.purchasePrice]); + await db.collection('inventories').insertOne({ + ...inventoryItem, + metadata: null + }); } } } diff --git a/src/repositories/OAuth2Repository.ts b/src/repositories/OAuth2Repository.ts index 371baac..dd48975 100644 --- a/src/repositories/OAuth2Repository.ts +++ b/src/repositories/OAuth2Repository.ts @@ -9,27 +9,31 @@ export class OAuth2Repository { async createApp(owner_id: string, name: string, redirect_urls: string[]): Promise { const client_id = v4(); const client_secret = v4(); - await this.db.request('INSERT INTO oauth2_apps (owner_id, client_id, client_secret, name, redirect_urls) VALUES (?, ?, ?, ?, ?)', [owner_id, client_id, client_secret, name, JSON.stringify(redirect_urls)]); + const db = await this.db.getDb(); + await db.collection('oauth2_apps').insertOne({ + owner_id, + client_id, + client_secret, + name, + redirect_urls + }); return { owner_id, client_id, client_secret, name, redirect_urls }; } async getApps(filters: { owner_id?: string; client_id?: string } = {}, select: string = '*'): Promise { - let query = `SELECT ${select} FROM oauth2_apps WHERE 1=1`; - const params = []; - if (filters.owner_id) { - query += ' AND owner_id = ?'; - params.push(filters.owner_id); - } - if (filters.client_id) { - query += ' AND client_id = ?'; - params.push(filters.client_id); - } - const apps = await this.db.read(query, params); - - return apps.map(app => ({ - ...app, - redirect_urls: typeof app.redirect_urls === 'string' ? JSON.parse(app.redirect_urls) : app.redirect_urls, + const db = await this.db.getDb(); + const query: any = {}; + if (filters.owner_id) query.owner_id = filters.owner_id; + if (filters.client_id) query.client_id = filters.client_id; + const docs = await db.collection('oauth2_apps').find(query).toArray(); + const apps: OAuth2App[] = docs.map(doc => ({ + owner_id: doc.owner_id, + client_id: doc.client_id, + client_secret: doc.client_secret, + name: doc.name, + redirect_urls: doc.redirect_urls })); + return apps; } async getAppsByOwner(owner_id: string): Promise { @@ -44,7 +48,11 @@ export class OAuth2Repository { redirect_urls: string[]; }> > { - const apps = await this.db.read('SELECT client_id, client_secret, name, redirect_urls FROM oauth2_apps WHERE owner_id = ?', [owner_id]); + const db = await this.db.getDb(); + const apps = await db.collection('oauth2_apps') + .find({ owner_id }) + .project({ client_id: 1, client_secret: 1, name: 1, redirect_urls: 1, _id: 0 }) + .toArray() as OAuth2App[]; return apps.map(app => ({ client_id: app.client_id, client_secret: app.client_secret, @@ -54,8 +62,16 @@ export class OAuth2Repository { } async getAppByClientId(client_id: string): Promise { - const apps = await this.getApps({ client_id }); - return apps[0] || null; + const db = await this.db.getDb(); + const app = await db.collection('oauth2_apps').findOne({ client_id }); + if (!app) return null; + return { + owner_id: app.owner_id, + client_id: app.client_id, + client_secret: app.client_secret, + name: app.name, + redirect_urls: app.redirect_urls + }; } async getFormattedAppByClientId(client_id: string): Promise<{ @@ -64,45 +80,51 @@ export class OAuth2Repository { name: string; redirect_urls: string[]; } | null> { - const rows = await this.db.read('SELECT client_id, client_secret, name, redirect_urls FROM oauth2_apps WHERE client_id = ?', [client_id]); - if (!rows) return null; - const app = rows[0]; - return { - client_id: app.client_id, - client_secret: app.client_secret, - name: app.name, - redirect_urls: app.redirect_urls, - }; + const db = await this.db.getDb(); + const app = await db.collection('oauth2_apps') + .findOne({ client_id }, { projection: { client_id: 1, client_secret: 1, name: 1, redirect_urls: 1, _id: 0 } }); + if (!app) return null; + return app as any; } async generateAuthCode(client_id: string, redirect_uri: string, user_id: string): Promise { const code = v4(); - await this.db.request('INSERT INTO oauth2_codes (code, client_id, redirect_uri, user_id) VALUES (?, ?, ?, ?)', [code, client_id, redirect_uri, user_id]); + const db = await this.db.getDb(); + await db.collection('oauth2_codes').insertOne({ code, client_id, redirect_uri, user_id }); return code; } async deleteApp(client_id: string, owner_id: string): Promise { - await this.db.request('DELETE FROM oauth2_apps WHERE client_id = ? AND owner_id = ?', [client_id, owner_id]); + const db = await this.db.getDb(); + await db.collection('oauth2_apps').deleteOne({ client_id, owner_id }); } async updateApp(client_id: string, owner_id: string, update: { name?: string; redirect_urls?: string[] }): Promise { - const { fields, values } = buildUpdateFields(update, { redirect_urls: v => JSON.stringify(v) }); - if (!fields.length) return; - values.push(client_id, owner_id); - await this.db.request(`UPDATE oauth2_apps SET ${fields.join(', ')} WHERE client_id = ? AND owner_id = ?`, values); + const db = await this.db.getDb(); + const set: any = {}; + if (update.name !== undefined) set.name = update.name; + if (update.redirect_urls !== undefined) set.redirect_urls = update.redirect_urls; + if (Object.keys(set).length === 0) return; + await db.collection('oauth2_apps').updateOne({ client_id, owner_id }, { $set: set }); } async getUserByCode(code: string, client_id: string): Promise { - const users = await this.db.read( - `SELECT u.username, u.user_id, u.email, u.balance, u.verified, - u.steam_username, u.steam_avatar_url, u.steam_id, u.discord_id, u.google_id - FROM oauth2_codes c - INNER JOIN oauth2_apps a ON c.client_id = a.client_id - INNER JOIN users u ON c.user_id = u.user_id - WHERE c.code = ? AND c.client_id = ?`, - [code, client_id] - ); - return users[0] || null; + const db = await this.db.getDb(); + const result = await db.collection('oauth2_codes').aggregate([ + { $match: { code, client_id } }, + { + $lookup: { + from: 'users', + localField: 'user_id', + foreignField: 'user_id', + as: 'userData' + } + }, + { $unwind: '$userData' }, + { $replaceRoot: { newRoot: '$userData' } }, + { $project: { _id: 0 } } + ]).toArray() as Oauth2User[]; + return result[0] || null; } } diff --git a/src/repositories/StudioRepository.ts b/src/repositories/StudioRepository.ts index dba86ed..294d0bd 100644 --- a/src/repositories/StudioRepository.ts +++ b/src/repositories/StudioRepository.ts @@ -1,3 +1,4 @@ +import { Studio } from 'interfaces/Studio'; import { IDatabaseService } from '../services/DatabaseService'; export class StudioRepository { @@ -7,21 +8,50 @@ export class StudioRepository { return Array.isArray(users) ? users : JSON.parse(users); } - async getStudio(user_id: string) { - const res = await this.db.read<{ user_id: string; admin_id: string; users: string[] }>('SELECT * FROM studios WHERE user_id = ?', [user_id]); - return res[0] ?? null; + async getStudio(user_id: string): Promise { + const db = await this.db.getDb(); + const studio = await db.collection('studios').findOne({ user_id }); + if (!studio) return null; + return { + ...studio, + users: studio.users, + admin_id: studio.admin_id, + user_id: studio.user_id, + me: studio.user_id, + }; } async setStudioProperties(user_id: string, admin_id: string, userIds: string[]) { - await this.db.request('UPDATE studios SET admin_id = ?, users = ? WHERE user_id = ?', [admin_id, JSON.stringify(userIds), user_id]); + const db = await this.db.getDb(); + await db.collection('studios').updateOne( + { user_id }, + { $set: { admin_id, users: userIds } } + ); } - async getUserStudios(user_id: string) { - const studios = await this.db.read<{ user_id: string; admin_id: string; users: string }>('SELECT * FROM studios WHERE admin_id = ? OR users LIKE ?', [user_id, `%"${user_id}"%`]); - return studios.map(s => ({ ...s, users: this.parseUsers(s.users) })); + async getUserStudios(user_id: string): Promise> { + const db = await this.db.getDb(); + const docs = await db.collection('studios').find({ + $or: [ + { admin_id: user_id }, + { users: user_id } + ] + }).toArray(); + // return docs.map(s => ({ ...s, users: this.parseUsers(s.users) })); + const studios: Array<{ user_id: string; admin_id: string; users: string[] }> = []; + for (const doc of docs) { + const users = this.parseUsers(doc.users); + studios.push({ + user_id: doc.user_id, + admin_id: doc.admin_id, + users, + }); + } + return studios; } async createStudio(user_id: string, admin_id: string) { - await this.db.request('INSERT INTO studios (user_id, admin_id, users) VALUES (?, ?, ?)', [user_id, admin_id, JSON.stringify([])]); + const db = await this.db.getDb(); + await db.collection('studios').insertOne({ user_id, admin_id, users: [] }); } } diff --git a/src/repositories/TradeRepository.ts b/src/repositories/TradeRepository.ts index 99c2121..0ac94f5 100644 --- a/src/repositories/TradeRepository.ts +++ b/src/repositories/TradeRepository.ts @@ -2,46 +2,77 @@ import { Trade } from '../interfaces/Trade'; import { IDatabaseService } from '../services/DatabaseService'; export class TradeRepository { - constructor(private db: IDatabaseService) {} - - async findPendingTrade(fromUserId: string, toUserId: string) { - const trades = await this.db.read( - `SELECT * FROM trades - WHERE status = 'pending' - AND ((fromUserId = ? AND toUserId = ?) OR (fromUserId = ? AND toUserId = ?)) - ORDER BY createdAt DESC - LIMIT 1`, - [fromUserId, toUserId, toUserId, fromUserId] - ); - return trades[0] ?? null; + constructor(private db: IDatabaseService) { } + + async findPendingTrade(fromUserId: string, toUserId: string): Promise { + const db = await this.db.getDb(); + const result = await db.collection('trades') + .find({ + status: 'pending', + $or: [ + { fromUserId, toUserId }, + { fromUserId: toUserId, toUserId: fromUserId } + ] + }) + .sort({ createdAt: -1 }) + .limit(1) + .next(); + + const trade = result as Trade | null; + return trade || null; } - async createTrade(trade: Trade) { - await this.db.request( - `INSERT INTO trades (id, fromUserId, toUserId, fromUserItems, toUserItems, approvedFromUser, approvedToUser, status, createdAt, updatedAt) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - [trade.id, trade.fromUserId, trade.toUserId, JSON.stringify(trade.fromUserItems), JSON.stringify(trade.toUserItems), 0, 0, trade.status, trade.createdAt, trade.updatedAt] - ); + async createTrade(trade: Trade): Promise { + const db = await this.db.getDb(); + await db.collection('trades').insertOne({ + ...trade, + fromUserItems: trade.fromUserItems, + toUserItems: trade.toUserItems, + approvedFromUser: 0, + approvedToUser: 0 + }); } - async getTradeById(id: string) { - const trades = await this.db.read('SELECT * FROM trades WHERE id = ?', [id]); - return trades[0] ?? null; + async getTradeById(id: string): Promise { + const db = await this.db.getDb(); + + const result = await db.collection('trades').findOne({ id }); + return result as Trade | null; } - async getTradesByUser(userId: string) { - return this.db.read('SELECT * FROM trades WHERE fromUserId = ? OR toUserId = ? ORDER BY createdAt DESC', [userId, userId]); + async getTradesByUser(userId: string): Promise { + const db = await this.db.getDb(); + const result = await db.collection('trades') + .find({ $or: [{ fromUserId: userId }, { toUserId: userId }] }) + .sort({ createdAt: -1 }) + .toArray(); + const trades: Trade[] = result.map( + doc => ({ + id: doc.id, + fromUserId: doc.fromUserId, + toUserId: doc.toUserId, + fromUserItems: doc.fromUserItems, + toUserItems: doc.toUserItems, + approvedFromUser: doc.approvedFromUser, + approvedToUser: doc.approvedToUser, + createdAt: doc.createdAt, + updatedAt: doc.updatedAt, + }) as Trade + ); + return trades as Trade[]; } - async updateTradeField(tradeId: string, field: string, value: unknown, updatedAt: string) { - await this.db.request(`UPDATE trades SET ${field} = ?, updatedAt = ? WHERE id = ?`, [value, updatedAt, tradeId]); + async updateTradeField(tradeId: string, field: string, value: unknown, updatedAt: string): Promise { + const db = await this.db.getDb(); + await db.collection('trades').updateOne( + { id: tradeId }, + { $set: { [field]: value, updatedAt } } + ); } - async updateTradeFields(tradeId: string, fields: Record) { - const setClause = Object.keys(fields) - .map(f => `${f} = ?`) - .join(', '); - const values = [...Object.values(fields), tradeId]; - await this.db.request(`UPDATE trades SET ${setClause} WHERE id = ?`, values); + async updateTradeFields(tradeId: string, fields: Record): Promise { + const db = await this.db.getDb(); + if (!Object.keys(fields).length) return; + await db.collection('trades').updateOne({ id: tradeId }, { $set: fields }); } } diff --git a/src/repositories/UserRepository.ts b/src/repositories/UserRepository.ts index 9edd6dc..8c1c58b 100644 --- a/src/repositories/UserRepository.ts +++ b/src/repositories/UserRepository.ts @@ -2,74 +2,207 @@ import { User } from '../interfaces/User'; import { IDatabaseService } from '../services/DatabaseService'; export class UserRepository { - constructor(private databaseService: IDatabaseService) {} + constructor(private databaseService: IDatabaseService) { } async getUserByAnyId(user_id: string, includeDisabled = false): Promise { - const base = '(user_id = ? OR discord_id = ? OR google_id = ? OR steam_id = ?)'; - const where = includeDisabled ? base : base + ' AND (disabled = 0 OR disabled IS NULL)'; - const users = await this.databaseService.read(`SELECT * FROM users WHERE ${where}`, [user_id, user_id, user_id, user_id]); - return users.length > 0 ? users[0] : null; + const db = await this.databaseService.getDb(); + const query: any = { + $or: [ + { user_id }, + { discord_id: user_id }, + { google_id: user_id }, + { steam_id: user_id } + ] + }; + if (!includeDisabled) { + query.$or = query.$or.map((clause: any) => ({ + ...clause, + $or: [{ disabled: 0 }, { disabled: { $exists: false } }] + })); + } + const users = await db.collection('users').findOne(query); + const user: User | null = users ? { + user_id: users.user_id, + username: users.username, + email: users.email, + password: users.password, + discord_id: users.discord_id, + google_id: users.google_id, + steam_id: users.steam_id, + steam_username: users.steam_username, + steam_avatar_url: users.steam_avatar_url, + forgot_password_token: users.forgot_password_token, + balance: users.balance, + free_balance: users.free_balance, + isStudio: users.isStudio, + disabled: users.disabled, + created_at: users.created_at, + verified: users.verified, + admin: users.admin, + badges: users.badges, + beta_user: users.beta_user, + webauthn_challenge: users.webauthn_challenge, + webauthn_credentials: users.webauthn_credentials, + updated_at: users.updated_at, + } : null; + return user; } async getAllUsers(includeDisabled = false): Promise { - if (includeDisabled) { - return await this.databaseService.read('SELECT * FROM users'); + const db = await this.databaseService.getDb(); + const query: any = {}; + if (!includeDisabled) { + query.$or = [{ disabled: 0 }, { disabled: { $exists: false } }]; } - return await this.databaseService.read('SELECT * FROM users WHERE (disabled = 0 OR disabled IS NULL)'); + // return db.collection('users').find(query).toArray(); + const users = await db.collection('users').find(query).toArray(); + return users.map(users => ({ + user_id: users.user_id, + username: users.username, + email: users.email, + password: users.password, + discord_id: users.discord_id, + google_id: users.google_id, + steam_id: users.steam_id, + steam_username: users.steam_username, + steam_avatar_url: users.steam_avatar_url, + forgot_password_token: users.forgot_password_token, + balance: users.balance, + free_balance: users.free_balance, + isStudio: users.isStudio, + disabled: users.disabled, + created_at: users.created_at, + verified: users.verified, + admin: users.admin, + badges: users.badges, + beta_user: users.beta_user, + webauthn_challenge: users.webauthn_challenge, + webauthn_credentials: users.webauthn_credentials, + updated_at: users.updated_at, + })); } async updateUserFields(user_id: string, fields: Partial>): Promise { - const updates: string[] = []; - const params: unknown[] = []; - if (fields.username !== undefined) { - updates.push('username = ?'); - params.push(fields.username); - } - if (fields.balance !== undefined) { - updates.push('balance = ?'); - params.push(fields.balance); - } - if (fields.password !== undefined) { - updates.push('password = ?'); - params.push(fields.password); - } - if (updates.length === 0) return; - params.push(user_id); - await this.databaseService.request(`UPDATE users SET ${updates.join(', ')} WHERE user_id = ?`, params); + const db = await this.databaseService.getDb(); + if (!Object.keys(fields).length) return; + await db.collection('users').updateOne({ user_id }, { $set: fields }); } async updateSteamFields(user_id: string, steam_id: string | null, steam_username: string | null, steam_avatar_url: string | null): Promise { - await this.databaseService.request('UPDATE users SET steam_id = ?, steam_username = ?, steam_avatar_url = ? WHERE user_id = ?', [steam_id, steam_username, steam_avatar_url, user_id]); + const db = await this.databaseService.getDb(); + await db.collection('users').updateOne( + { user_id }, + { $set: { steam_id, steam_username, steam_avatar_url } } + ); } async findByEmail(email: string): Promise { - const users = await this.databaseService.read('SELECT * FROM users WHERE email = ?', [email]); - return users.length > 0 ? users[0] : null; + const db = await this.databaseService.getDb(); + const user = await db.collection('users').findOne({ email }); + if (!user) return null; + return { + user_id: user.user_id, + username: user.username, + email: user.email, + password: user.password, + discord_id: user.discord_id, + google_id: user.google_id, + steam_id: user.steam_id, + steam_username: user.steam_username, + steam_avatar_url: user.steam_avatar_url, + forgot_password_token: user.forgot_password_token, + balance: user.balance, + free_balance: user.free_balance, + isStudio: user.isStudio, + disabled: user.disabled, + created_at: user.created_at, + verified: user.verified, + admin: user.admin, + badges: user.badges, + beta_user: user.beta_user, + webauthn_challenge: user.webauthn_challenge, + webauthn_credentials: user.webauthn_credentials, + updated_at: user.updated_at, + }; + // return db.collection('users').findOne({ email }); } async associateOAuth(user_id: string, provider: 'discord' | 'google', providerId: string): Promise { - const column = provider === 'discord' ? 'discord_id' : 'google_id'; - await this.databaseService.request(`UPDATE users SET ${column} = ? WHERE user_id = ?`, [providerId, user_id]); + const db = await this.databaseService.getDb(); + const update: any = {}; + update[provider === 'discord' ? 'discord_id' : 'google_id'] = providerId; + await db.collection('users').updateOne({ user_id }, { $set: update }); } async disableAccount(targetUserId: string): Promise { - await this.databaseService.request('UPDATE users SET disabled = 1 WHERE user_id = ?', [targetUserId]); + const db = await this.databaseService.getDb(); + await db.collection('users').updateOne({ user_id: targetUserId }, { $set: { disabled: 1 } }); } async reenableAccount(targetUserId: string): Promise { - await this.databaseService.request('UPDATE users SET disabled = 0 WHERE user_id = ?', [targetUserId]); + const db = await this.databaseService.getDb(); + await db.collection('users').updateOne({ user_id: targetUserId }, { $set: { disabled: 0 } }); } async searchUsers(): Promise { - return await this.databaseService.read(`SELECT user_id, username, verified, isStudio, admin, badges, beta_user, disabled FROM users LIMIT 100`); + const db = await this.databaseService.getDb(); + + const users = await db.collection('users') + .find({}, { projection: { user_id: 1, username: 1, verified: 1, isStudio: 1, admin: 1, badges: 1, beta_user: 1, disabled: 1, _id: 0 } }) + .limit(100) + .toArray(); + + return users.map(user => ({ + user_id: user.user_id, + username: user.username, + email: user.email ?? '', + password: user.password ?? '', + discord_id: user.discord_id ?? null, + google_id: user.google_id ?? null, + steam_id: user.steam_id ?? null, + steam_username: user.steam_username ?? null, + steam_avatar_url: user.steam_avatar_url ?? null, + forgot_password_token: user.forgot_password_token ?? null, + balance: user.balance ?? 0, + free_balance: user.free_balance ?? 0, + isStudio: user.isStudio ?? 0, + disabled: user.disabled ?? 0, + created_at: user.created_at ?? '', + verified: user.verified ?? 0, + admin: user.admin ?? 0, + badges: user.badges ?? [], + beta_user: user.beta_user ?? 0, + webauthn_challenge: user.webauthn_challenge ?? null, + webauthn_credentials: user.webauthn_credentials ?? null, + updated_at: user.updated_at ?? '', + })); } async createUser(user_id: string, username: string, email: string, password: string | null, provider?: 'discord' | 'google', providerId?: string, created_at?: string): Promise { - await this.databaseService.request('INSERT INTO users (user_id, username, email, password, balance, discord_id, google_id, created_at) VALUES (?, ?, ?, ?, 0, ?, ?, ?)', [user_id, username, email, password, provider === 'discord' ? providerId : null, provider === 'google' ? providerId : null, created_at || new Date().toISOString().slice(0, 19).replace('T', ' ')]); + const db = await this.databaseService.getDb(); + await db.collection('users').insertOne({ + user_id, + username, + email, + password, + balance: 0, + discord_id: provider === 'discord' ? providerId : null, + google_id: provider === 'google' ? providerId : null, + created_at: created_at || new Date().toISOString(), + disabled: 0 + }); } async createBrandUser(user_id: string, username: string): Promise { - await this.databaseService.request('INSERT INTO users (user_id, username, email, balance, isStudio) VALUES (?, ?, ?, 0, 1)', [user_id, username, '']); + const db = await this.databaseService.getDb(); + await db.collection('users').insertOne({ + user_id, + username, + email: '', + balance: 0, + isStudio: 1, + disabled: 0 + }); } async updateUserPassword(user_id: string, hashedPassword: string): Promise { @@ -77,37 +210,118 @@ export class UserRepository { } async getUserBySteamId(steamId: string): Promise { - const users = await this.databaseService.read('SELECT * FROM users WHERE steam_id = ? AND (disabled = 0 OR disabled IS NULL)', [steamId]); - return users.length > 0 ? users[0] : null; + const db = await this.databaseService.getDb(); + const user = await db.collection('users').findOne({ steam_id: steamId, $or: [{ disabled: 0 }, { disabled: { $exists: false } }] }); + if (!user) return null; + return { + user_id: user.user_id, + username: user.username, + email: user.email ?? '', + password: user.password ?? '', + discord_id: user.discord_id ?? null, + google_id: user.google_id ?? null, + steam_id: user.steam_id ?? null, + steam_username: user.steam_username ?? null, + steam_avatar_url: user.steam_avatar_url ?? null, + forgot_password_token: user.forgot_password_token ?? null, + balance: user.balance ?? 0, + free_balance: user.free_balance ?? 0, + isStudio: user.isStudio ?? 0, + disabled: user.disabled ?? 0, + created_at: user.created_at ?? '', + verified: user.verified ?? 0, + admin: user.admin ?? 0, + badges: user.badges ?? [], + beta_user: user.beta_user ?? 0, + webauthn_challenge: user.webauthn_challenge ?? null, + webauthn_credentials: user.webauthn_credentials ?? null, + updated_at: user.updated_at ?? '', + }; } async generatePasswordResetToken(email: string, token: string): Promise { - await this.databaseService.request('UPDATE users SET forgot_password_token = ? WHERE email = ?', [token, email]); + const db = await this.databaseService.getDb(); + await db.collection('users').updateOne({ email }, { $set: { forgot_password_token: token } }); } async deleteUser(user_id: string): Promise { - await this.databaseService.request('DELETE FROM users WHERE user_id = ?', [user_id]); + const db = await this.databaseService.getDb(); + await db.collection('users').deleteOne({ user_id }); } async updateWebauthnChallenge(user_id: string, challenge: string | null): Promise { - await this.databaseService.request('UPDATE users SET webauthn_challenge = ? WHERE user_id = ?', [challenge, user_id]); + const db = await this.databaseService.getDb(); + await db.collection('users').updateOne({ user_id }, { $set: { webauthn_challenge: challenge } }); } async addWebauthnCredential(userId: string, credentials: string): Promise { - await this.databaseService.request('UPDATE users SET webauthn_credentials = ? WHERE user_id = ?', [credentials, userId]); + const db = await this.databaseService.getDb(); + await db.collection('users').updateOne({ user_id: userId }, { $set: { webauthn_credentials: credentials } }); } async getUserByCredentialId(credentialId: string): Promise { - const users = await this.databaseService.read('SELECT * FROM users WHERE webauthn_credentials LIKE ? AND (disabled = 0 OR disabled IS NULL)', [`%${credentialId}%`]); - return users.length > 0 ? users[0] : null; + const db = await this.databaseService.getDb(); + const user = await db.collection('users').findOne({ webauthn_credentials: { $regex: credentialId }, $or: [{ disabled: 0 }, { disabled: { $exists: false } }] }); + if (!user) return null; + return { + user_id: user.user_id, + username: user.username, + email: user.email ?? '', + password: user.password ?? '', + discord_id: user.discord_id ?? null, + google_id: user.google_id ?? null, + steam_id: user.steam_id ?? null, + steam_username: user.steam_username ?? null, + steam_avatar_url: user.steam_avatar_url ?? null, + forgot_password_token: user.forgot_password_token ?? null, + balance: user.balance ?? 0, + free_balance: user.free_balance ?? 0, + isStudio: user.isStudio ?? 0, + disabled: user.disabled ?? 0, + created_at: user.created_at ?? '', + verified: user.verified ?? 0, + admin: user.admin ?? 0, + badges: user.badges ?? [], + beta_user: user.beta_user ?? 0, + webauthn_challenge: user.webauthn_challenge ?? null, + webauthn_credentials: user.webauthn_credentials ?? null, + updated_at: user.updated_at ?? '', + }; } async setAuthenticatorSecret(userId: string, secret: string | null): Promise { - await this.databaseService.request('UPDATE users SET authenticator_secret = ? WHERE user_id = ?', [secret, userId]); + const db = await this.databaseService.getDb(); + await db.collection('users').updateOne({ user_id: userId }, { $set: { authenticator_secret: secret } }); } async findByResetToken(reset_token: string): Promise { - const users = await this.databaseService.read('SELECT * FROM users WHERE forgot_password_token = ?', [reset_token]); - return users.length > 0 ? users[0] : null; + const db = await this.databaseService.getDb(); + const user = await db.collection('users').findOne({ forgot_password_token: reset_token }); + if (!user) return null; + return { + user_id: user.user_id, + username: user.username, + email: user.email ?? '', + password: user.password ?? '', + discord_id: user.discord_id ?? null, + google_id: user.google_id ?? null, + steam_id: user.steam_id ?? null, + steam_username: user.steam_username ?? null, + steam_avatar_url: user.steam_avatar_url ?? null, + forgot_password_token: user.forgot_password_token ?? null, + balance: user.balance ?? 0, + free_balance: user.free_balance ?? 0, + isStudio: user.isStudio ?? 0, + disabled: user.disabled ?? 0, + created_at: user.created_at ?? '', + verified: user.verified ?? 0, + admin: user.admin ?? 0, + badges: user.badges ?? [], + beta_user: user.beta_user ?? 0, + webauthn_challenge: user.webauthn_challenge ?? null, + webauthn_credentials: user.webauthn_credentials ?? null, + updated_at: user.updated_at ?? '', + }; + // return db.collection('users').findOne({ forgot_password_token: reset_token }); } } diff --git a/src/services/DatabaseService.ts b/src/services/DatabaseService.ts index 392261a..b18ad50 100644 --- a/src/services/DatabaseService.ts +++ b/src/services/DatabaseService.ts @@ -1,90 +1,56 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ - import { injectable } from 'inversify'; -import { Knex, knex } from 'knex'; +import { Db, MongoClient } from 'mongodb'; + import 'reflect-metadata'; export interface IDatabaseService { - request(query: string, params?: unknown[]): Promise; - read(query: string, params?: unknown[]): Promise; - getKnex(): Knex; + // request(query: string, params?: unknown[]): Promise; + // read(query: string, params?: unknown[]): Promise; + getDb(): Promise; + destroy(): Promise; } @injectable() export class DatabaseService implements IDatabaseService { - private db: Knex; + private client: MongoClient; + private db: Db | null = null; constructor() { - console.log(process.env.DB_HOST, process.env.DB_USER, process.env.DB_NAME); - - this.db = knex({ - client: 'mysql', - connection: { - host: process.env.DB_HOST, - user: process.env.DB_USER, - port: 3306, - password: process.env.DB_PASS, - database: process.env.DB_NAME, - }, - useNullAsDefault: true, - }); + this.client = new MongoClient(process.env.MONGO_URI as string); - this.db - .raw('SELECT 1') - .then(() => { - console.log('Database connection established'); + this.client.connect() + .then(async () => { + this.db = this.client.db(process.env.MONGO_DB); + await this.db.command({ ping: 1 }); + console.log('MongoDB connection established'); }) .catch(err => { - console.error('Database connection error:', err); + console.error('MongoDB connection error:', err); }); } - public getKnex(): Knex { - return this.db; - } - - public async request(query: string, params: unknown[] = []): Promise { - try { - await this.db.raw(query, params); - } catch (err) { - console.error('Error executing query', err); - throw err; - } - } - - public async read(query: string, params: unknown[] = []): Promise { - try { - const result = await this.db.raw(query, params); - - const rows = Array.isArray(result) && Array.isArray(result[0]) ? result[0] : result; - - if (!Array.isArray(rows)) { - console.warn('Database query returned non-array result:', rows); - return []; - } - - return rows.map((row: { [key: string]: string }) => { - for (const key in row) { - if (typeof row[key] === 'string') { - try { - const parsed = JSON.parse(row[key]); - row[key] = parsed; - } catch (e: unknown) { - // Not JSON, leave as string - } + public async getDb(): Promise { + // if (!this.db) { + // throw new Error('Database not initialized'); + // } + if (!this.db) { + // Wait for the database to be initialized + await new Promise((resolve, reject) => { + const checkDb = () => { + if (this.db) { + resolve(); } - } - return row as T; + setTimeout(checkDb, 100); + }; + checkDb(); }); - } catch (err) { - console.error('Error reading data', err); - throw err; } + + // We assume that by the time we get here, the database is initialized. If not, it will throw an error. + return this.db as Db; } public async destroy(): Promise { - await this.db.destroy(); + await this.client.close(); } -} - -export default DatabaseService; +} \ No newline at end of file diff --git a/src/services/StudioService.ts b/src/services/StudioService.ts index dc948a3..66d5dfa 100644 --- a/src/services/StudioService.ts +++ b/src/services/StudioService.ts @@ -30,7 +30,7 @@ export class StudioService implements IStudioService { async getStudio(user_id: string) { const studio = await this.studioRepository.getStudio(user_id); if (!studio) return null; - const users = await this.getUsersByIds(studio.users); + const users = await this.getUsersByIds(studio.users.map(u => u.user_id)); const me = (await this.userService.getUserWithPublicProfile(studio.user_id)) as StudioUser; return { ...studio, users, me }; } @@ -91,6 +91,9 @@ export class StudioService implements IStudioService { private async getUsersByIds(userIds: string[]) { if (!userIds.length) return []; - return this.db.read(`SELECT user_id, username, verified, admin FROM users WHERE user_id IN (${userIds.map(() => '?').join(',')})`, userIds); + const users = await this.userService.getAllUsersWithDisabled(); + // return users.filter(u => userIds.some(user => user.user_id === u.user_id)); + return users.filter(u => userIds.includes(u.user_id)); + // return this.db.read(`SELECT user_id, username, verified, admin FROM users WHERE user_id IN (${userIds.map(() => '?').join(',')})`, userIds); } } diff --git a/src/services/TradeService.ts b/src/services/TradeService.ts index f5889f8..facc54a 100644 --- a/src/services/TradeService.ts +++ b/src/services/TradeService.ts @@ -1,4 +1,3 @@ -import { InventoryItem } from 'interfaces/Inventory'; import { Item } from 'interfaces/Item'; import { inject, injectable } from 'inversify'; import { v4 } from 'uuid'; @@ -63,7 +62,26 @@ export class TradeService implements ITradeService { let itemsInfo: Record = {}; if (uniqueItemIds.length) { const placeholders = uniqueItemIds.map(() => '?').join(','); - const items = await this.databaseService.read(`SELECT * FROM items WHERE itemId IN (${placeholders}) AND (deleted IS NULL OR deleted = 0)`, uniqueItemIds); + // const items = await this.databaseService.read(`SELECT * FROM items WHERE itemId IN (${placeholders}) AND (deleted IS NULL OR deleted = 0)`, uniqueItemIds); + + //MongoDB query to get items info + const db = await this.databaseService.getDb(); + const result = await db.collection('items').find({ + itemId: { $in: uniqueItemIds }, + $or: [{ deleted: null }, { deleted: false }] + }).toArray(); + + const items = result.map(doc => ({ + itemId: doc.itemId, + name: doc.name, + description: doc.description, + price: doc.price, + owner: doc.owner, + iconHash: doc.iconHash, + showInStore: doc.showInStore, + deleted: doc.deleted + })) as Item[]; + itemsInfo = Object.fromEntries(items.map(item => [item.itemId, item])); } const enrich = (arr: TradeItem[]) => arr.map(item => ({ ...item, ...(itemsInfo[item.itemId] || {}) })); @@ -86,7 +104,26 @@ export class TradeService implements ITradeService { let itemsInfo: Record = {}; if (allItemIds.length) { const placeholders = allItemIds.map(() => '?').join(','); - const items = await this.databaseService.read(`SELECT * FROM items WHERE itemId IN (${placeholders}) AND (deleted IS NULL OR deleted = 0)`, allItemIds); + // const items = await this.databaseService.read(`SELECT * FROM items WHERE itemId IN (${placeholders}) AND (deleted IS NULL OR deleted = 0)`, allItemIds); + + //MongoDB query to get items info + const db = await this.databaseService.getDb(); + const result = await db.collection('items').find({ + itemId: { $in: allItemIds }, + $or: [{ deleted: null }, { deleted: false }] + }).toArray(); + + const items = result.map(doc => ({ + itemId: doc.itemId, + name: doc.name, + description: doc.description, + price: doc.price, + owner: doc.owner, + iconHash: doc.iconHash, + showInStore: doc.showInStore, + deleted: doc.deleted + })) as Item[]; + itemsInfo = Object.fromEntries(items.map(item => [item.itemId, item])); } const enrich = (arr: TradeItem[]) => arr.map(item => ({ ...item, ...(itemsInfo[item.itemId] || {}) })); @@ -126,18 +163,40 @@ export class TradeService implements ITradeService { // Vérification de la possession de l'item if (tradeItem.metadata?._unique_id) { - const inventoryItems = await this.databaseService.read<{ user_id: string; item_id: string; amount: number }>( - `SELECT user_id, item_id, amount FROM inventories - WHERE user_id = ? AND item_id = ? AND JSON_EXTRACT(metadata, '$._unique_id') = ?`, - [userId, tradeItem.itemId, tradeItem.metadata._unique_id] - ); + // const inventoryItems = await this.databaseService.read<{ user_id: string; item_id: string; amount: number }>( + // `SELECT user_id, item_id, amount FROM inventories + // WHERE user_id = ? AND item_id = ? AND JSON_EXTRACT(metadata, '$._unique_id') = ?`, + // [userId, tradeItem.itemId, tradeItem.metadata._unique_id] + // ); + const db = await this.databaseService.getDb(); + const result = await db.collection('inventories').find({ + user_id: userId, + item_id: tradeItem.itemId, + metadata: { $elemMatch: { _unique_id: tradeItem.metadata._unique_id } } + }).toArray(); + const inventoryItems = result.map(doc => ({ + user_id: doc.user_id, + item_id: doc.item_id, + amount: doc.amount + })) as { user_id: string; item_id: string; amount: number }[]; if (!inventoryItems.length) throw new Error('User does not have this specific item'); } else if (tradeItem.purchasePrice) { - const inventoryItems = await this.databaseService.read( - `SELECT user_id, item_id, amount FROM inventories - WHERE user_id = ? AND item_id = ? AND purchasePrice = ?`, - [userId, tradeItem.itemId, tradeItem.purchasePrice] - ); + // const inventoryItems = await this.databaseService.read( + // `SELECT user_id, item_id, amount FROM inventories + // WHERE user_id = ? AND item_id = ? AND purchasePrice = ?`, + // [userId, tradeItem.itemId, tradeItem.purchasePrice] + // ); + const db = await this.databaseService.getDb(); + const result = await db.collection('inventories').find({ + user_id: userId, + item_id: tradeItem.itemId, + purchasePrice: tradeItem.purchasePrice + }).toArray(); + const inventoryItems = result.map(doc => ({ + user_id: doc.user_id, + item_id: doc.item_id, + amount: doc.amount + })) as { user_id: string; item_id: string; amount: number }[]; const totalAvailable = inventoryItems.reduce((sum, item) => sum + item.amount, 0); if (totalAvailable < tradeItem.amount) throw new Error('User does not have enough of the item with specified purchase price'); } else { diff --git a/src/services/UserService.ts b/src/services/UserService.ts index d85f7de..7955ac5 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -1,7 +1,7 @@ import crypto from 'crypto'; import removeDiacritics from 'diacritics'; import { config } from 'dotenv'; -import { InventoryItem } from 'interfaces/Inventory'; +import { Game } from 'interfaces/Game'; import { Item } from 'interfaces/Item'; import { inject, injectable } from 'inversify'; import path from 'path'; @@ -243,120 +243,138 @@ export class UserService implements IUserService { } async getUserWithCompleteProfile(user_id: string): Promise<(User & UserExtensions) | null> { - const query = ` - SELECT - u.*, - CONCAT('[', GROUP_CONCAT( - CASE WHEN inv.item_id IS NOT NULL AND i.itemId IS NOT NULL THEN - JSON_OBJECT( - 'user_id', inv.user_id, - 'item_id', inv.item_id, - 'itemId', i.itemId, - 'name', i.name, - 'description', i.description, - 'amount', inv.amount, - 'iconHash', i.iconHash, - 'sellable', IF(inv.sellable = 1, 1, 0), - 'purchasePrice', inv.purchasePrice, - 'rarity', inv.rarity, - 'custom_url_link', inv.custom_url_link, - 'metadata', inv.metadata - ) - END - ), ']') as inventory, - (SELECT CONCAT('[', GROUP_CONCAT( - JSON_OBJECT( - 'itemId', oi.itemId, - 'name', oi.name, - 'description', oi.description, - 'owner', oi.owner, - 'price', oi.price, - 'iconHash', oi.iconHash, - 'showInStore', oi.showInStore - ) - ), ']') FROM items oi WHERE oi.owner = u.user_id AND (oi.deleted IS NULL OR oi.deleted = 0) AND oi.showInStore = 1 ORDER BY oi.name) as ownedItems, - (SELECT CONCAT('[', GROUP_CONCAT( - JSON_OBJECT( - 'gameId', g.gameId, - 'name', g.name, - 'description', g.description, - 'price', g.price, - 'owner_id', g.owner_id, - 'showInStore', g.showInStore, - 'iconHash', g.iconHash, - 'splashHash', g.splashHash, - 'bannerHash', g.bannerHash, - 'genre', g.genre, - 'release_date', g.release_date, - 'developer', g.developer, - 'publisher', g.publisher, - 'platforms', g.platforms, - 'rating', g.rating, - 'website', g.website, - 'trailer_link', g.trailer_link, - 'multiplayer', g.multiplayer, - 'download_link', g.download_link - ) - ), ']') FROM games g WHERE g.owner_id = u.user_id AND g.showInStore = 1 ORDER BY g.name) as createdGames - FROM users u - LEFT JOIN inventories inv ON u.user_id = inv.user_id AND inv.amount > 0 - LEFT JOIN items i ON inv.item_id = i.itemId AND (i.deleted IS NULL OR i.deleted = 0) - WHERE (u.user_id = ? OR u.discord_id = ? OR u.google_id = ? OR u.steam_id = ?) - GROUP BY u.user_id - `; - - await this.databaseService.request( - `DELETE FROM inventories - WHERE user_id = ( - SELECT user_id FROM users - WHERE user_id = ? - OR discord_id = ? - OR google_id = ? - OR steam_id = ? - ) - AND item_id NOT IN ( - SELECT itemId FROM items WHERE deleted IS NULL OR deleted = 0 - )`, - [user_id, user_id, user_id, user_id] - ); - - const results = await this.databaseService.read(query, [user_id, user_id, user_id, user_id]); - if (results.length === 0) return null; - - const user = results[0]; - if (user.beta_user) { - user.badges = ['early_user', ...user.badges]; - } - if (user.inventory) { - user.inventory = user.inventory - .filter((item: InventoryItem) => item !== null) - .map((item: InventoryItem) => ({ - ...item, - metadata: - typeof item.metadata === 'string' && item.metadata - ? (() => { - try { - return JSON.parse(item.metadata); - } catch { - return item.metadata; - } - })() - : item.metadata, - })); - } - if (user.ownedItems) { - user.ownedItems = user.ownedItems.sort((a: Item, b: Item) => { - const nameCompare = a.name?.localeCompare(b.name || '') || 0; - if (nameCompare !== 0) return nameCompare; - return 0; - }); - } - if (user.badges) { - const badgeOrder = ['early_user', 'staff', 'bug_hunter', 'contributor', 'moderator', 'community_manager', 'partner']; - user.badges = user.badges.filter(badge => badgeOrder.includes(badge)); - user.badges.sort((a, b) => badgeOrder.indexOf(a) - badgeOrder.indexOf(b)); + // Find user by any id field + const db = await this.databaseService.getDb(); + const userResult = await db.collection('users').findOne({ + $or: [ + { user_id }, + { discord_id: user_id }, + { google_id: user_id }, + { steam_id: user_id } + ] + }) as User | null; + + if (!userResult) return null; + + // Remove deleted inventory items + await db.collection('inventories').deleteMany({ + user_id: userResult.user_id, + item_id: { $nin: await db.collection('items').distinct('itemId', { $or: [{ deleted: null }, { deleted: 0 }] }) } + }); + + // Inventory + const inventory = await db.collection('inventories').aggregate([ + { $match: { user_id: userResult.user_id, amount: { $gt: 0 } } }, + { + $lookup: { + from: 'items', + foreignField: 'itemId', + as: 'item' + } + }, + { $unwind: '$item' }, + { $match: { $or: [{ 'item.deleted': null }, { 'item.deleted': 0 }] } }, + { + $project: { + user_id: 1, + item_id: 1, + itemId: '$item.itemId', + name: '$item.name', + description: '$item.description', + amount: 1, + iconHash: '$item.iconHash', + sellable: 1, + purchasePrice: '$item.purchasePrice', + rarity: '$item.rarity', + custom_url_link: 1, + metadata: 1 + } + } + ]).toArray(); + + // Owned items + const ownedItemsResult = await db.collection('items').find({ + owner: userResult.user_id, + $or: [{ deleted: null }, { deleted: 0 }], + showInStore: 1 + }).sort({ name: 1 }).toArray(); + + const ownedItems: Item[] = ownedItemsResult.map((item: any) => ({ + itemId: item.itemId, + name: item.name, + description: item.description, + price: item.price, + owner: item.owner, + showInStore: item.showInStore, + iconHash: item.iconHash, + deleted: item.deleted, + })); + + // Created games + const createdGamesResult = await db.collection('games').find({ + owner_id: userResult.user_id, + showInStore: 1 + }).sort({ name: 1 }).toArray(); + + const createdGames: Game[] = createdGamesResult.map((game: any) => ({ + game_id: game.game_id, + name: game.name, + description: game.description, + owner_id: game.owner_id, + showInStore: game.showInStore, + iconHash: game.iconHash, + splashHash: game.splashHash, + bannerHash: game.bannerHash, + genre: game.genre, + release_date: game.release_date, + developer: game.developer, + publisher: game.publisher, + rating: game.rating, + website: game.website, + trailer_link: game.trailer_link, + multiplayer: game.multiplayer, + gameId: game.game_id, + price: game.price, + })); + + // Parse metadata if needed + const parsedInventory = inventory.map((item: any) => ({ + ...item, + metadata: + typeof item.metadata === 'string' && item.metadata + ? (() => { + try { + return JSON.parse(item.metadata); + } catch { + return item.metadata; + } + })() + : item.metadata, + })); + + // Badges + let badges = userResult.badges || []; + if (userResult.beta_user) { + badges = ['early_user', ...badges]; } - return user; + const badgeOrder = ['early_user', 'staff', 'bug_hunter', 'contributor', 'moderator', 'community_manager', 'partner']; + badges = badges.filter((badge: string) => badgeOrder.includes(badge)); + badges.sort((a: string, b: string) => badgeOrder.indexOf(a) - badgeOrder.indexOf(b)); + + // Owned items sort + ownedItems.sort((a: Item, b: Item) => { + const nameCompare = a.name?.localeCompare(b.name || '') || 0; + if (nameCompare !== 0) return nameCompare; + return 0; + }); + + return { + ...userResult, + inventory: parsedInventory, + ownedItems, + createdGames, + badges + }; } async getUserWithPublicProfile(user_id: string): Promise<(PublicUser & UserExtensions) | null> { From dd314c76124db60de72243c010d293c29d14cba7 Mon Sep 17 00:00:00 2001 From: fox3000foxy <40730498+fox3000foxy@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:26:40 +0100 Subject: [PATCH 2/2] refactor: update aggregation pipeline in BadgeRepository and add localField in UserService --- src/repositories/BadgeRepository.ts | 7 +++++-- src/services/UserService.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/repositories/BadgeRepository.ts b/src/repositories/BadgeRepository.ts index ee7a5cd..c9d6e3e 100644 --- a/src/repositories/BadgeRepository.ts +++ b/src/repositories/BadgeRepository.ts @@ -37,10 +37,13 @@ export class BadgeRepository { const db = await this.databaseService.getDb(); const result = await db.collection('game_badges').aggregate([ { $match: { gameId: gameId, expires_at: { $gt: new Date() } } }, + // use pipeline style lookup to avoid driver errors and give us more control { $lookup: { from: 'badge_types', - localField: 'badgeId', - foreignField: 'id', + let: { badgeId: '$badgeId' }, + pipeline: [ + { $match: { $expr: { $eq: ['$id', '$$badgeId'] } } } + ], as: 'badge_info' } }, diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 7955ac5..2fbe788 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -268,6 +268,7 @@ export class UserService implements IUserService { { $lookup: { from: 'items', + localField: 'item_id', foreignField: 'itemId', as: 'item' }