diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5f198..765b7cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # GameVault Backend Server Changelog +## 8.0.0 + +Recommended Gamevault App Version: `v1.7.0` + +### Breaking Changes & Migration + +- [Issue #234](https://github.com/Phalcode/gamevault-app/issues/234): Added a new Health API endpoint for administrators to access detailed server information. + +- The health endpoints now provide data in JSON format, replacing the previous fancy HTML page. + +### Changes + +- [Issue #253](https://github.com/Phalcode/gamevault-app/issues/253): Implemented the Database Backup and Restoration API. +- Improved data management: The server no longer saves empty progress entries with a "UNPLAYED" state and 0 minutes of playtime. + - This change involved a database migration to remove such empty progress entries. + - The Progress API now permanently deletes entries marked as unplayed with 0 minutes of playtime. +- Enhanced the Progress Upsert API to handle nullable fields, enabling partial updates. +- Performed additional code refactoring to ensure consistency in code structure and naming. + +### Thanks + +- @Kudjo + ## 7.0.0 Recommended Gamevault App Version: `v1.6.1` @@ -54,7 +77,7 @@ Recommended Gamevault App Version: `v1.6.0` - Fixed the Broken Content-Disposition Header for some downloads. [#209](https://github.com/Phalcode/gamevault-app/issues/209). - Game Type only gets detected once, or when a game file changes and not on every index. [#200](https://github.com/Phalcode/gamevault-backend/issues/200) - Unified global error handler for 4XX and 5XX messages. The Problem is now directly inside the response without the duplicated status. -- Implemented `(NC)` flag to disable rawg-caching for single games. #194(https://github.com/Phalcode/gamevault-app/issues/194) +- Implemented `(NC)` flag to disable rawg-caching for single games. [#194](https://github.com/Phalcode/gamevault-app/issues/194) ### Thanks diff --git a/Dockerfile b/Dockerfile index 0bb2c27..7fa910d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ VOLUME /files /images /logs /db # Install pnpm and other needed tools RUN sed -i -e's/ main/ main non-free non-free-firmware contrib/g' /etc/apt/sources.list.d/debian.sources \ && apt update \ - && apt install -y sudo tzdata curl p7zip-full p7zip-rar \ + && apt install -y sudo tzdata curl p7zip-full p7zip-rar postgresql-client \ && npm i -g pnpm WORKDIR /app diff --git a/package.json b/package.json index b9434ef..2f8befd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gamevault-backend", - "version": "7.0.0", + "version": "8.0.0", "description": "the self-hosted gaming platform for drm-free games", "author": "Alkan Alper, Schäfer Philip GbR / Phalcode", "private": true, @@ -22,36 +22,36 @@ }, "dependencies": { "@nestjs/axios": "3.0.0", - "@nestjs/common": "10.2.6", - "@nestjs/core": "10.2.6", + "@nestjs/common": "10.2.7", + "@nestjs/core": "10.2.7", "@nestjs/passport": "10.0.2", - "@nestjs/platform-express": "10.2.6", + "@nestjs/platform-express": "10.2.7", "@nestjs/schedule": "3.0.4", - "@nestjs/swagger": "7.1.12", + "@nestjs/swagger": "7.1.13", "@nestjs/typeorm": "10.0.0", "async-g-i-s": "1.5.1", - "axios": "1.5.0", + "axios": "1.5.1", "bcrypt": "5.1.1", - "better-sqlite3": "8.6.0", + "better-sqlite3": "9.0.0", "class-transformer": "0.5.1", "class-validator": "0.14.0", "compression": "1.7.4", "cookie-parser": "1.4.6", "dotenv": "16.3.1", "express": "4.18.2", - "fastify": "4.23.2", + "fastify": "4.24.1", "file-type-checker": "^1.0.8", "helmet": "7.0.0", "mime": "3.0.0", "morgan": "1.10.0", "nest-winston": "1.9.4", - "nestjs-paginate": "8.3.0", + "nestjs-paginate": "8.3.3", "node-7z": "3.0.0", "passport": "0.6.0", "passport-http": "0.3.0", "pg": "8.11.3", "reflect-metadata": "0.1.13", - "rimraf": "5.0.1", + "rimraf": "5.0.5", "rxjs": "7.8.1", "sanitize-filename": "^1.6.3", "sharp": "0.32.6", @@ -60,7 +60,7 @@ "typeorm": "0.3.17", "typeorm-naming-strategies": "4.1.0", "unidecode": "^0.1.8", - "winston": "3.10.0", + "winston": "3.11.0", "winston-console-format": "1.0.8", "winston-daily-rotate-file": "4.7.1" }, @@ -70,24 +70,24 @@ "@types/bcrypt": "5.0.0", "@types/compression": "1.7.3", "@types/cookie-parser": "1.4.4", - "@types/express": "4.17.18", - "@types/mime": "3.0.1", - "@types/morgan": "1.9.5", - "@types/multer": "^1.4.7", - "@types/node": "20.6.5", + "@types/express": "4.17.19", + "@types/mime": "3.0.2", + "@types/morgan": "1.9.6", + "@types/multer": "^1.4.8", + "@types/node": "20.8.6", "@types/node-7z": "2.1.6", "@types/passport-http": "0.3.9", "@types/string-similarity": "4.0.0", "@types/throttle": "^1.0.2", "@types/unidecode": "^0.1.1", - "@typescript-eslint/eslint-plugin": "6.7.2", - "@typescript-eslint/parser": "6.7.2", - "eslint": "8.50.0", + "@typescript-eslint/eslint-plugin": "6.7.5", + "@typescript-eslint/parser": "6.7.5", + "eslint": "8.51.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-import": "2.28.1", - "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-prettier": "5.0.1", "prettier": "3.0.3", - "prettier-plugin-jsdoc": "1.0.2", + "prettier-plugin-jsdoc": "1.1.1", "simple-git-hooks": "2.9.0", "ts-node": "10.9.1", "typescript": "5.2.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 06d2c1a..56c32e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,40 +7,40 @@ settings: dependencies: '@nestjs/axios': specifier: 3.0.0 - version: 3.0.0(@nestjs/common@10.2.6)(axios@1.5.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + version: 3.0.0(@nestjs/common@10.2.7)(axios@1.5.1)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/common': - specifier: 10.2.6 - version: 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 10.2.7 + version: 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/core': - specifier: 10.2.6 - version: 10.2.6(@nestjs/common@10.2.6)(@nestjs/platform-express@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) + specifier: 10.2.7 + version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@nestjs/passport': specifier: 10.0.2 - version: 10.0.2(@nestjs/common@10.2.6)(passport@0.6.0) + version: 10.0.2(@nestjs/common@10.2.7)(passport@0.6.0) '@nestjs/platform-express': - specifier: 10.2.6 - version: 10.2.6(@nestjs/common@10.2.6)(@nestjs/core@10.2.6) + specifier: 10.2.7 + version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) '@nestjs/schedule': specifier: 3.0.4 - version: 3.0.4(@nestjs/common@10.2.6)(@nestjs/core@10.2.6)(reflect-metadata@0.1.13) + version: 3.0.4(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(reflect-metadata@0.1.13) '@nestjs/swagger': - specifier: 7.1.12 - version: 7.1.12(@nestjs/common@10.2.6)(@nestjs/core@10.2.6)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) + specifier: 7.1.13 + version: 7.1.13(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) '@nestjs/typeorm': specifier: 10.0.0 - version: 10.0.0(@nestjs/common@10.2.6)(@nestjs/core@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17) + version: 10.0.0(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17) async-g-i-s: specifier: 1.5.1 version: 1.5.1(node-fetch@2.7.0) axios: - specifier: 1.5.0 - version: 1.5.0 + specifier: 1.5.1 + version: 1.5.1 bcrypt: specifier: 5.1.1 version: 5.1.1 better-sqlite3: - specifier: 8.6.0 - version: 8.6.0 + specifier: 9.0.0 + version: 9.0.0 class-transformer: specifier: 0.5.1 version: 0.5.1 @@ -60,8 +60,8 @@ dependencies: specifier: 4.18.2 version: 4.18.2 fastify: - specifier: 4.23.2 - version: 4.23.2 + specifier: 4.24.1 + version: 4.24.1 file-type-checker: specifier: ^1.0.8 version: 1.0.8 @@ -76,10 +76,10 @@ dependencies: version: 1.10.0 nest-winston: specifier: 1.9.4 - version: 1.9.4(@nestjs/common@10.2.6)(winston@3.10.0) + version: 1.9.4(@nestjs/common@10.2.7)(winston@3.11.0) nestjs-paginate: - specifier: 8.3.0 - version: 8.3.0(@nestjs/common@10.2.6)(@nestjs/swagger@7.1.12)(express@4.18.2)(fastify@4.23.2)(typeorm@0.3.17) + specifier: 8.3.3 + version: 8.3.3(@nestjs/common@10.2.7)(@nestjs/swagger@7.1.13)(express@4.18.2)(fastify@4.24.1)(typeorm@0.3.17) node-7z: specifier: 3.0.0 version: 3.0.0 @@ -96,8 +96,8 @@ dependencies: specifier: 0.1.13 version: 0.1.13 rimraf: - specifier: 5.0.1 - version: 5.0.1 + specifier: 5.0.5 + version: 5.0.5 rxjs: specifier: 7.8.1 version: 7.8.1 @@ -115,7 +115,7 @@ dependencies: version: 1.0.3 typeorm: specifier: 0.3.17 - version: 0.3.17(better-sqlite3@8.6.0)(pg@8.11.3)(ts-node@10.9.1) + version: 0.3.17(better-sqlite3@9.0.0)(pg@8.11.3)(ts-node@10.9.1) typeorm-naming-strategies: specifier: 4.1.0 version: 4.1.0(typeorm@0.3.17) @@ -123,14 +123,14 @@ dependencies: specifier: ^0.1.8 version: 0.1.8 winston: - specifier: 3.10.0 - version: 3.10.0 + specifier: 3.11.0 + version: 3.11.0 winston-console-format: specifier: 1.0.8 version: 1.0.8 winston-daily-rotate-file: specifier: 4.7.1 - version: 4.7.1(winston@3.10.0) + version: 4.7.1(winston@3.11.0) devDependencies: '@nestjs/cli': @@ -149,20 +149,20 @@ devDependencies: specifier: 1.4.4 version: 1.4.4 '@types/express': - specifier: 4.17.18 - version: 4.17.18 + specifier: 4.17.19 + version: 4.17.19 '@types/mime': - specifier: 3.0.1 - version: 3.0.1 + specifier: 3.0.2 + version: 3.0.2 '@types/morgan': - specifier: 1.9.5 - version: 1.9.5 + specifier: 1.9.6 + version: 1.9.6 '@types/multer': - specifier: ^1.4.7 - version: 1.4.7 + specifier: ^1.4.8 + version: 1.4.8 '@types/node': - specifier: 20.6.5 - version: 20.6.5 + specifier: 20.8.6 + version: 20.8.6 '@types/node-7z': specifier: 2.1.6 version: 2.1.6 @@ -179,35 +179,35 @@ devDependencies: specifier: ^0.1.1 version: 0.1.1 '@typescript-eslint/eslint-plugin': - specifier: 6.7.2 - version: 6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.51.0)(typescript@5.2.2) '@typescript-eslint/parser': - specifier: 6.7.2 - version: 6.7.2(eslint@8.50.0)(typescript@5.2.2) + specifier: 6.7.5 + version: 6.7.5(eslint@8.51.0)(typescript@5.2.2) eslint: - specifier: 8.50.0 - version: 8.50.0 + specifier: 8.51.0 + version: 8.51.0 eslint-config-prettier: specifier: 9.0.0 - version: 9.0.0(eslint@8.50.0) + version: 9.0.0(eslint@8.51.0) eslint-plugin-import: specifier: 2.28.1 - version: 2.28.1(@typescript-eslint/parser@6.7.2)(eslint@8.50.0) + version: 2.28.1(@typescript-eslint/parser@6.7.5)(eslint@8.51.0) eslint-plugin-prettier: - specifier: 5.0.0 - version: 5.0.0(eslint-config-prettier@9.0.0)(eslint@8.50.0)(prettier@3.0.3) + specifier: 5.0.1 + version: 5.0.1(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@3.0.3) prettier: specifier: 3.0.3 version: 3.0.3 prettier-plugin-jsdoc: - specifier: 1.0.2 - version: 1.0.2(prettier@3.0.3) + specifier: 1.1.1 + version: 1.1.1(prettier@3.0.3) simple-git-hooks: specifier: 2.9.0 version: 2.9.0 ts-node: specifier: 10.9.1 - version: 10.9.1(@types/node@20.6.5)(typescript@5.2.2) + version: 10.9.1(@types/node@20.8.6)(typescript@5.2.2) typescript: specifier: 5.2.2 version: 5.2.2 @@ -328,6 +328,11 @@ packages: engines: {node: '>=0.1.90'} requiresBuild: true + /@colors/colors@1.6.0: + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + dev: false + /@cspotcode/source-map-support@0.8.1: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -342,13 +347,13 @@ packages: kuler: 2.0.0 dev: false - /@eslint-community/eslint-utils@4.4.0(eslint@8.50.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.51.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.50.0 + eslint: 8.51.0 eslint-visitor-keys: 3.4.3 dev: true @@ -374,8 +379,8 @@ packages: - supports-color dev: true - /@eslint/js@8.50.0: - resolution: {integrity: sha512-NCC3zz2+nvYd+Ckfh87rA47zfu2QsQpvc6k1yzTk+b9KzRj0wkGa8LSoGOXN6Zv4lRf/EIoZ80biDh9HOI+RNQ==} + /@eslint/js@8.51.0: + resolution: {integrity: sha512-HxjQ8Qn+4SI3/AFv6sOrDB+g6PpUTDwSJiQqOrnneEk8L71161srI9gjzzZvYVbzHiVg/BvcH95+cK/zfIt4pg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -391,14 +396,14 @@ packages: resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} dev: false - /@fastify/error@3.2.0: - resolution: {integrity: sha512-KAfcLa+CnknwVi5fWogrLXgidLic+GXnLjijXdpl8pvkvbXU5BGa37iZO9FGvsh9ZL4y+oFi5cbHBm5UOG+dmQ==} + /@fastify/error@3.4.0: + resolution: {integrity: sha512-e/mafFwbK3MNqxUcFBLgHhgxsF8UT1m8aj0dAlqEa2nJEgPsRtpHTZ3ObgrgkZ2M1eJHPTwgyUl/tXkvabsZdQ==} dev: false /@fastify/fast-json-stringify-compiler@4.3.0: resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==} dependencies: - fast-json-stringify: 5.7.0 + fast-json-stringify: 5.8.0 dev: false /@humanwhocodes/config-array@0.11.11: @@ -421,6 +426,18 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: false + /@jridgewell/gen-mapping@0.3.2: resolution: {integrity: sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==} engines: {node: '>=6.0.0'} @@ -489,7 +506,7 @@ packages: - supports-color dev: false - /@nestjs/axios@3.0.0(@nestjs/common@10.2.6)(axios@1.5.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): + /@nestjs/axios@3.0.0(@nestjs/common@10.2.7)(axios@1.5.1)(reflect-metadata@0.1.13)(rxjs@7.8.1): resolution: {integrity: sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==} peerDependencies: '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -497,8 +514,8 @@ packages: reflect-metadata: ^0.1.12 rxjs: ^6.0.0 || ^7.0.0 dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - axios: 1.5.0 + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + axios: 1.5.1 reflect-metadata: 0.1.13 rxjs: 7.8.1 dev: false @@ -544,8 +561,8 @@ packages: - webpack-cli dev: true - /@nestjs/common@10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-ma8R7n+FXsWM4XF9QXjjrsRceyRzid/xKmNKVOa/sTJntkVG8lL71BHBEfjtFvO6EJUqjs/15LbDc0iaN5nCwA==} + /@nestjs/common@10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-cUtCRXiUstDmh4bSBhVbq4cI439Gngp4LgLGLBmd5dqFQodfXKnSD441ldYfFiLz4rbUsnoMJz/8ZjuIEI+B7A==} peerDependencies: class-transformer: '*' class-validator: '*' @@ -566,8 +583,8 @@ packages: uid: 2.0.2 dev: false - /@nestjs/core@10.2.6(@nestjs/common@10.2.6)(@nestjs/platform-express@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1): - resolution: {integrity: sha512-oGQ2CoBeFRT7egG47MFqS89xlXBTIRZBkRpKRTPMftEfL1RMXhXIcIIaGfzp11wx6qxrBVxBXpVLM09oaqHpaQ==} + /@nestjs/core@10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1): + resolution: {integrity: sha512-5GSu53QUUcwX17sNmlJPa1I0wIeAZOKbedyVuQx0ZAwWVa9g0wJBbsNP+R4EJ+j5Dkdzt/8xkiZvnKt8RFRR8g==} requiresBuild: true peerDependencies: '@nestjs/common': ^10.0.0 @@ -584,8 +601,8 @@ packages: '@nestjs/websockets': optional: true dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/platform-express': 10.2.6(@nestjs/common@10.2.6)(@nestjs/core@10.2.6) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/platform-express': 10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -598,7 +615,7 @@ packages: - encoding dev: false - /@nestjs/mapped-types@2.0.2(@nestjs/common@10.2.6)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): + /@nestjs/mapped-types@2.0.2(@nestjs/common@10.2.7)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): resolution: {integrity: sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -611,30 +628,30 @@ packages: class-validator: optional: true dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) class-transformer: 0.5.1 class-validator: 0.14.0 reflect-metadata: 0.1.13 dev: false - /@nestjs/passport@10.0.2(@nestjs/common@10.2.6)(passport@0.6.0): + /@nestjs/passport@10.0.2(@nestjs/common@10.2.7)(passport@0.6.0): resolution: {integrity: sha512-od31vfB2z3y05IDB5dWSbCGE2+pAf2k2WCBinNuTTOxN0O0+wtO1L3kawj/aCW3YR9uxsTOVbTDwtwgpNNsnjQ==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 passport: ^0.4.0 || ^0.5.0 || ^0.6.0 dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) passport: 0.6.0 dev: false - /@nestjs/platform-express@10.2.6(@nestjs/common@10.2.6)(@nestjs/core@10.2.6): - resolution: {integrity: sha512-4U16D5ot2570CR8Qm5qu/SBXsA2l5KxN7AVSGvzoWoBxjEoOnnZOapC5Pler3yYa0tT1xLhji61RX1gceKW3dw==} + /@nestjs/platform-express@10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7): + resolution: {integrity: sha512-p+kp6aJtkgAdVpUrCVmM6MKtOvjsbt7QofBiZMidjYesZkMeG5gZ1D2SK8XzvQ8VXHJfFgEdY2xcKGB+wJLOYQ==} peerDependencies: '@nestjs/common': ^10.0.0 '@nestjs/core': ^10.0.0 dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.6(@nestjs/common@10.2.6)(@nestjs/platform-express@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 express: 4.18.2 @@ -644,15 +661,15 @@ packages: - supports-color dev: false - /@nestjs/schedule@3.0.4(@nestjs/common@10.2.6)(@nestjs/core@10.2.6)(reflect-metadata@0.1.13): + /@nestjs/schedule@3.0.4(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(reflect-metadata@0.1.13): resolution: {integrity: sha512-uFJpuZsXfpvgx2y7/KrIZW9e1L68TLiwRodZ6+Gc8xqQiHSUzAVn+9F4YMxWFlHITZvvkjWziUFgRNCitDcTZQ==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 reflect-metadata: ^0.1.12 dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.6(@nestjs/common@10.2.6)(@nestjs/platform-express@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) cron: 2.4.3 reflect-metadata: 0.1.13 uuid: 9.0.1 @@ -673,8 +690,8 @@ packages: - chokidar dev: true - /@nestjs/swagger@7.1.12(@nestjs/common@10.2.6)(@nestjs/core@10.2.6)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): - resolution: {integrity: sha512-Q1P/IE+cws0sJeNtbs+8uDalcVylpmAnaEUFenGOa3KSNnXF/8DOE84mET/uUhFXsiz9PLHK8Hy7o7B6fRpMhg==} + /@nestjs/swagger@7.1.13(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-aHfW0rDZZKTuPVSkxutBCB16lBy5vrsHVoRF5RvPtH7U2cm4Vf+OnfhxKKuG2g2Xocn9sDL+JAyVlY2VN3ytTw==} peerDependencies: '@fastify/static': ^6.0.0 '@nestjs/common': ^9.0.0 || ^10.0.0 @@ -690,19 +707,19 @@ packages: class-validator: optional: true dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.6(@nestjs/common@10.2.6)(@nestjs/platform-express@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/mapped-types': 2.0.2(@nestjs/common@10.2.6)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.2(@nestjs/common@10.2.7)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) class-transformer: 0.5.1 class-validator: 0.14.0 js-yaml: 4.1.0 lodash: 4.17.21 path-to-regexp: 3.2.0 reflect-metadata: 0.1.13 - swagger-ui-dist: 5.7.2 + swagger-ui-dist: 5.9.0 dev: false - /@nestjs/typeorm@10.0.0(@nestjs/common@10.2.6)(@nestjs/core@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17): + /@nestjs/typeorm@10.0.0(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17): resolution: {integrity: sha512-WQU4HCDTz4UavsFzvGUKDHqi0MO5K47yFoPXdmh+Z/hCNO7SHCMmV9jLiLukM8n5nKUqJ3jDqiljkWBcZPdCtA==} peerDependencies: '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -711,11 +728,11 @@ packages: rxjs: ^7.2.0 typeorm: ^0.3.0 dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/core': 10.2.6(@nestjs/common@10.2.6)(@nestjs/platform-express@10.2.6)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.8.1) reflect-metadata: 0.1.13 rxjs: 7.8.1 - typeorm: 0.3.17(better-sqlite3@8.6.0)(pg@8.11.3)(ts-node@10.9.1) + typeorm: 0.3.17(better-sqlite3@9.0.0)(pg@8.11.3)(ts-node@10.9.1) uuid: 9.0.0 dev: false @@ -790,32 +807,32 @@ packages: /@types/bcrypt@5.0.0: resolution: {integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==} dependencies: - '@types/node': 20.6.5 + '@types/node': 20.8.6 dev: true /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} dependencies: '@types/connect': 3.4.35 - '@types/node': 20.6.5 + '@types/node': 20.8.6 dev: true /@types/compression@1.7.3: resolution: {integrity: sha512-rKquEGjebqizyHNMOpaE/4FdYR5VQiWFeesqYfvJU0seSEyB4625UGhNOO/qIkH10S3wftiV7oefc8WdLZ/gCQ==} dependencies: - '@types/express': 4.17.18 + '@types/express': 4.17.19 dev: true /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.6.5 + '@types/node': 20.8.6 dev: true /@types/cookie-parser@1.4.4: resolution: {integrity: sha512-Var+aj5I6ZgIqsQ05N2V8q5OBrFfZXtIGWWDSrEYLIbMw758obagSwdGcLCjwh1Ga7M7+wj0SDIAaAC/WT7aaA==} dependencies: - '@types/express': 4.17.18 + '@types/express': 4.17.19 dev: true /@types/debug@4.1.7: @@ -845,13 +862,13 @@ packages: /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: - '@types/node': 20.6.5 + '@types/node': 20.8.6 '@types/qs': 6.9.7 '@types/range-parser': 1.2.4 dev: true - /@types/express@4.17.18: - resolution: {integrity: sha512-Sxv8BSLLgsBYmcnGdGjjEjqET2U+AKAdCRODmMiq02FgjwuV75Ut85DRpvFjyw/Mk0vgUOliGRU0UUmuuZHByQ==} + /@types/express@4.17.19: + resolution: {integrity: sha512-UtOfBtzN9OvpZPPbnnYunfjM7XCI4jyk1NvnFhTVz5krYAnW4o5DCoIekvms+8ApqhB4+9wSge1kBijdfTSmfg==} dependencies: '@types/body-parser': 1.19.2 '@types/express-serve-static-core': 4.17.33 @@ -871,40 +888,42 @@ packages: resolution: {integrity: sha512-l5cpE57br4BIjK+9BSkFBOsWtwv6J9bJpC7gdXIzZyI0vuKvNTk0wZZrkQxMGsUAuGW9+WMNWF2IJMD7br2yeQ==} dev: false - /@types/mdast@3.0.10: - resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==} + /@types/mdast@4.0.1: + resolution: {integrity: sha512-IlKct1rUTJ1T81d8OHzyop15kGv9A/ff7Gz7IJgrk6jDb4Udw77pCJ+vq8oxZf4Ghpm+616+i1s/LNg/Vh7d+g==} dependencies: - '@types/unist': 2.0.6 + '@types/unist': 3.0.0 dev: true - /@types/mime@3.0.1: - resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + /@types/mime@3.0.2: + resolution: {integrity: sha512-Wj+fqpTLtTbG7c0tH47dkahefpLKEbB+xAZuLq7b4/IDHPl/n6VoXcyUQ2bypFlbSwvCr0y+bD4euTTqTJsPxQ==} dev: true - /@types/morgan@1.9.5: - resolution: {integrity: sha512-5TgfIWm0lcTGnbCZExwc19dCOMOMmAiiBZQj8Ko3NRxsVDgRxf+AEGRQTqNVA5Yh2xfdWp4clbAEMbYP+jkOqg==} + /@types/morgan@1.9.6: + resolution: {integrity: sha512-xfKogz5WcKww2DAiVT9zxMgrqQt+Shq8tDVeLT+otoj6dJnkRkyJxMF51mHtUc3JCPKGk5x1EBU0buuGpfftlQ==} dependencies: - '@types/node': 20.6.5 + '@types/node': 20.8.6 dev: true /@types/ms@0.7.31: resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==} dev: true - /@types/multer@1.4.7: - resolution: {integrity: sha512-/SNsDidUFCvqqcWDwxv2feww/yqhNeTRL5CVoL3jU4Goc4kKEL10T7Eye65ZqPNi4HRx8sAEX59pV1aEH7drNA==} + /@types/multer@1.4.8: + resolution: {integrity: sha512-VMZOW6mnmMMhA5m3fsCdXBwFwC+a+27/8gctNMuQC4f7UtWcF79KAFGoIfKZ4iqrElgWIa3j5vhMJDp0iikQ1g==} dependencies: - '@types/express': 4.17.18 + '@types/express': 4.17.19 dev: true /@types/node-7z@2.1.6: resolution: {integrity: sha512-26aONb5grye/fklu1FXy5NEnbT2QyLA7DoCjQVTT3zAUJRjp/D44oiYrBVj/CLGmAeCJMkVbdxL6vROxaf7JYQ==} dependencies: - '@types/node': 20.6.5 + '@types/node': 20.8.6 dev: true - /@types/node@20.6.5: - resolution: {integrity: sha512-2qGq5LAOTh9izcc0+F+dToFigBWiK1phKPt7rNhOqJSr35y8rlIBjDwGtFSgAI6MGIhjwOVNSQZVdJsZJ2uR1w==} + /@types/node@20.8.6: + resolution: {integrity: sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==} + dependencies: + undici-types: 5.25.3 /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} @@ -913,14 +932,14 @@ packages: /@types/passport-http@0.3.9: resolution: {integrity: sha512-uQ4vyRdvM0jdWuKpLmi6Q6ri9Nwt8YnHmF7kE6snbthxPrsMWcjRCVc5WcPaQ356ODSZTDgiRYURMPIspCkn3Q==} dependencies: - '@types/express': 4.17.18 + '@types/express': 4.17.19 '@types/passport': 1.0.12 dev: true /@types/passport@1.0.12: resolution: {integrity: sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw==} dependencies: - '@types/express': 4.17.18 + '@types/express': 4.17.19 dev: true /@types/qs@6.9.7: @@ -938,8 +957,8 @@ packages: /@types/serve-static@1.15.0: resolution: {integrity: sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==} dependencies: - '@types/mime': 3.0.1 - '@types/node': 20.6.5 + '@types/mime': 3.0.2 + '@types/node': 20.8.6 dev: true /@types/string-similarity@4.0.0: @@ -949,23 +968,23 @@ packages: /@types/throttle@1.0.2: resolution: {integrity: sha512-ieT8dv6eJRCcyRXrw6o25/mBkJKT+A92C5JFVqFY99ea6uRWKGta4Q9Xl1IPHEejtHjBeYZK/ffcVcnlIKk8hg==} dependencies: - '@types/node': 20.6.5 + '@types/node': 20.8.6 dev: true /@types/unidecode@0.1.1: resolution: {integrity: sha512-ReYnWajSY+QnZk5dBpSztek+kHUtpHJkf0pSZBx2ZVIxcSBkRLY7m0caPwVKXC6WjE65G2fVU84EpiFbdJVDuQ==} dev: true - /@types/unist@2.0.6: - resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} + /@types/unist@3.0.0: + resolution: {integrity: sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==} dev: true /@types/validator@13.7.10: resolution: {integrity: sha512-t1yxFAR2n0+VO6hd/FJ9F2uezAZVWHLmpmlJzm1eX03+H7+HsuTAp7L8QJs+2pQCfWkP1+EXsGK9Z9v7o/qPVQ==} dev: false - /@typescript-eslint/eslint-plugin@6.7.2(@typescript-eslint/parser@6.7.2)(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-ooaHxlmSgZTM6CHYAFRlifqh1OAr3PAQEwi7lhYhaegbnXrnh7CDcHmc3+ihhbQC7H0i4JF0psI5ehzkF6Yl6Q==} + /@typescript-eslint/eslint-plugin@6.7.5(@typescript-eslint/parser@6.7.5)(eslint@8.51.0)(typescript@5.2.2): + resolution: {integrity: sha512-JhtAwTRhOUcP96D0Y6KYnwig/MRQbOoLGXTON2+LlyB/N35SP9j1boai2zzwXb7ypKELXMx3DVk9UTaEq1vHEw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -976,13 +995,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.6.2 - '@typescript-eslint/parser': 6.7.2(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.7.2 - '@typescript-eslint/type-utils': 6.7.2(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.2(eslint@8.50.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.7.2 + '@typescript-eslint/parser': 6.7.5(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/scope-manager': 6.7.5 + '@typescript-eslint/type-utils': 6.7.5(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.5(eslint@8.51.0)(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.7.5 debug: 4.3.4 - eslint: 8.50.0 + eslint: 8.51.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -993,8 +1012,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.7.2(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==} + /@typescript-eslint/parser@6.7.5(eslint@8.51.0)(typescript@5.2.2): + resolution: {integrity: sha512-bIZVSGx2UME/lmhLcjdVc7ePBwn7CLqKarUBL4me1C5feOd663liTGjMBGVcGr+BhnSLeP4SgwdvNnnkbIdkCw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1003,27 +1022,27 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.7.2 - '@typescript-eslint/types': 6.7.2 - '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.7.2 + '@typescript-eslint/scope-manager': 6.7.5 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) + '@typescript-eslint/visitor-keys': 6.7.5 debug: 4.3.4 - eslint: 8.50.0 + eslint: 8.51.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/scope-manager@6.7.2: - resolution: {integrity: sha512-bgi6plgyZjEqapr7u2mhxGR6E8WCzKNUFWNh6fkpVe9+yzRZeYtDTbsIBzKbcxI+r1qVWt6VIoMSNZ4r2A+6Yw==} + /@typescript-eslint/scope-manager@6.7.5: + resolution: {integrity: sha512-GAlk3eQIwWOJeb9F7MKQ6Jbah/vx1zETSDw8likab/eFcqkjSD7BI75SDAeC5N2L0MmConMoPvTsmkrg71+B1A==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.7.2 - '@typescript-eslint/visitor-keys': 6.7.2 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/visitor-keys': 6.7.5 dev: true - /@typescript-eslint/type-utils@6.7.2(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-36F4fOYIROYRl0qj95dYKx6kybddLtsbmPIYNK0OBeXv2j9L5nZ17j9jmfy+bIDHKQgn2EZX+cofsqi8NPATBQ==} + /@typescript-eslint/type-utils@6.7.5(eslint@8.51.0)(typescript@5.2.2): + resolution: {integrity: sha512-Gs0qos5wqxnQrvpYv+pf3XfcRXW6jiAn9zE/K+DlmYf6FcpxeNYN0AIETaPR7rHO4K2UY+D0CIbDP9Ut0U4m1g==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -1032,23 +1051,23 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.2.2) - '@typescript-eslint/utils': 6.7.2(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) + '@typescript-eslint/utils': 6.7.5(eslint@8.51.0)(typescript@5.2.2) debug: 4.3.4 - eslint: 8.50.0 + eslint: 8.51.0 ts-api-utils: 1.0.1(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: - supports-color dev: true - /@typescript-eslint/types@6.7.2: - resolution: {integrity: sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==} + /@typescript-eslint/types@6.7.5: + resolution: {integrity: sha512-WboQBlOXtdj1tDFPyIthpKrUb+kZf2VroLZhxKa/VlwLlLyqv/PwUNgL30BlTVZV1Wu4Asu2mMYPqarSO4L5ZQ==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.7.2(typescript@5.2.2): - resolution: {integrity: sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==} + /@typescript-eslint/typescript-estree@6.7.5(typescript@5.2.2): + resolution: {integrity: sha512-NhJiJ4KdtwBIxrKl0BqG1Ur+uw7FiOnOThcYx9DpOGJ/Abc9z2xNzLeirCG02Ig3vkvrc2qFLmYSSsaITbKjlg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -1056,8 +1075,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.7.2 - '@typescript-eslint/visitor-keys': 6.7.2 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/visitor-keys': 6.7.5 debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 @@ -1068,30 +1087,30 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.7.2(eslint@8.50.0)(typescript@5.2.2): - resolution: {integrity: sha512-ZCcBJug/TS6fXRTsoTkgnsvyWSiXwMNiPzBUani7hDidBdj1779qwM1FIAmpH4lvlOZNF3EScsxxuGifjpLSWQ==} + /@typescript-eslint/utils@6.7.5(eslint@8.51.0)(typescript@5.2.2): + resolution: {integrity: sha512-pfRRrH20thJbzPPlPc4j0UNGvH1PjPlhlCMq4Yx7EGjV7lvEeGX0U6MJYe8+SyFutWgSHsdbJ3BXzZccYggezA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) '@types/json-schema': 7.0.12 '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 6.7.2 - '@typescript-eslint/types': 6.7.2 - '@typescript-eslint/typescript-estree': 6.7.2(typescript@5.2.2) - eslint: 8.50.0 + '@typescript-eslint/scope-manager': 6.7.5 + '@typescript-eslint/types': 6.7.5 + '@typescript-eslint/typescript-estree': 6.7.5(typescript@5.2.2) + eslint: 8.51.0 semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript dev: true - /@typescript-eslint/visitor-keys@6.7.2: - resolution: {integrity: sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==} + /@typescript-eslint/visitor-keys@6.7.5: + resolution: {integrity: sha512-3MaWdDZtLlsexZzDSdQWsFQ9l9nL8B80Z4fImSpyllFC/KLqWQRdEcB+gGGO+N3Q2uL40EsG66wZLsohPxNXvg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.7.2 + '@typescript-eslint/types': 6.7.5 eslint-visitor-keys: 3.4.3 dev: true @@ -1317,6 +1336,11 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: false + /ansi-styles@3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -1330,6 +1354,11 @@ packages: dependencies: color-convert: 2.0.1 + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: false + /any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} dev: false @@ -1483,8 +1512,8 @@ packages: - supports-color dev: false - /axios@1.5.0: - resolution: {integrity: sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==} + /axios@1.5.1: + resolution: {integrity: sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==} dependencies: follow-redirects: 1.15.2 form-data: 4.0.0 @@ -1522,8 +1551,8 @@ packages: - supports-color dev: false - /better-sqlite3@8.6.0: - resolution: {integrity: sha512-jwAudeiTMTSyby+/SfbHDebShbmC2MCH8mU2+DXi0WJfv13ypEJm47cd3kljmy/H130CazEvkf2Li//ewcMJ1g==} + /better-sqlite3@9.0.0: + resolution: {integrity: sha512-lDxQ9qg/XuUHZG6xzrQaMHkNWl37t35/LPB/VJGV8DdScSuGFNfFSqgscXEd8UIuyk/d9wU8iaMxQa4If5Wqog==} requiresBuild: true dependencies: bindings: 1.5.0 @@ -2157,15 +2186,16 @@ packages: engines: {node: '>=8'} dev: false + /devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dependencies: + dequal: 2.0.3 + dev: true + /diff@4.0.2: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} - /diff@5.1.0: - resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} - engines: {node: '>=0.3.1'} - dev: true - /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -2192,6 +2222,10 @@ packages: engines: {node: '>=12'} dev: false + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: false + /ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} dev: false @@ -2203,6 +2237,10 @@ packages: /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: false + /enabled@2.0.0: resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} dev: false @@ -2322,13 +2360,13 @@ packages: engines: {node: '>=10'} dev: true - /eslint-config-prettier@9.0.0(eslint@8.50.0): + /eslint-config-prettier@9.0.0(eslint@8.51.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.50.0 + eslint: 8.51.0 dev: true /eslint-import-resolver-node@0.3.7: @@ -2341,7 +2379,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.2)(eslint-import-resolver-node@0.3.7)(eslint@8.50.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.7)(eslint@8.51.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -2362,15 +2400,15 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.7.2(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.5(eslint@8.51.0)(typescript@5.2.2) debug: 3.2.7 - eslint: 8.50.0 + eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.2)(eslint@8.50.0): + /eslint-plugin-import@2.28.1(@typescript-eslint/parser@6.7.5)(eslint@8.51.0): resolution: {integrity: sha512-9I9hFlITvOV55alzoKBI+K9q74kv0iKMeY6av5+umsNwayt59fz692daGyjR+oStBQgx6nwR9rXldDev3Clw+A==} engines: {node: '>=4'} peerDependencies: @@ -2380,16 +2418,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.7.2(eslint@8.50.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.7.5(eslint@8.51.0)(typescript@5.2.2) array-includes: 3.1.6 array.prototype.findlastindex: 1.2.2 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.50.0 + eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.2)(eslint-import-resolver-node@0.3.7)(eslint@8.50.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.7.5)(eslint-import-resolver-node@0.3.7)(eslint@8.51.0) has: 1.0.3 is-core-module: 2.13.0 is-glob: 4.0.3 @@ -2405,8 +2443,8 @@ packages: - supports-color dev: true - /eslint-plugin-prettier@5.0.0(eslint-config-prettier@9.0.0)(eslint@8.50.0)(prettier@3.0.3): - resolution: {integrity: sha512-AgaZCVuYDXHUGxj/ZGu1u8H8CYgDY3iG6w5kUFw4AzMVXzB7VvbKgYR4nATIN+OvUrghMbiDLeimVjVY5ilq3w==} + /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.0.0)(eslint@8.51.0)(prettier@3.0.3): + resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -2419,8 +2457,8 @@ packages: eslint-config-prettier: optional: true dependencies: - eslint: 8.50.0 - eslint-config-prettier: 9.0.0(eslint@8.50.0) + eslint: 8.51.0 + eslint-config-prettier: 9.0.0(eslint@8.51.0) prettier: 3.0.3 prettier-linter-helpers: 1.0.0 synckit: 0.8.5 @@ -2447,15 +2485,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.50.0: - resolution: {integrity: sha512-FOnOGSuFuFLv/Sa+FDVRZl4GGVAAFFi8LecRsI5a1tMO5HIE8nCm4ivAlzt4dT3ol/PaaGC0rJEEXQmHJBGoOg==} + /eslint@8.51.0: + resolution: {integrity: sha512-2WuxRZBrlwnXi+/vFSJyjMqrNjtJqiasMzehF0shoLaW7DzS3/9Yvrmq5JiT66+pNjiX4UBnLDiKHcWAr/OInA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.50.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.51.0) '@eslint-community/regexpp': 4.6.2 '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.50.0 + '@eslint/js': 8.51.0 '@humanwhocodes/config-array': 0.11.11 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -2649,8 +2687,8 @@ packages: tmp: 0.0.33 dev: true - /fast-content-type-parse@1.0.0: - resolution: {integrity: sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==} + /fast-content-type-parse@1.1.0: + resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==} dev: false /fast-decode-uri-component@1.0.1: @@ -2683,8 +2721,8 @@ packages: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true - /fast-json-stringify@5.7.0: - resolution: {integrity: sha512-sBVPTgnAZseLu1Qgj6lUbQ0HfjFhZWXAmpZ5AaSGkyLh5gAXBga/uPJjQPHpDFjC9adWIpdOcCLSDTgrZ7snoQ==} + /fast-json-stringify@5.8.0: + resolution: {integrity: sha512-VVwK8CFMSALIvt14U8AvrSzQAwN/0vaVRiFFUVlpnXSnDGrSkOAO5MtzyN8oQNjLd5AqTW5OZRgyjoNuAuR3jQ==} dependencies: '@fastify/deepmerge': 1.3.0 ajv: 8.12.0 @@ -2717,25 +2755,25 @@ packages: resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} dev: false - /fastify@4.23.2: - resolution: {integrity: sha512-WFSxsHES115svC7NrerNqZwwM0UOxbC/P6toT9LRHgAAFvG7o2AN5W+H4ihCtOGuYXjZf4z+2jXC89rVEoPWOA==} + /fastify@4.24.1: + resolution: {integrity: sha512-uMthXpH0DjWWxb3/ERmBsVkFm5cllozjbOFkhsD4Xlw+OUg5wGASRBjVRcp2XeYZa91W2BbwgIWJrnGGPWY2dQ==} dependencies: '@fastify/ajv-compiler': 3.5.0 - '@fastify/error': 3.2.0 + '@fastify/error': 3.4.0 '@fastify/fast-json-stringify-compiler': 4.3.0 abstract-logging: 2.0.1 avvio: 8.2.1 - fast-content-type-parse: 1.0.0 - fast-json-stringify: 5.7.0 - find-my-way: 7.6.0 - light-my-request: 5.9.1 - pino: 8.14.1 + fast-content-type-parse: 1.1.0 + fast-json-stringify: 5.8.0 + find-my-way: 7.7.0 + light-my-request: 5.11.0 + pino: 8.16.0 process-warning: 2.2.0 proxy-addr: 2.0.7 rfdc: 1.3.0 secure-json-parse: 2.7.0 semver: 7.5.4 - toad-cache: 3.2.0 + toad-cache: 3.3.0 transitivePeerDependencies: - supports-color dev: false @@ -2799,8 +2837,8 @@ packages: - supports-color dev: false - /find-my-way@7.6.0: - resolution: {integrity: sha512-H7berWdHJ+5CNVr4ilLWPai4ml7Y2qAsxjw3pfeBxPigZmaDTzF0wjJLj90xRCmGcWYcyt050yN+34OZDJm1eQ==} + /find-my-way@7.7.0: + resolution: {integrity: sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==} engines: {node: '>=14'} dependencies: fast-deep-equal: 3.1.3 @@ -3020,16 +3058,16 @@ packages: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} dev: true - /glob@10.2.6: - resolution: {integrity: sha512-U/rnDpXJGF414QQQZv5uVsabTVxMSwzS5CH0p3DRCIV6ownl4f7PzGnkGmvlum2wB+9RlJWJZ6ACU1INnBqiPA==} + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true dependencies: foreground-child: 3.1.1 - jackspeak: 2.1.0 + jackspeak: 2.3.6 minimatch: 9.0.1 minipass: 5.0.0 - path-scurry: 1.7.0 + path-scurry: 1.10.1 dev: false /glob@7.2.3: @@ -3498,11 +3536,11 @@ packages: engines: {node: '>=6'} dev: false - /jackspeak@2.1.0: - resolution: {integrity: sha512-DiEwVPqsieUzZBNxQ2cxznmFzfg/AMgJUjYw5xl6rSmCxAQXECcbSdwcLM6Ds6T09+SBfSNCGPhYUoQ96P4h7A==} + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} engines: {node: '>=14'} dependencies: - cliui: 7.0.4 + '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 dev: false @@ -3511,7 +3549,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.6.5 + '@types/node': 20.8.6 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true @@ -3566,11 +3604,6 @@ packages: graceful-fs: 4.2.10 dev: true - /kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - dev: true - /kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} dev: false @@ -3587,8 +3620,8 @@ packages: resolution: {integrity: sha512-sLeVLmWX17VCKKulc+aDIRHS95TxoTsKMRJi5s5gJdwlqNzMWcBCtSHHruVyXjqfi67daXM2SnLf2juSrdx5Sg==} dev: false - /light-my-request@5.9.1: - resolution: {integrity: sha512-UT7pUk8jNCR1wR7w3iWfIjx32DiB2f3hFdQSOwy3/EPQ3n3VocyipUxcyRZR0ahoev+fky69uA+GejPa9KuHKg==} + /light-my-request@5.11.0: + resolution: {integrity: sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==} dependencies: cookie: 0.5.0 process-warning: 2.2.0 @@ -3700,29 +3733,29 @@ packages: /make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - /mdast-util-from-markdown@1.3.0: - resolution: {integrity: sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g==} + /mdast-util-from-markdown@2.0.0: + resolution: {integrity: sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==} dependencies: - '@types/mdast': 3.0.10 - '@types/unist': 2.0.6 + '@types/mdast': 4.0.1 + '@types/unist': 3.0.0 decode-named-character-reference: 1.0.2 - mdast-util-to-string: 3.1.1 - micromark: 3.1.0 - micromark-util-decode-numeric-character-reference: 1.0.0 - micromark-util-decode-string: 1.0.2 - micromark-util-normalize-identifier: 1.0.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - unist-util-stringify-position: 3.0.3 - uvu: 0.5.6 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.0 + micromark-util-decode-numeric-character-reference: 2.0.0 + micromark-util-decode-string: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 + unist-util-stringify-position: 4.0.0 transitivePeerDependencies: - supports-color dev: true - /mdast-util-to-string@3.1.1: - resolution: {integrity: sha512-tGvhT94e+cVnQt8JWE9/b3cUQZWS732TJxXHktvP+BYo62PpYD53Ls/6cC60rW21dW+txxiM4zMdc6abASvZKA==} + /mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} dependencies: - '@types/mdast': 3.0.10 + '@types/mdast': 4.0.1 dev: true /media-typer@0.3.0: @@ -3755,178 +3788,177 @@ packages: engines: {node: '>= 0.6'} dev: false - /micromark-core-commonmark@1.0.6: - resolution: {integrity: sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==} + /micromark-core-commonmark@2.0.0: + resolution: {integrity: sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==} dependencies: decode-named-character-reference: 1.0.2 - micromark-factory-destination: 1.0.0 - micromark-factory-label: 1.0.2 - micromark-factory-space: 1.0.0 - micromark-factory-title: 1.0.2 - micromark-factory-whitespace: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-chunked: 1.0.0 - micromark-util-classify-character: 1.0.0 - micromark-util-html-tag-name: 1.1.0 - micromark-util-normalize-identifier: 1.0.0 - micromark-util-resolve-all: 1.0.0 - micromark-util-subtokenize: 1.0.2 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 + devlop: 1.1.0 + micromark-factory-destination: 2.0.0 + micromark-factory-label: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-factory-title: 2.0.0 + micromark-factory-whitespace: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-chunked: 2.0.0 + micromark-util-classify-character: 2.0.0 + micromark-util-html-tag-name: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-subtokenize: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-factory-destination@1.0.0: - resolution: {integrity: sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==} + /micromark-factory-destination@2.0.0: + resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} dependencies: - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-factory-label@1.0.2: - resolution: {integrity: sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==} + /micromark-factory-label@2.0.0: + resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} dependencies: - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 + devlop: 1.1.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-factory-space@1.0.0: - resolution: {integrity: sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==} + /micromark-factory-space@2.0.0: + resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} dependencies: - micromark-util-character: 1.1.0 - micromark-util-types: 1.0.2 + micromark-util-character: 2.0.1 + micromark-util-types: 2.0.0 dev: true - /micromark-factory-title@1.0.2: - resolution: {integrity: sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==} + /micromark-factory-title@2.0.0: + resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} dependencies: - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-factory-whitespace@1.0.0: - resolution: {integrity: sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==} + /micromark-factory-whitespace@2.0.0: + resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} dependencies: - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-util-character@1.1.0: - resolution: {integrity: sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==} + /micromark-util-character@2.0.1: + resolution: {integrity: sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==} dependencies: - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-util-chunked@1.0.0: - resolution: {integrity: sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==} + /micromark-util-chunked@2.0.0: + resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} dependencies: - micromark-util-symbol: 1.0.1 + micromark-util-symbol: 2.0.0 dev: true - /micromark-util-classify-character@1.0.0: - resolution: {integrity: sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==} + /micromark-util-classify-character@2.0.0: + resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} dependencies: - micromark-util-character: 1.1.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 + micromark-util-character: 2.0.1 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-util-combine-extensions@1.0.0: - resolution: {integrity: sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==} + /micromark-util-combine-extensions@2.0.0: + resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} dependencies: - micromark-util-chunked: 1.0.0 - micromark-util-types: 1.0.2 + micromark-util-chunked: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-util-decode-numeric-character-reference@1.0.0: - resolution: {integrity: sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==} + /micromark-util-decode-numeric-character-reference@2.0.0: + resolution: {integrity: sha512-pIgcsGxpHEtTG/rPJRz/HOLSqp5VTuIIjXlPI+6JSDlK2oljApusG6KzpS8AF0ENUMCHlC/IBb5B9xdFiVlm5Q==} dependencies: - micromark-util-symbol: 1.0.1 + micromark-util-symbol: 2.0.0 dev: true - /micromark-util-decode-string@1.0.2: - resolution: {integrity: sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==} + /micromark-util-decode-string@2.0.0: + resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} dependencies: decode-named-character-reference: 1.0.2 - micromark-util-character: 1.1.0 - micromark-util-decode-numeric-character-reference: 1.0.0 - micromark-util-symbol: 1.0.1 + micromark-util-character: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.0 + micromark-util-symbol: 2.0.0 dev: true - /micromark-util-encode@1.0.1: - resolution: {integrity: sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA==} + /micromark-util-encode@2.0.0: + resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} dev: true - /micromark-util-html-tag-name@1.1.0: - resolution: {integrity: sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA==} + /micromark-util-html-tag-name@2.0.0: + resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} dev: true - /micromark-util-normalize-identifier@1.0.0: - resolution: {integrity: sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==} + /micromark-util-normalize-identifier@2.0.0: + resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} dependencies: - micromark-util-symbol: 1.0.1 + micromark-util-symbol: 2.0.0 dev: true - /micromark-util-resolve-all@1.0.0: - resolution: {integrity: sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==} + /micromark-util-resolve-all@2.0.0: + resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} dependencies: - micromark-util-types: 1.0.2 + micromark-util-types: 2.0.0 dev: true - /micromark-util-sanitize-uri@1.1.0: - resolution: {integrity: sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg==} + /micromark-util-sanitize-uri@2.0.0: + resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} dependencies: - micromark-util-character: 1.1.0 - micromark-util-encode: 1.0.1 - micromark-util-symbol: 1.0.1 + micromark-util-character: 2.0.1 + micromark-util-encode: 2.0.0 + micromark-util-symbol: 2.0.0 dev: true - /micromark-util-subtokenize@1.0.2: - resolution: {integrity: sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==} + /micromark-util-subtokenize@2.0.0: + resolution: {integrity: sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==} dependencies: - micromark-util-chunked: 1.0.0 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 + devlop: 1.1.0 + micromark-util-chunked: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 dev: true - /micromark-util-symbol@1.0.1: - resolution: {integrity: sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ==} + /micromark-util-symbol@2.0.0: + resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} dev: true - /micromark-util-types@1.0.2: - resolution: {integrity: sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w==} + /micromark-util-types@2.0.0: + resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} dev: true - /micromark@3.1.0: - resolution: {integrity: sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA==} + /micromark@4.0.0: + resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} dependencies: '@types/debug': 4.1.7 debug: 4.3.4 decode-named-character-reference: 1.0.2 - micromark-core-commonmark: 1.0.6 - micromark-factory-space: 1.0.0 - micromark-util-character: 1.1.0 - micromark-util-chunked: 1.0.0 - micromark-util-combine-extensions: 1.0.0 - micromark-util-decode-numeric-character-reference: 1.0.0 - micromark-util-encode: 1.0.1 - micromark-util-normalize-identifier: 1.0.0 - micromark-util-resolve-all: 1.0.0 - micromark-util-sanitize-uri: 1.1.0 - micromark-util-subtokenize: 1.0.2 - micromark-util-symbol: 1.0.1 - micromark-util-types: 1.0.2 - uvu: 0.5.6 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.0 + micromark-factory-space: 2.0.0 + micromark-util-character: 2.0.1 + micromark-util-chunked: 2.0.0 + micromark-util-combine-extensions: 2.0.0 + micromark-util-decode-numeric-character-reference: 2.0.0 + micromark-util-encode: 2.0.0 + micromark-util-normalize-identifier: 2.0.0 + micromark-util-resolve-all: 2.0.0 + micromark-util-sanitize-uri: 2.0.0 + micromark-util-subtokenize: 2.0.0 + micromark-util-symbol: 2.0.0 + micromark-util-types: 2.0.0 transitivePeerDependencies: - supports-color dev: true @@ -4069,11 +4101,6 @@ packages: - supports-color dev: false - /mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - dev: true - /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} dev: false @@ -4126,32 +4153,32 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: true - /nest-winston@1.9.4(@nestjs/common@10.2.6)(winston@3.10.0): + /nest-winston@1.9.4(@nestjs/common@10.2.7)(winston@3.11.0): resolution: {integrity: sha512-ilEmHuuYSAI6aMNR120fLBl42EdY13QI9WRggHdEizt9M7qZlmXJwpbemVWKW/tqRmULjSx/otKNQ3GMQbfoUQ==} peerDependencies: '@nestjs/common': ^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 winston: ^3.0.0 dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) fast-safe-stringify: 2.1.1 - winston: 3.10.0 + winston: 3.11.0 dev: false - /nestjs-paginate@8.3.0(@nestjs/common@10.2.6)(@nestjs/swagger@7.1.12)(express@4.18.2)(fastify@4.23.2)(typeorm@0.3.17): - resolution: {integrity: sha512-ZZLzPhsngxeNUUY9AdnLFlnpDhD2paitaTFuvYIVMNURhcRG/XfgEIU4sfxXKzJWVU95zQEyMmcuTacYCybScw==} + /nestjs-paginate@8.3.3(@nestjs/common@10.2.7)(@nestjs/swagger@7.1.13)(express@4.18.2)(fastify@4.24.1)(typeorm@0.3.17): + resolution: {integrity: sha512-NuPqQrcMkodO6m4AfoI0KuX2yxFXlzgm16euyc4CyN7vTEn0Q2nN+v7Ams7LBKrxIsGzpwFux7hjLHBD/gEItg==} peerDependencies: - '@nestjs/common': ^10.2.4 - '@nestjs/swagger': ^7.1.10 + '@nestjs/common': ^10.2.6 + '@nestjs/swagger': ^7.1.12 express: ^4.18.2 - fastify: ^4.22.2 + fastify: ^4.23.2 typeorm: ^0.3.17 dependencies: - '@nestjs/common': 10.2.6(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) - '@nestjs/swagger': 7.1.12(@nestjs/common@10.2.6)(@nestjs/core@10.2.6)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) + '@nestjs/common': 10.2.7(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/swagger': 7.1.13(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) express: 4.18.2 - fastify: 4.23.2 + fastify: 4.24.1 lodash: 4.17.21 - typeorm: 0.3.17(better-sqlite3@8.6.0)(pg@8.11.3)(ts-node@10.9.1) + typeorm: 0.3.17(better-sqlite3@9.0.0)(pg@8.11.3)(ts-node@10.9.1) dev: false /node-7z@3.0.0: @@ -4507,12 +4534,21 @@ packages: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 9.1.1 + minipass: 5.0.0 + dev: false + /path-scurry@1.7.0: resolution: {integrity: sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==} engines: {node: '>=16 || 14 >=14.17'} dependencies: lru-cache: 9.1.1 minipass: 5.0.0 + dev: true /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -4604,8 +4640,8 @@ packages: engines: {node: '>=8.6'} dev: true - /pino-abstract-transport@1.0.0: - resolution: {integrity: sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==} + /pino-abstract-transport@1.1.0: + resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==} dependencies: readable-stream: 4.3.0 split2: 4.1.0 @@ -4615,20 +4651,20 @@ packages: resolution: {integrity: sha512-IWgSzUL8X1w4BIWTwErRgtV8PyOGOOi60uqv0oKuS/fOA8Nco/OeI6lBuc4dyP8MMfdFwyHqTMcBIA7nDiqEqA==} dev: false - /pino@8.14.1: - resolution: {integrity: sha512-8LYNv7BKWXSfS+k6oEc6occy5La+q2sPwU3q2ljTX5AZk7v+5kND2o5W794FyRaqha6DJajmkNRsWtPpFyMUdw==} + /pino@8.16.0: + resolution: {integrity: sha512-UUmvQ/7KTZt/vHjhRrnyS7h+J7qPBQnpG80V56xmIC+o9IqYmQOw/UIny9S9zYDfRBR0ClouCr464EkBMIT7Fw==} hasBin: true dependencies: atomic-sleep: 1.0.0 fast-redact: 3.1.2 on-exit-leak-free: 2.1.0 - pino-abstract-transport: 1.0.0 + pino-abstract-transport: 1.1.0 pino-std-serializers: 6.2.0 process-warning: 2.2.0 quick-format-unescaped: 4.0.4 real-require: 0.2.0 safe-stable-stringify: 2.3.1 - sonic-boom: 3.3.0 + sonic-boom: 3.7.0 thread-stream: 2.3.0 dev: false @@ -4690,15 +4726,15 @@ packages: fast-diff: 1.2.0 dev: true - /prettier-plugin-jsdoc@1.0.2(prettier@3.0.3): - resolution: {integrity: sha512-mhLT3qiSmfzjOEDvgLntX3XmSJaiDrgoN7WmOp4IH2mZ6LhbvZAnPDJH3Rs0k1O6WR7HcmM92fU1ArB0ALLG+A==} + /prettier-plugin-jsdoc@1.1.1(prettier@3.0.3): + resolution: {integrity: sha512-yA13k0StQ+g0RJBrmo2IldVSp3ANXlJdsNzQNhGtQ0LY7JFC+u01No/1Z9xp0ZhT4u98BXlPAc4SC0iambqy5A==} engines: {node: '>=14.13.1 || >=16.0.0'} peerDependencies: prettier: ^3.0.0 dependencies: binary-searching: 2.0.5 comment-parser: 1.4.0 - mdast-util-from-markdown: 1.3.0 + mdast-util-from-markdown: 2.0.0 prettier: 3.0.3 transitivePeerDependencies: - supports-color @@ -4934,12 +4970,12 @@ packages: glob: 9.3.1 dev: true - /rimraf@5.0.1: - resolution: {integrity: sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==} + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} engines: {node: '>=14'} hasBin: true dependencies: - glob: 10.2.6 + glob: 10.3.10 dev: false /run-applescript@5.0.0: @@ -4965,13 +5001,6 @@ packages: dependencies: tslib: 2.6.1 - /sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - dependencies: - mri: 1.2.0 - dev: true - /safe-array-concat@1.0.0: resolution: {integrity: sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==} engines: {node: '>=0.4'} @@ -5179,8 +5208,8 @@ packages: engines: {node: '>=8'} dev: true - /sonic-boom@3.3.0: - resolution: {integrity: sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==} + /sonic-boom@3.7.0: + resolution: {integrity: sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==} dependencies: atomic-sleep: 1.0.0 dev: false @@ -5248,6 +5277,15 @@ packages: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: false + /string.prototype.trim@1.2.7: resolution: {integrity: sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==} engines: {node: '>= 0.4'} @@ -5290,6 +5328,13 @@ packages: dependencies: ansi-regex: 5.0.1 + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: false + /strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -5340,8 +5385,8 @@ packages: engines: {node: '>= 0.4'} dev: true - /swagger-ui-dist@5.7.2: - resolution: {integrity: sha512-mVZc9QVQ6pTCV5crli3+Ng+DoMPwdtMHK8QLk2oX8Mtamp4D/hV+uYdC3lV0JZrDgpNEcjs0RrWTqMwwosuLPQ==} + /swagger-ui-dist@5.9.0: + resolution: {integrity: sha512-NUHSYoe5XRTk/Are8jPJ6phzBh3l9l33nEyXosM17QInoV95/jng8+PuSGtbD407QoPf93MH3Bkh773OgesJpA==} dev: false /symbol-observable@4.0.0: @@ -5354,7 +5399,7 @@ packages: engines: {node: ^14.18.0 || >=16.0.0} dependencies: '@pkgr/utils': 2.4.2 - tslib: 2.6.1 + tslib: 2.6.2 dev: true /tapable@2.2.1: @@ -5505,8 +5550,8 @@ packages: is-number: 7.0.0 dev: true - /toad-cache@3.2.0: - resolution: {integrity: sha512-Hj5zSqBS6OHbZoQk9IU8VqIr+0JUpwzunnwSlFJhG8aJSInYUMEuzItl3kJsGteTPd1qtflafdRHlRtUazYeqg==} + /toad-cache@3.3.0: + resolution: {integrity: sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==} engines: {node: '>=12'} dev: false @@ -5543,7 +5588,7 @@ packages: typescript: 5.2.2 dev: true - /ts-node@10.9.1(@types/node@20.6.5)(typescript@5.2.2): + /ts-node@10.9.1(@types/node@20.8.6)(typescript@5.2.2): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -5562,7 +5607,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 20.6.5 + '@types/node': 20.8.6 acorn: 8.10.0 acorn-walk: 8.2.0 arg: 4.1.3 @@ -5684,10 +5729,10 @@ packages: peerDependencies: typeorm: ^0.2.0 || ^0.3.0 dependencies: - typeorm: 0.3.17(better-sqlite3@8.6.0)(pg@8.11.3)(ts-node@10.9.1) + typeorm: 0.3.17(better-sqlite3@9.0.0)(pg@8.11.3)(ts-node@10.9.1) dev: false - /typeorm@0.3.17(better-sqlite3@8.6.0)(pg@8.11.3)(ts-node@10.9.1): + /typeorm@0.3.17(better-sqlite3@9.0.0)(pg@8.11.3)(ts-node@10.9.1): resolution: {integrity: sha512-UDjUEwIQalO9tWw9O2A4GU+sT3oyoUXheHJy4ft+RFdnRdQctdQ34L9SqE2p7LdwzafHx1maxT+bqXON+Qnmig==} engines: {node: '>= 12.9.0'} hasBin: true @@ -5747,7 +5792,7 @@ packages: dependencies: '@sqltools/formatter': 1.2.5 app-root-path: 3.1.0 - better-sqlite3: 8.6.0 + better-sqlite3: 9.0.0 buffer: 6.0.3 chalk: 4.1.2 cli-highlight: 2.1.11 @@ -5759,7 +5804,7 @@ packages: pg: 8.11.3 reflect-metadata: 0.1.13 sha.js: 2.4.11 - ts-node: 10.9.1(@types/node@20.6.5)(typescript@5.2.2) + ts-node: 10.9.1(@types/node@20.8.6)(typescript@5.2.2) tslib: 2.6.1 uuid: 9.0.0 yargs: 17.6.2 @@ -5788,15 +5833,18 @@ packages: which-boxed-primitive: 1.0.2 dev: true + /undici-types@5.25.3: + resolution: {integrity: sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==} + /unidecode@0.1.8: resolution: {integrity: sha512-SdoZNxCWpN2tXTCrGkPF/0rL2HEq+i2gwRG1ReBvx8/0yTzC3enHfugOf8A9JBShVwwrRIkLX0YcDUGbzjbVCA==} engines: {node: '>= 0.4.12'} dev: false - /unist-util-stringify-position@3.0.3: - resolution: {integrity: sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==} + /unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} dependencies: - '@types/unist': 2.0.6 + '@types/unist': 3.0.0 dev: true /universalify@2.0.0: @@ -5852,17 +5900,6 @@ packages: hasBin: true dev: false - /uvu@0.5.6: - resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} - engines: {node: '>=8'} - hasBin: true - dependencies: - dequal: 2.0.3 - diff: 5.1.0 - kleur: 4.1.5 - sade: 1.8.1 - dev: true - /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -6000,7 +6037,7 @@ packages: triple-beam: 1.3.0 dev: false - /winston-daily-rotate-file@4.7.1(winston@3.10.0): + /winston-daily-rotate-file@4.7.1(winston@3.11.0): resolution: {integrity: sha512-7LGPiYGBPNyGHLn9z33i96zx/bd71pjBn9tqQzO3I4Tayv94WPmBNwKC7CO1wPHdP9uvu+Md/1nr6VSH9h0iaA==} engines: {node: '>=8'} peerDependencies: @@ -6009,7 +6046,7 @@ packages: file-stream-rotator: 0.6.1 object-hash: 2.2.0 triple-beam: 1.3.0 - winston: 3.10.0 + winston: 3.11.0 winston-transport: 4.5.0 dev: false @@ -6022,11 +6059,11 @@ packages: triple-beam: 1.3.0 dev: false - /winston@3.10.0: - resolution: {integrity: sha512-nT6SIDaE9B7ZRO0u3UvdrimG0HkB7dSTAgInQnNR2SOPJ4bvq5q79+pXLftKmP52lJGW15+H5MCK0nM9D3KB/g==} + /winston@3.11.0: + resolution: {integrity: sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==} engines: {node: '>= 12.0.0'} dependencies: - '@colors/colors': 1.5.0 + '@colors/colors': 1.6.0 '@dabh/diagnostics': 2.0.3 async: 3.2.4 is-stream: 2.0.1 @@ -6056,6 +6093,15 @@ packages: string-width: 4.2.3 strip-ansi: 6.0.1 + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: false + /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} diff --git a/src/app.module.ts b/src/app.module.ts index 37e79d8..927b114 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,3 +1,4 @@ +import { DatabaseModule } from "./modules/database/database.module"; import { FilesModule } from "./modules/files/files.module"; import { BoxartsModule } from "./modules/boxarts/boxarts.module"; import { DevelopersModule } from "./modules/developers/developers.module"; @@ -16,11 +17,9 @@ import { APP_FILTER } from "@nestjs/core"; import { LoggingExceptionFilter } from "./modules/log/exception.filter"; import { RawgModule } from "./modules/providers/rawg/rawg.module"; import { DefaultStrategy } from "./modules/auth/basic-auth.strategy"; -import { TypeOrmModule } from "@nestjs/typeorm"; -import { getDatabaseConfiguration } from "./modules/database/db_configuration"; @Module({ imports: [ - TypeOrmModule.forRoot(getDatabaseConfiguration()), + DatabaseModule, ScheduleModule.forRoot(), BoxartsModule, DevelopersModule, diff --git a/src/globals.ts b/src/globals.ts index ac4a485..8e120b9 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -55,3 +55,19 @@ export default { "image/x-icon", ], }; + +export interface FindOptions { + /** + * Indicates whether deleted (sub)entities should be loaded. + * + * @default false + */ + loadDeletedEntities: boolean; + + /** + * Indicates whether related entities should be loaded. + * + * @default false + */ + loadRelations: boolean; +} diff --git a/src/modules/boxarts/boxarts.service.ts b/src/modules/boxarts/boxarts.service.ts index 611409f..38898de 100644 --- a/src/modules/boxarts/boxarts.service.ts +++ b/src/modules/boxarts/boxarts.service.ts @@ -19,7 +19,7 @@ export class BoxArtsService { private imagesService: ImagesService, ) {} - public async checkBoxArts(games: Game[]): Promise { + public async checkMultiple(games: Game[]): Promise { if (configuration.TESTING.GOOGLE_API_DISABLED) { this.logger.warn( "Skipping Box Art Search because Google API is disabled", @@ -31,7 +31,7 @@ export class BoxArtsService { for (const game of games) { try { - await this.checkBoxArt(game); + await this.check(game); this.logger.debug( { gameId: game.id, title: game.title }, `Checked BoxArt Successfully`, @@ -41,7 +41,7 @@ export class BoxArtsService { { gameId: game.id, title: game.title, - error: error, + error, }, "Checking BoxArt Failed!", ); @@ -56,10 +56,10 @@ export class BoxArtsService { * * @param game - The game for which to check the box art. */ - public async checkBoxArt(game: Game): Promise { + public async check(game: Game): Promise { if ( game.box_image?.id && - (await this.imagesService.isImageAvailable(game.box_image.id)) + (await this.imagesService.isAvailable(game.box_image.id)) ) { this.logger.debug(`Box Art for "${game.title}" is still available`); return; @@ -179,8 +179,8 @@ export class BoxArtsService { ): Promise { for (const image of images) { try { - game.box_image = await this.imagesService.downloadImageByUrl(image.url); - await this.gamesService.saveGame(game); + game.box_image = await this.imagesService.downloadByUrl(image.url); + await this.gamesService.save(game); this.logger.log( `Saved new Box Art for "${game.title}" (${image.width}px x ${image.height}px) | URL: ${image.url}`, ); diff --git a/src/modules/database/database.controller.ts b/src/modules/database/database.controller.ts new file mode 100644 index 0000000..4edc973 --- /dev/null +++ b/src/modules/database/database.controller.ts @@ -0,0 +1,66 @@ +import { + Controller, + Get, + Headers, + Post, + UploadedFile, + UseInterceptors, +} from "@nestjs/common"; +import { + ApiOperation, + ApiTags, + ApiBasicAuth, + ApiHeader, +} from "@nestjs/swagger"; +import { MinimumRole } from "../pagination/minimum-role.decorator"; +import { Role } from "../users/models/role.enum"; +import { DatabaseService } from "./database.service"; +import { FileInterceptor } from "@nestjs/platform-express"; + +@ApiBasicAuth() +@ApiTags("database") +@Controller("database") +export class DatabaseController { + constructor(private databaseService: DatabaseService) {} + + @Get("backup") + @ApiOperation({ + summary: + "Create and download a database backup. This process will generate an unencrypted file containing all the data currently stored in the database, which can be restored at a later time.", + operationId: "backupDatabase", + }) + @ApiHeader({ + name: "X-Database-Password", + required: true, + description: + "This header should include the database password. Without the correct password, your request will be denied.", + example: "SecretPassword123", + }) + @MinimumRole(Role.ADMIN) + async backup(@Headers("X-Database-Password") password: string) { + return this.databaseService.backup(password); + } + + @Post("restore") + @ApiOperation({ + summary: + "Upload and restore a previously saved database dump. This action will replace all current data in the database.", + operationId: "restoreDatabase", + }) + @ApiHeader({ + name: "X-Database-Password", + required: true, + description: + "This header should include the database password. Without the correct password, your request will be denied.", + example: "SecretPassword123", + }) + @UseInterceptors(FileInterceptor("file")) + @MinimumRole(Role.ADMIN) + async restore( + @UploadedFile() + file: Express.Multer.File, + @Headers("X-Database-Password") password: string, + ) { + return await this.databaseService.restore(file, password); + } +} diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts new file mode 100644 index 0000000..b25810e --- /dev/null +++ b/src/modules/database/database.module.ts @@ -0,0 +1,12 @@ +import { DatabaseService } from "./database.service"; +import { DatabaseController } from "./database.controller"; +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { getDatabaseConfiguration } from "./db_configuration"; + +@Module({ + imports: [TypeOrmModule.forRoot(getDatabaseConfiguration())], + controllers: [DatabaseController], + providers: [DatabaseService], +}) +export class DatabaseModule {} diff --git a/src/modules/database/database.service.ts b/src/modules/database/database.service.ts new file mode 100644 index 0000000..fcdb112 --- /dev/null +++ b/src/modules/database/database.service.ts @@ -0,0 +1,245 @@ +import { + Injectable, + InternalServerErrorException, + Logger, + NotAcceptableException, + StreamableFile, + UnauthorizedException, +} from "@nestjs/common"; +import configuration from "../../configuration"; +import { + copyFileSync, + createReadStream, + existsSync, + statSync, + writeFileSync, +} from "fs"; +import { DataSource } from "typeorm"; +import unidecode from "unidecode"; +import filenameSanitizer from "sanitize-filename"; +import mime from "mime"; +import path from "path"; +import { exec } from "child_process"; +import { promisify } from "util"; + +@Injectable() +export class DatabaseService { + private readonly logger = new Logger(DatabaseService.name); + private execPromise = promisify(exec); + + constructor(private dataSource: DataSource) {} + + async backup(password: string): Promise { + if (configuration.TESTING.IN_MEMORY_DB) { + throw new NotAcceptableException( + "This server can't backup its data as it uses an in-memory database.", + ); + } + + this.validatePassword(password); + await this.disconnect(); + + let backupFile: Promise; + + switch (configuration.DB.SYSTEM) { + case "POSTGRESQL": + backupFile = this.backupPostgresql(this.generateBackupFilepath()); + break; + case "SQLITE": + backupFile = this.backupSqlite(this.generateBackupFilepath()); + break; + default: + throw new InternalServerErrorException( + "This server's DB_SYSTEM environment variable is set to an unknown value.", + ); + } + + await this.connect(); + return backupFile; + } + + async restore(file: Express.Multer.File, password: string) { + if (configuration.TESTING.IN_MEMORY_DB) { + throw new NotAcceptableException( + "This server can't restore backups as it uses an in-memory database.", + ); + } + + this.validatePassword(password); + await this.disconnect(); + + switch (configuration.DB.SYSTEM) { + case "POSTGRESQL": + await this.restorePostgresql(file); + break; + case "SQLITE": + await this.restoreSqlite(file); + break; + default: + throw new InternalServerErrorException( + "This server's DB_SYSTEM environment variable is set to an unknown value.", + ); + } + + await this.connect(); + await this.migrate(); + } + + async connect() { + this.logger.log("Connecting Database..."); + return await this.dataSource.initialize(); + } + + async disconnect() { + this.logger.log("Disconnecting Database..."); + return await this.dataSource.destroy(); + } + + async migrate() { + this.logger.log("Migrating Database..."); + return await this.dataSource.runMigrations(); + } + + async backupPostgresql(backupFilePath: string): Promise { + this.logger.log("Backing up PostgreSQL Database..."); + try { + await this.execPromise( + `pg_dump -w -F t -h ${configuration.DB.HOST} -p ${configuration.DB.PORT} -U ${configuration.DB.USERNAME} -d ${configuration.DB.DATABASE} -f ${backupFilePath}`, + { env: { PGPASSWORD: configuration.DB.PASSWORD } }, + ); + + return this.createStreamableFile(backupFilePath); + } catch (error) { + this.handleBackupError(error); + } + } + + private async backupSqlite(backupFilePath: string): Promise { + this.logger.log("Backing up SQLITE Database..."); + copyFileSync( + `${configuration.VOLUMES.SQLITEDB}/database.sqlite`, + backupFilePath, + ); + + return this.createStreamableFile(backupFilePath); + } + + async restorePostgresql(file: Express.Multer.File) { + this.logger.log("Restoring PostgreSQL Database..."); + try { + await this.backupPostgresql("/tmp/gamevault_database_pre_restore.db"); + + writeFileSync("/tmp/gamevault_database_restore.db", file.buffer); + + await this.execPromise( + `dropdb --if-exists -f -w -h ${configuration.DB.HOST} -p ${configuration.DB.PORT} -U ${configuration.DB.USERNAME} ${configuration.DB.DATABASE}`, + { env: { PGPASSWORD: configuration.DB.PASSWORD } }, + ); + + await this.execPromise( + `createdb -w -h ${configuration.DB.HOST} -p ${configuration.DB.PORT} -U ${configuration.DB.USERNAME} ${configuration.DB.DATABASE}`, + { env: { PGPASSWORD: configuration.DB.PASSWORD } }, + ); + + try { + await this.execPromise( + `pg_restore -O -w -F t -h ${configuration.DB.HOST} -p ${configuration.DB.PORT} -U ${configuration.DB.USERNAME} -d ${configuration.DB.DATABASE} /tmp/gamevault_database_restore.db`, + { env: { PGPASSWORD: configuration.DB.PASSWORD } }, + ); + + this.logger.log("Successfully restored PostgreSQL Database."); + } catch (error) { + this.logger.warn( + error, + "Restoring your backup might have encountered an issue. Please examine the logs. If it reads 'pg_restore: warning: errors ignored on restore,' things are likely alright. It could have succeeded.", + ); + } + } catch (error) { + this.logger.error(error, "Error restoring PostgreSQL database."); + + if (existsSync("/tmp/gamevault_database_pre_restore.db")) { + this.logger.log("Restoring pre-restore database."); + try { + await this.execPromise( + `dropdb --if-exists -f -w -h ${configuration.DB.HOST} -p ${configuration.DB.PORT} -U ${configuration.DB.USERNAME} ${configuration.DB.DATABASE}`, + { env: { PGPASSWORD: configuration.DB.PASSWORD } }, + ); + + await this.execPromise( + `createdb -w -h ${configuration.DB.HOST} -p ${configuration.DB.PORT} -U ${configuration.DB.USERNAME} ${configuration.DB.DATABASE}`, + { env: { PGPASSWORD: configuration.DB.PASSWORD } }, + ); + + await this.execPromise( + `pg_restore -O -w -F t -h ${configuration.DB.HOST} -p ${configuration.DB.PORT} -U ${configuration.DB.USERNAME} -d ${configuration.DB.DATABASE} /tmp/gamevault_database_pre_restore.db`, + { env: { PGPASSWORD: configuration.DB.PASSWORD } }, + ); + this.logger.log("Restored pre-restore database."); + } catch (error) { + this.logger.error( + error, + "Errors occured restoring pre-restore PostgreSQL database. Please restore the backup manually.", + ); + throw new InternalServerErrorException( + "Error restoring pre-restore PostgreSQL Database.", + ); + } + } + } + } + + private async restoreSqlite(file: Express.Multer.File) { + this.logger.log("Restoring SQLITE Database..."); + try { + if (existsSync(`${configuration.VOLUMES.SQLITEDB}/database.sqlite`)) { + this.backupSqlite("/tmp/gamevault_database_pre_restore.db"); + } + writeFileSync( + `${configuration.VOLUMES.SQLITEDB}/database.sqlite`, + file.buffer, + ); + } catch (error) { + this.logger.error(error, "Error restoring SQLITE database"); + if (existsSync("/tmp/gamevault_database_pre_restore.db")) { + this.logger.log("Restoring pre-restore database."); + copyFileSync( + "/tmp/gamevault_database_pre_restore.db", + `${configuration.VOLUMES.SQLITEDB}/database.sqlite`, + ); + this.logger.log("Restored pre-restore database."); + } + } + } + + private validatePassword(password: string) { + if (configuration.DB.PASSWORD !== password) { + throw new UnauthorizedException( + "The database password provided in the X-Database-Password Header is incorrect.", + ); + } + } + + private generateBackupFilepath(): string { + const now = new Date(); + const timestamp = now.toISOString().replace(/[:.]/g, "-"); + return `/tmp/gamevault_${configuration.SERVER.VERSION}_database_backup_${timestamp}.db`; + } + + private createStreamableFile(filePath: string): StreamableFile { + const file = createReadStream(filePath); + const length = statSync(filePath).size; + const type = mime.getType(filePath); + const filename = filenameSanitizer(unidecode(path.basename(filePath))); + + return new StreamableFile(file, { + disposition: `attachment; filename="${filename}"`, + length, + type, + }); + } + + private handleBackupError(error: unknown) { + this.logger.error(error, "Error backing up database"); + throw new InternalServerErrorException("Error backing up database."); + } +} diff --git a/src/modules/database/migrations/postgres/1696967362000-delete-empty-progresses.ts b/src/modules/database/migrations/postgres/1696967362000-delete-empty-progresses.ts new file mode 100644 index 0000000..7143093 --- /dev/null +++ b/src/modules/database/migrations/postgres/1696967362000-delete-empty-progresses.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { Progress } from "../../../progress/progress.entity"; +import { State } from "../../../progress/models/state.enum"; +import { Logger } from "@nestjs/common"; +export class DeleteEmptyProgresses1696967362000 implements MigrationInterface { + private readonly logger = new Logger(DeleteEmptyProgresses1696967362000.name); + name?: string; + transaction?: boolean; + + public async up(queryRunner: QueryRunner): Promise { + // Check for State Unplayed progresses with 0 Minutes. + const progressRepository = queryRunner.connection.getRepository(Progress); + const progressesToDelete = await progressRepository.findBy({ + minutes_played: 0, + state: State.UNPLAYED, + }); + progressRepository.remove(progressesToDelete); + this.logger.log( + `Database Migration deleted ${progressesToDelete.length} empty progresses from your database.`, + ); + } + + public async down(): Promise { + // This is a migration to delete useless data, no down function needed + } +} diff --git a/src/modules/database/migrations/sqlite/1696967362000-delete-empty-progresses.ts b/src/modules/database/migrations/sqlite/1696967362000-delete-empty-progresses.ts new file mode 100644 index 0000000..7143093 --- /dev/null +++ b/src/modules/database/migrations/sqlite/1696967362000-delete-empty-progresses.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { Progress } from "../../../progress/progress.entity"; +import { State } from "../../../progress/models/state.enum"; +import { Logger } from "@nestjs/common"; +export class DeleteEmptyProgresses1696967362000 implements MigrationInterface { + private readonly logger = new Logger(DeleteEmptyProgresses1696967362000.name); + name?: string; + transaction?: boolean; + + public async up(queryRunner: QueryRunner): Promise { + // Check for State Unplayed progresses with 0 Minutes. + const progressRepository = queryRunner.connection.getRepository(Progress); + const progressesToDelete = await progressRepository.findBy({ + minutes_played: 0, + state: State.UNPLAYED, + }); + progressRepository.remove(progressesToDelete); + this.logger.log( + `Database Migration deleted ${progressesToDelete.length} empty progresses from your database.`, + ); + } + + public async down(): Promise { + // This is a migration to delete useless data, no down function needed + } +} diff --git a/src/modules/developers/developers.service.ts b/src/modules/developers/developers.service.ts index 24fb997..9e95228 100644 --- a/src/modules/developers/developers.service.ts +++ b/src/modules/developers/developers.service.ts @@ -15,10 +15,7 @@ export class DevelopersService { * Returns the developer with the specified RAWG ID, creating a new developer * if one does not already exist. */ - async getOrCreateDeveloper( - name: string, - rawg_id: number, - ): Promise { + async getOrCreate(name: string, rawg_id: number): Promise { const existingDeveloper = await this.developerRepository.findOneBy({ rawg_id, }); diff --git a/src/modules/files/files.service.ts b/src/modules/files/files.service.ts index cb122bc..9c684d6 100644 --- a/src/modules/files/files.service.ts +++ b/src/modules/files/files.service.ts @@ -53,22 +53,20 @@ export class FilesService implements OnApplicationBootstrap { @Cron(`*/${configuration.GAMES.INDEX_INTERVAL_IN_MINUTES} * * * *`) public async index(): Promise { //Get all games in file system - const gamesInFileSystem = this.fetchFiles(); + const gamesInFileSystem = this.fetch(); //Feed Game - await this.ingestGames(gamesInFileSystem); + await this.ingest(gamesInFileSystem); //Get all games in database - const gamesInDatabase = await this.gamesService.getAllGames(); + const gamesInDatabase = await this.gamesService.getAll(); //Check integrity of games in database with games in file system - await this.integrityCheck(gamesInFileSystem, gamesInDatabase); + await this.checkIntegrity(gamesInFileSystem, gamesInDatabase); //Check cache of games in database - await this.rawgService.cacheGames(gamesInDatabase); + await this.rawgService.checkCache(gamesInDatabase); //Check boxart of games in database - await this.boxartService.checkBoxArts(gamesInDatabase); + await this.boxartService.checkMultiple(gamesInDatabase); } - private async ingestGames( - gamesInFileSystem: IGameVaultFile[], - ): Promise { + private async ingest(gamesInFileSystem: IGameVaultFile[]): Promise { this.logger.log("Started Game Ingestion"); for (const file of gamesInFileSystem) { const gameToIndex = new Game(); @@ -81,7 +79,7 @@ export class FilesService implements OnApplicationBootstrap { gameToIndex.early_access = this.extractEarlyAccessFlag(file.name); // For each file, check if it already exists in the database. const existingGameTuple: [GameExistence, Game] = - await this.gamesService.checkIfGameExistsInDatabase(gameToIndex); + await this.gamesService.checkIfExistsInDatabase(gameToIndex); switch (existingGameTuple[0]) { case GameExistence.EXISTS: { @@ -93,8 +91,8 @@ export class FilesService implements OnApplicationBootstrap { case GameExistence.DOES_NOT_EXIST: { this.logger.debug(`Indexing new file "${gameToIndex.file_path}"`); - gameToIndex.type = await this.detectGameType(gameToIndex.file_path); - await this.gamesService.saveGame(gameToIndex); + gameToIndex.type = await this.detectType(gameToIndex.file_path); + await this.gamesService.save(gameToIndex); continue; } @@ -102,11 +100,11 @@ export class FilesService implements OnApplicationBootstrap { this.logger.debug( `A soft-deleted duplicate of file "${gameToIndex.file_path}" has been detected in the database. Restoring it and updating the information.`, ); - const restoredGame = await this.gamesService.restoreGame( + const restoredGame = await this.gamesService.restore( existingGameTuple[1].id, ); - gameToIndex.type = await this.detectGameType(gameToIndex.file_path); - await this.updateGame(restoredGame, gameToIndex); + gameToIndex.type = await this.detectType(gameToIndex.file_path); + await this.update(restoredGame, gameToIndex); continue; } @@ -114,8 +112,8 @@ export class FilesService implements OnApplicationBootstrap { this.logger.debug( `Detected changes in file "${gameToIndex.file_path}" in the database. Updating the information.`, ); - gameToIndex.type = await this.detectGameType(gameToIndex.file_path); - await this.updateGame(existingGameTuple[1], gameToIndex); + gameToIndex.type = await this.detectType(gameToIndex.file_path); + await this.update(existingGameTuple[1], gameToIndex); continue; } } @@ -136,7 +134,7 @@ export class FilesService implements OnApplicationBootstrap { * @param {Game} updatesToApply - The updates to apply to the game. * @returns {Promise} The updated game. */ - private async updateGame( + private async update( gameToUpdate: Game, updatesToApply: Game, ): Promise { @@ -155,7 +153,7 @@ export class FilesService implements OnApplicationBootstrap { `Updated new Game Information for "${gameToUpdate.file_path}".`, ); - return this.gamesService.saveGame(updatedGame); + return this.gamesService.save(updatedGame); } /** @@ -251,7 +249,7 @@ export class FilesService implements OnApplicationBootstrap { return detectedPatterns.length > 0; } - private async detectGameType(path: string): Promise { + private async detectType(path: string): Promise { try { if (/\(W_P\)/.test(path)) { this.logger.debug( @@ -345,10 +343,7 @@ export class FilesService implements OnApplicationBootstrap { }); } - private async archiveFiles( - output: string, - sourcePath: string, - ): Promise { + private async archive(output: string, sourcePath: string): Promise { if (!existsSync(sourcePath)) { throw new NotFoundException(`The game file could not be found.`); } @@ -378,7 +373,7 @@ export class FilesService implements OnApplicationBootstrap { * the database. * @returns */ - private async integrityCheck( + private async checkIntegrity( gamesInFileSystem: IGameVaultFile[], gamesInDatabase: Game[], ): Promise { @@ -398,7 +393,7 @@ export class FilesService implements OnApplicationBootstrap { ); // If game is not in file system, mark it as deleted if (!gameInFileSystem) { - await this.gamesService.deleteGame(gameInDatabase); + await this.gamesService.delete(gameInDatabase); this.logger.log( `Game "${gameInDatabase.file_path}" marked as deleted, as it can not be found in the filesystem.`, ); @@ -422,7 +417,7 @@ export class FilesService implements OnApplicationBootstrap { * @throws {Error} - If there's an error during the process. * @public */ - private fetchFiles(): IGameVaultFile[] { + private fetch(): IGameVaultFile[] { try { if (configuration.TESTING.MOCK_FILES) { return mock; @@ -464,7 +459,7 @@ export class FilesService implements OnApplicationBootstrap { * the downloaded game file. * @public */ - public async downloadGame( + public async download( gameId: number, speedlimit?: number, ): Promise { @@ -477,7 +472,7 @@ export class FilesService implements OnApplicationBootstrap { speedlimit *= 1024; } - const game = await this.gamesService.getGameById(gameId); + const game = await this.gamesService.findByIdOrFail(gameId); let fileDownloadPath = game.file_path; if (!globals.ARCHIVE_FORMATS.includes(path.extname(game.file_path))) { @@ -491,7 +486,7 @@ export class FilesService implements OnApplicationBootstrap { this.logger.debug( `Temporarily tarballing "${game.file_path}" as "${fileDownloadPath}" for downloading...`, ); - await this.archiveFiles(fileDownloadPath, game.file_path); + await this.archive(fileDownloadPath, game.file_path); } } @@ -502,19 +497,18 @@ export class FilesService implements OnApplicationBootstrap { const file = createReadStream(fileDownloadPath).pipe( new Throttle(speedlimit), ); - const type = mime.getType(fileDownloadPath); + const length = statSync(fileDownloadPath).size; + const type = mime.getType(fileDownloadPath); const filename = filenameSanitizer( unidecode(path.basename(fileDownloadPath)), ); - const headers = { + return new StreamableFile(file, { disposition: `attachment; filename="${filename}"`, - length: statSync(fileDownloadPath).size, + length, type, - }; - - return new StreamableFile(file, headers); + }); } /** diff --git a/src/modules/games/game.entity.ts b/src/modules/games/game.entity.ts index 10ee302..4bafdf7 100644 --- a/src/modules/games/game.entity.ts +++ b/src/modules/games/game.entity.ts @@ -181,7 +181,7 @@ export class Game extends DatabaseEntity { type: () => Progress, isArray: true, }) - progresses: Progress[]; + progresses?: Progress[]; @JoinTable() @ManyToMany(() => Publisher, (publisher) => publisher.games) @@ -190,7 +190,7 @@ export class Game extends DatabaseEntity { type: () => Publisher, isArray: true, }) - publishers: Publisher[]; + publishers?: Publisher[]; @JoinTable() @ManyToMany(() => Developer, (developer) => developer.games) @@ -199,7 +199,7 @@ export class Game extends DatabaseEntity { type: () => Developer, isArray: true, }) - developers: Developer[]; + developers?: Developer[]; @JoinTable() @ManyToMany(() => Store, (store) => store.games) @@ -208,7 +208,7 @@ export class Game extends DatabaseEntity { type: () => Store, isArray: true, }) - stores: Store[]; + stores?: Store[]; @JoinTable() @ManyToMany(() => Tag, (tag) => tag.games) @@ -217,7 +217,7 @@ export class Game extends DatabaseEntity { type: () => Tag, isArray: true, }) - tags: Tag[]; + tags?: Tag[]; @JoinTable() @ManyToMany(() => Genre, (genre) => genre.games) @@ -226,5 +226,5 @@ export class Game extends DatabaseEntity { type: () => Genre, isArray: true, }) - genres: Genre[]; + genres?: Genre[]; } diff --git a/src/modules/games/games.controller.ts b/src/modules/games/games.controller.ts index b8bc40d..80e0136 100644 --- a/src/modules/games/games.controller.ts +++ b/src/modules/games/games.controller.ts @@ -66,7 +66,7 @@ export class GamesController { operationId: "getGames", }) @MinimumRole(Role.GUEST) - async getGames(@Paginate() query: PaginateQuery): Promise> { + async get(@Paginate() query: PaginateQuery): Promise> { return paginate(query, this.gamesRepository, { paginationType: PaginationType.TAKE_AND_SKIP, defaultLimit: 100, @@ -114,8 +114,8 @@ export class GamesController { }) @ApiOkResponse({ type: () => Game }) @MinimumRole(Role.GUEST) - async getRandomGame(): Promise { - return await this.gamesService.getRandomGame(); + async getRandom(): Promise { + return await this.gamesService.getRandom(); } /** @@ -131,8 +131,11 @@ export class GamesController { }) @ApiOkResponse({ type: () => Game }) @MinimumRole(Role.GUEST) - async getGameById(@Param() params: IdDto): Promise { - return await this.gamesService.getGameById(Number(params.id), true); + async findById(@Param() params: IdDto): Promise { + return await this.gamesService.findByIdOrFail(Number(params.id), { + loadRelations: true, + loadDeletedEntities: true, + }); } /** * Download a game by its ID. @@ -157,11 +160,11 @@ export class GamesController { }) @MinimumRole(Role.USER) @ApiOkResponse({ type: () => StreamableFile }) - async downloadGame( + async download( @Param() params: IdDto, @Headers("X-Download-Speed-Limit") speedlimit?: string, ): Promise { - return await this.filesService.downloadGame( + return await this.filesService.download( Number(params.id), Number(speedlimit), ); @@ -174,12 +177,12 @@ export class GamesController { }) @ApiBody({ type: () => UpdateGameDto }) @MinimumRole(Role.EDITOR) - async updateGame( + async update( @Param() params: IdDto, @Body() dto: UpdateGameDto, @Request() req: { gamevaultuser: GamevaultUser }, ): Promise { - return await this.gamesService.updateGame( + return await this.gamesService.update( Number(params.id), dto, req.gamevaultuser.username, diff --git a/src/modules/games/games.service.ts b/src/modules/games/games.service.ts index ed0b580..b4b264d 100644 --- a/src/modules/games/games.service.ts +++ b/src/modules/games/games.service.ts @@ -14,6 +14,7 @@ import { GameExistence } from "./models/game-existence.enum"; import { BoxArtsService } from "../boxarts/boxarts.service"; import { UpdateGameDto } from "./models/update-game.dto"; import { ImagesService } from "../images/images.service"; +import { FindOptions } from "../../globals"; @Injectable() export class GamesService { @@ -28,21 +29,14 @@ export class GamesService { private imagesService: ImagesService, ) {} - /** - * Retrieves a Game from the database by its id. - * - * @param id - The id of the Game to retrieve. - * @param [loadRelations=false] - A flag indicating whether to load the Game's - * related entities. Default is `false` - * @returns - A Promise that resolves to the retrieved Game. - * @throws {NotFoundException} - If a Game with the specified id is not found - * in the database. - */ - public async getGameById(id: number, loadRelations = false): Promise { + public async findByIdOrFail( + id: number, + options: FindOptions = { loadDeletedEntities: true, loadRelations: false }, + ): Promise { try { return await this.gamesRepository.findOneOrFail({ where: { id }, - relations: loadRelations + relations: options.loadRelations ? [ "developers", "publishers", @@ -55,7 +49,7 @@ export class GamesService { "background_image", ] : [], - withDeleted: true, + withDeleted: options.loadDeletedEntities, }); } catch (error) { throw new NotFoundException( @@ -73,7 +67,7 @@ export class GamesService { * @throws {InternalServerErrorException} If the game does not have necessary * data for dupe-checking. */ - public async checkIfGameExistsInDatabase( + public async checkIfExistsInDatabase( game: Game, ): Promise<[GameExistence, Game]> { if (!game.file_path || (!game.title && !game.release_date)) { @@ -154,11 +148,11 @@ export class GamesService { * @throws {Error} If there is an error retrieving the games from the * database. */ - public async getAllGames(): Promise { + public async getAll(): Promise { return this.gamesRepository.find(); } - public async getRandomGame(): Promise { + public async getRandom(): Promise { const game = await this.gamesRepository .createQueryBuilder("game") .select("game.id") @@ -166,7 +160,10 @@ export class GamesService { .limit(1) .getOne(); - return this.getGameById(game.id, true); + return this.findByIdOrFail(game.id, { + loadDeletedEntities: true, + loadRelations: true, + }); } /** @@ -179,9 +176,9 @@ export class GamesService { * @throws {NotFoundException} - If the game with the provided ID is not found * in the database. */ - public async remapGame(id: number, new_rawg_id: number): Promise { + public async remap(id: number, new_rawg_id: number): Promise { // Fetch the game to remap from the database and set the new rawg_id - let game = await this.getGameById(id); + let game = await this.findByIdOrFail(id); game.rawg_id = new_rawg_id; // Null all related fields but keep progresses @@ -201,11 +198,11 @@ export class GamesService { game.tags = []; game.genres = []; - game = await this.saveGame(game); + game = await this.save(game); // Recache the game - await this.rawgService.cacheGames([game]); + await this.rawgService.checkCache([game]); // Refetch the boxart - await this.boxartService.checkBoxArt(game); + await this.boxartService.check(game); // Return the new game object return game; } @@ -216,7 +213,7 @@ export class GamesService { * @param game - The game object to save. * @returns - The saved game object. */ - public async saveGame(game: Game): Promise { + public async save(game: Game): Promise { return this.gamesRepository.save(game); } @@ -225,37 +222,38 @@ export class GamesService { * * @param id - The id of the game to delete. */ - public deleteGame(game: Game) { + public delete(game: Game) { return this.gamesRepository.softRemove(game); } - public async updateGame(id: number, dto: UpdateGameDto, username: string) { - const game = dto.rawg_id - ? await this.remapGame(id, dto.rawg_id) - : await this.getGameById(id); + public async update(id: number, dto: UpdateGameDto, username: string) { + const game = + dto.rawg_id != null + ? await this.remap(id, dto.rawg_id) + : await this.findByIdOrFail(id); // Updates BoxArt if Necessary - if (dto.box_image_url) { - game.box_image = await this.imagesService.downloadImageByUrl( + if (dto.box_image_url != null) { + game.box_image = await this.imagesService.downloadByUrl( dto.box_image_url, username, ); } - if (dto.box_image_id) + if (dto.box_image_id != null) game.box_image = await this.imagesService.findByIdOrFail( dto.box_image_id, ); // Updates Background Image if Necessary - if (dto.background_image_url) { - game.background_image = await this.imagesService.downloadImageByUrl( + if (dto.background_image_url != null) { + game.background_image = await this.imagesService.downloadByUrl( dto.background_image_url, username, ); } - if (dto.background_image_id) + if (dto.background_image_id != null) game.background_image = await this.imagesService.findByIdOrFail( dto.background_image_id, ); @@ -269,8 +267,8 @@ export class GamesService { * @param id - The id of the game to restore. * @returns - The restored game object. */ - public async restoreGame(id: number): Promise { + public async restore(id: number): Promise { await this.gamesRepository.recover({ id }); - return this.getGameById(id); + return this.findByIdOrFail(id); } } diff --git a/src/modules/genres/genres.service.ts b/src/modules/genres/genres.service.ts index ac4c491..b39ba5d 100644 --- a/src/modules/genres/genres.service.ts +++ b/src/modules/genres/genres.service.ts @@ -15,7 +15,7 @@ export class GenresService { * Returns the genre with the specified RAWG ID, creating a new genre if one * does not already exist. */ - async getOrCreateGenre(name: string, rawg_id: number): Promise { + async getOrCreate(name: string, rawg_id: number): Promise { const existingGenre = await this.tagRepository.findOneBy({ rawg_id }); if (existingGenre) return existingGenre; diff --git a/src/modules/health/health.controller.ts b/src/modules/health/health.controller.ts index ccf1807..6315905 100644 --- a/src/modules/health/health.controller.ts +++ b/src/modules/health/health.controller.ts @@ -1,20 +1,41 @@ import { Controller, Get } from "@nestjs/common"; import { ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; import { Public } from "../pagination/public.decorator"; - +import { Health } from "./models/health.model"; +import { HealthService } from "./health.service"; +import { MinimumRole } from "../pagination/minimum-role.decorator"; +import { Role } from "../users/models/role.enum"; @Controller("health") @ApiTags("health") export class HealthController { + constructor(private healthService: HealthService) {} + /** * Returns a lifesign. * * @returns */ @Get() - @ApiOperation({ summary: "returns a lifesign", operationId: "healthcheck" }) - @ApiOkResponse() + @ApiOkResponse({ type: () => Health }) + @ApiOperation({ + summary: + "returns a lifesign, if an admin calls this api additional server infos are returned.", + operationId: "healthcheck", + }) @Public() - healthcheck(): string { - return "

All systems green! 🤖🟢

"; + async healthcheck(): Promise { + return this.healthService.get(); + } + + @Get("admin") + @ApiOkResponse({ type: () => Health }) + @ApiOperation({ + summary: + "returns a lifesign and additional server metrics for administrators", + operationId: "healthcheck", + }) + @MinimumRole(Role.ADMIN) + async extensiveHealthcheck(): Promise { + return this.healthService.getExtensive(); } } diff --git a/src/modules/health/health.module.ts b/src/modules/health/health.module.ts index fa64833..c416237 100644 --- a/src/modules/health/health.module.ts +++ b/src/modules/health/health.module.ts @@ -1,10 +1,10 @@ +import { HealthService } from "./health.service"; import { Module } from "@nestjs/common"; import { HealthController } from "./health.controller"; - @Module({ imports: [], controllers: [HealthController], - providers: [], + providers: [HealthService], exports: [], }) export class HealthModule {} diff --git a/src/modules/health/health.service.ts b/src/modules/health/health.service.ts new file mode 100644 index 0000000..f7edd33 --- /dev/null +++ b/src/modules/health/health.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from "@nestjs/common"; +import { Health, HealthProtocolEntry } from "./models/health.model"; +import configuration from "../../configuration"; +import { HealthStatus } from "./models/health-status.enum"; + +@Injectable() +export class HealthService { + private epoch: Date = new Date(); + private currentHealth: Health = new Health(); + + constructor() { + this.set(HealthStatus.HEALTHY, "Server started successfully"); + this.currentHealth = this.getExtensive(); + } + + getExtensive(): Health { + const newHealth = new Health(); + newHealth.version = configuration.SERVER.VERSION; + newHealth.uptime = Math.floor( + (new Date().getTime() - this.epoch.getTime()) / 1000, + ); + newHealth.status = this.currentHealth.status; + newHealth.protocol = [...this.currentHealth.protocol]; + this.currentHealth = newHealth; + return newHealth; + } + + get(): Health { + const health = this.getExtensive(); + const healthCopy = { ...health }; + delete healthCopy.protocol; + delete healthCopy.uptime; + delete healthCopy.version; + return healthCopy; + } + + set(status: HealthStatus, reason: string) { + this.currentHealth.status = status; + this.currentHealth.protocol.push(new HealthProtocolEntry(status, reason)); + } +} diff --git a/src/modules/health/models/health-status.enum.ts b/src/modules/health/models/health-status.enum.ts new file mode 100644 index 0000000..dcda4c8 --- /dev/null +++ b/src/modules/health/models/health-status.enum.ts @@ -0,0 +1,4 @@ +export enum HealthStatus { + HEALTHY = "HEALTHY", + UNHEALTHY = "UNHEALTHY", +} diff --git a/src/modules/health/models/health.model.ts b/src/modules/health/models/health.model.ts new file mode 100644 index 0000000..2e14bdd --- /dev/null +++ b/src/modules/health/models/health.model.ts @@ -0,0 +1,59 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { HealthStatus } from "./health-status.enum"; + +export class HealthProtocolEntry { + constructor(status: HealthStatus, reason: string) { + this.timestamp = new Date(); + this.reason = reason; + this.status = status; + } + + @ApiProperty({ + description: "Timestamp of the protocol entry", + example: "2021-01-01T00:00:00.000Z", + }) + timestamp: Date; + + @ApiProperty({ + description: "Status that was set", + type: "enum", + enum: HealthStatus, + example: HealthStatus.UNHEALTHY, + }) + status: HealthStatus; + + @ApiProperty({ + description: "Reason for the status to be set", + example: "Database disconnected.", + }) + reason: string; +} + +export class Health { + @ApiProperty({ + description: "Current status of the server", + type: "enum", + enum: HealthStatus, + example: HealthStatus.HEALTHY, + }) + status: HealthStatus = HealthStatus.HEALTHY; + + @ApiPropertyOptional({ + description: "Server's version (Only visible to admins)", + example: "1.0.0", + }) + version?: string = ""; + + @ApiPropertyOptional({ + description: "Server's uptime in seconds (Only visible to admins)", + example: 300, + }) + uptime?: number = 0; + + @ApiPropertyOptional({ + description: "Server's health protocol (Only visible to admins)", + type: () => HealthProtocolEntry, + isArray: true, + }) + protocol?: HealthProtocolEntry[] = []; +} diff --git a/src/modules/images/images.controller.ts b/src/modules/images/images.controller.ts index 73a46e7..f4c8e8d 100644 --- a/src/modules/images/images.controller.ts +++ b/src/modules/images/images.controller.ts @@ -103,6 +103,6 @@ export class ImagesController { ) file: Express.Multer.File, ) { - return this.imagesService.uploadImage(file, req.gamevaultuser.username); + return this.imagesService.upload(file, req.gamevaultuser.username); } } diff --git a/src/modules/images/images.service.ts b/src/modules/images/images.service.ts index b05ed9f..9bb4fe3 100644 --- a/src/modules/images/images.service.ts +++ b/src/modules/images/images.service.ts @@ -32,7 +32,7 @@ export class ImagesService { private usersService: UsersService, ) {} - public async isImageAvailable(id: number): Promise { + public async isAvailable(id: number): Promise { try { if (!id) { throw new NotFoundException("No image id given!"); @@ -48,7 +48,7 @@ export class ImagesService { try { const image = await this.imageRepository.findOneByOrFail({ id }); if (!existsSync(image.path) || configuration.TESTING.MOCK_FILES) { - await this.deleteImage(image); + await this.delete(image); throw new NotFoundException("Image not found on filesystem."); } return image; @@ -57,7 +57,7 @@ export class ImagesService { } } - async downloadImageByUrl( + async downloadByUrl( sourceUrl: string, uploaderUsername?: string, ): Promise { @@ -67,22 +67,22 @@ export class ImagesService { try { if (uploaderUsername) { image.uploader = - await this.usersService.getUserByUsernameOrFail(uploaderUsername); + await this.usersService.findByUsernameOrFail(uploaderUsername); } this.logger.debug(`Downloading Image from "${image.source}" ...`); - const response = await this.downloadImageFromUrl(image.source); + const response = await this.fetchFromUrl(image.source); const imageBuffer = Buffer.from(response.data); - const fileType = this.checkImageFileType(imageBuffer); + const fileType = this.checkFileType(imageBuffer); image.path = `${configuration.VOLUMES.IMAGES}/${randomUUID()}.${ fileType.extension }`; - await this.saveImageToFileSystem(image.path, imageBuffer); + await this.saveToFileSystem(image.path, imageBuffer); return await this.imageRepository.save(image); } catch (error) { if (image.id) { - await this.deleteImage(image); + await this.delete(image); } throw new InternalServerErrorException( error, @@ -91,9 +91,7 @@ export class ImagesService { } } - private async downloadImageFromUrl( - sourceUrl: string, - ): Promise { + private async fetchFromUrl(sourceUrl: string): Promise { return await firstValueFrom( this.httpService .get(sourceUrl, { @@ -110,7 +108,7 @@ export class ImagesService { ); } - private checkImageFileType(imageBuffer: Buffer) { + private checkFileType(imageBuffer: Buffer) { const fileType = fileTypeChecker.detectFile(imageBuffer); if ( !configuration.IMAGE.SUPPORTED_IMAGE_FORMATS.includes(fileType.mimeType) @@ -122,7 +120,7 @@ export class ImagesService { return fileType; } - private async saveImageToFileSystem( + private async saveToFileSystem( path: string, imageBuffer: Buffer, ): Promise { @@ -138,7 +136,7 @@ export class ImagesService { this.logger.debug(`Saved image to '${path}'`); } - async deleteImage(image: Image): Promise { + async delete(image: Image): Promise { if (configuration.TESTING.MOCK_FILES) { this.logger.warn( "Not deleting image from the filesystem because TESTING_MOCK_FILES is set to true", @@ -153,25 +151,25 @@ export class ImagesService { ); } - public async uploadImage( + public async upload( file: Express.Multer.File, username: string, ): Promise { - const image = await this.createImageFromUpload(file, username); + const image = await this.createFromUpload(file, username); try { - await this.saveImageToFileSystem(image.path, file.buffer); + await this.saveToFileSystem(image.path, file.buffer); this.logger.log(`Uploaded image ${image.id} to "${image.path}"`); return await this.imageRepository.save(image); } catch (error) { - await this.deleteImage(image); + await this.delete(image); throw new InternalServerErrorException( "Error uploading image. Please retry or try another one.", ); } } - private async validateImage(imageBuffer: Buffer) { + private async validate(imageBuffer: Buffer) { const fileType = fileTypeChecker.detectFile(imageBuffer); if (!fileType?.extension || !fileType?.mimeType) { throw new BadRequestException( @@ -188,16 +186,15 @@ export class ImagesService { return fileType; } - private async createImageFromUpload( + private async createFromUpload( file: Express.Multer.File, username?: string, ): Promise { - const fileType = await this.validateImage(file.buffer); + const fileType = await this.validate(file.buffer); const image = new Image(); if (username) { - image.uploader = - await this.usersService.getUserByUsernameOrFail(username); + image.uploader = await this.usersService.findByUsernameOrFail(username); } image.path = `${configuration.VOLUMES.IMAGES}/${randomUUID()}.${ diff --git a/src/modules/progress/models/progress.dto.ts b/src/modules/progress/models/progress.dto.ts deleted file mode 100644 index 0a6a29b..0000000 --- a/src/modules/progress/models/progress.dto.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum, IsNotEmpty, IsNumber } from "class-validator"; -import { State } from "./state.enum"; - -export class ProgressDto { - @IsNumber() - @IsNotEmpty() - @ApiProperty({ - description: "minutes of progress in the game by the user", - example: 22, - }) - minutes_played: number; - - @IsEnum(State) - @IsNotEmpty() - @ApiProperty({ - description: "state of the game progress", - type: "enum", - enum: State, - example: State.PLAYING, - }) - state: State; -} diff --git a/src/modules/progress/models/update-progress.dto.ts b/src/modules/progress/models/update-progress.dto.ts new file mode 100644 index 0000000..f603033 --- /dev/null +++ b/src/modules/progress/models/update-progress.dto.ts @@ -0,0 +1,26 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsEnum, IsNotEmpty, IsNumber, IsOptional } from "class-validator"; +import { State } from "./state.enum"; + +export class UpdateProgressDto { + @IsOptional() + @IsNumber() + @IsNotEmpty() + @ApiPropertyOptional({ + description: + "minutes of progress in the game by the user, this can only be incremented or be equal to the current value", + example: 22, + }) + minutes_played: number; + + @IsOptional() + @IsEnum(State) + @IsNotEmpty() + @ApiPropertyOptional({ + description: "the new state of the game progress", + type: "enum", + enum: State, + example: State.PLAYING, + }) + state: State; +} diff --git a/src/modules/progress/progress.controller.ts b/src/modules/progress/progress.controller.ts index e6bf708..f3b1681 100644 --- a/src/modules/progress/progress.controller.ts +++ b/src/modules/progress/progress.controller.ts @@ -22,7 +22,7 @@ import { ProgressService } from "./progress.service"; import { MinimumRole } from "../pagination/minimum-role.decorator"; import { Role } from "../users/models/role.enum"; import { GamevaultUser } from "../users/gamevault-user.entity"; -import { ProgressDto } from "./models/progress.dto"; +import { UpdateProgressDto } from "./models/update-progress.dto"; import { UserIdGameIdDto } from "./models/user-id-game-id.dto"; @Controller("progresses") @@ -61,8 +61,8 @@ export class ProgressController { }) @MinimumRole(Role.GUEST) @ApiOkResponse({ type: () => Progress, isArray: true }) - async getAllProgresses(): Promise { - return await this.progressService.getAllProgresses(); + async getAll(): Promise { + return await this.progressService.getAll(); } /** @@ -81,8 +81,8 @@ export class ProgressController { }) @MinimumRole(Role.GUEST) @ApiOkResponse({ type: () => Progress, isArray: true }) - async getProgressById(@Param() params: IdDto): Promise { - return await this.progressService.getProgressById(Number(params.id)); + async findById(@Param() params: IdDto): Promise { + return await this.progressService.findById(Number(params.id)); } /** @@ -104,11 +104,11 @@ export class ProgressController { }) @ApiOkResponse({ type: () => Progress, isArray: true }) @MinimumRole(Role.USER) - async deleteProgressById( + async deleteById( @Param() params: IdDto, @Request() req: { gamevaultuser: GamevaultUser }, ): Promise { - return await this.progressService.deleteProgressById( + return await this.progressService.delete( Number(params.id), req.gamevaultuser.username, ); @@ -127,8 +127,8 @@ export class ProgressController { }) @MinimumRole(Role.GUEST) @ApiOkResponse({ type: () => Progress, isArray: true }) - async getProgressesByUser(@Param() params: IdDto) { - return await this.progressService.getProgressesByUser(Number(params.id)); + async findByUserId(@Param() params: IdDto) { + return await this.progressService.findByUserId(Number(params.id)); } /** @@ -145,8 +145,8 @@ export class ProgressController { }) @MinimumRole(Role.GUEST) @ApiOkResponse({ type: () => Progress, isArray: true }) - async getProgressesByGame(@Param() params: IdDto): Promise { - return await this.progressService.getProgressesByGame(Number(params.id)); + async findByGameId(@Param() params: IdDto): Promise { + return await this.progressService.findByGameId(Number(params.id)); } /** @@ -166,10 +166,10 @@ export class ProgressController { }) @MinimumRole(Role.GUEST) @ApiOkResponse({ type: () => Progress }) - async getProgressByUserAndGame( + async findByUserAndGameOrFail( @Param() params: UserIdGameIdDto, ): Promise { - return await this.progressService.getProgressByUserAndGame( + return await this.progressService.findOrCreateByUserIdAndGameId( Number(params.userId), Number(params.gameId), ); @@ -188,19 +188,19 @@ export class ProgressController { * @throws {Error} If there was an error setting the progress. */ @Put("/user/:userId/game/:gameId") - @ApiBody({ type: () => ProgressDto }) + @ApiBody({ type: () => UpdateProgressDto }) @ApiOperation({ summary: "create or update a progress", operationId: "setProgressForUser", }) @ApiOkResponse({ type: () => Progress }) @MinimumRole(Role.USER) - async setProgressForUser( + async set( @Param() params: UserIdGameIdDto, - @Body() progress: ProgressDto, + @Body() progress: UpdateProgressDto, @Request() req: { gamevaultuser: GamevaultUser }, ): Promise { - return await this.progressService.setProgress( + return await this.progressService.set( Number(params.userId), Number(params.gameId), progress, @@ -226,11 +226,11 @@ export class ProgressController { }) @ApiOkResponse({ type: () => Progress }) @MinimumRole(Role.USER) - async incrementProgressForUser( + async incrementByOne( @Param() params: UserIdGameIdDto, @Request() req: { gamevaultuser: GamevaultUser }, ): Promise { - return await this.progressService.incrementProgress( + return await this.progressService.increment( Number(params.userId), Number(params.gameId), req.gamevaultuser.username, @@ -260,7 +260,7 @@ export class ProgressController { @Param() params: IncrementProgressByMinutesDto, @Request() req: { gamevaultuser: GamevaultUser }, ): Promise { - return await this.progressService.incrementProgress( + return await this.progressService.increment( Number(params.userId), Number(params.gameId), req.gamevaultuser.username, diff --git a/src/modules/progress/progress.entity.ts b/src/modules/progress/progress.entity.ts index 8962a49..9fed67a 100644 --- a/src/modules/progress/progress.entity.ts +++ b/src/modules/progress/progress.entity.ts @@ -9,19 +9,19 @@ import { DatabaseEntity } from "../database/database.entity"; export class Progress extends DatabaseEntity { @Index() @ManyToOne(() => GamevaultUser, (user) => user.progresses) - @ApiProperty({ + @ApiPropertyOptional({ description: "user the progress belongs to", type: () => GamevaultUser, }) - user: GamevaultUser; + user?: GamevaultUser; @Index() @ManyToOne(() => Game, (game) => game.progresses) - @ApiProperty({ + @ApiPropertyOptional({ description: "game the progress belongs to", type: () => Game, }) - game: Game; + game?: Game; @Column({ default: 0 }) @ApiProperty({ diff --git a/src/modules/progress/progress.service.ts b/src/modules/progress/progress.service.ts index 25a21a1..0f60878 100644 --- a/src/modules/progress/progress.service.ts +++ b/src/modules/progress/progress.service.ts @@ -6,14 +6,14 @@ import { NotFoundException, } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; -import { IsNull, MoreThan, Not, Repository } from "typeorm"; +import { IsNull, Repository } from "typeorm"; import { Progress } from "./progress.entity"; import { State } from "./models/state.enum"; import { GamesService } from "../games/games.service"; import { UsersService } from "../users/users.service"; import path from "path"; import * as fs from "fs"; -import { ProgressDto } from "./models/progress.dto"; +import { UpdateProgressDto } from "./models/update-progress.dto"; @Injectable() export class ProgressService { @@ -45,11 +45,9 @@ export class ProgressService { } } - public async getAllProgresses() { + public async getAll() { return await this.progressRepository.find({ where: { - minutes_played: MoreThan(0), - state: Not(State.UNPLAYED), deleted_at: IsNull(), }, relations: ["game", "user"], @@ -58,14 +56,11 @@ export class ProgressService { }); } - public async getProgressById(progressId: number) { + public async findById(progressId: number) { try { return await this.progressRepository.findOneOrFail({ where: { id: progressId, - minutes_played: MoreThan(0), - state: Not(State.UNPLAYED), - deleted_at: IsNull(), }, relations: ["game", "user"], order: { minutes_played: "DESC" }, @@ -78,11 +73,11 @@ export class ProgressService { } } - public async deleteProgressById( + public async delete( progressId: number, executorUsername: string, ): Promise { - const progress = await this.getProgressById(progressId); + const progress = await this.findById(progressId); await this.usersService.checkIfUsernameMatchesIdOrIsAdmin( progress.user.id, @@ -95,13 +90,11 @@ export class ProgressService { return this.progressRepository.softRemove(progress); } - public async getProgressesByUser(userId: number) { + public async findByUserId(userId: number) { return await this.progressRepository.find({ order: { minutes_played: "DESC" }, where: { user: { id: userId }, - minutes_played: MoreThan(0), - state: Not(State.UNPLAYED), deleted_at: IsNull(), }, relations: ["game"], @@ -109,12 +102,10 @@ export class ProgressService { }); } - public async getProgressesByGame(gameId: number): Promise { + public async findByGameId(gameId: number): Promise { return await this.progressRepository.find({ where: { game: { id: gameId }, - minutes_played: MoreThan(0), - state: Not(State.UNPLAYED), deleted_at: IsNull(), }, relations: ["user"], @@ -123,36 +114,39 @@ export class ProgressService { }); } - public async getProgressByUserAndGame( + public async findOrCreateByUserIdAndGameId( userId: number, gameId: number, ): Promise { - let progress = await this.progressRepository.findOne({ - where: { - user: { id: userId }, - game: { id: gameId }, - minutes_played: MoreThan(0), - state: Not(State.UNPLAYED), - deleted_at: IsNull(), - }, - withDeleted: true, - }); - - if (!progress) { - progress = new Progress(); - progress.user = await this.usersService.getUserByIdOrFail(userId); - progress.game = await this.gamesService.getGameById(gameId); - progress.state = State.UNPLAYED; - progress.minutes_played = 0; + try { + return await this.progressRepository.findOneOrFail({ + where: { + user: { id: userId }, + game: { id: gameId }, + deleted_at: IsNull(), + }, + withDeleted: true, + }); + } catch (error) { + const newProgress = new Progress(); + newProgress.user = await this.usersService.findByIdOrFail(userId, { + loadDeletedEntities: true, + loadRelations: false, + }); + newProgress.game = await this.gamesService.findByIdOrFail(gameId, { + loadDeletedEntities: true, + loadRelations: false, + }); + newProgress.minutes_played = 0; + newProgress.state = State.UNPLAYED; + return newProgress; } - - return progress; } - public async setProgress( + public async set( userId: number, gameId: number, - progressDto: ProgressDto, + updateProgressDto: UpdateProgressDto, executorUsername: string, ) { await this.usersService.checkIfUsernameMatchesIdOrIsAdmin( @@ -160,29 +154,46 @@ export class ProgressService { executorUsername, ); - const progress = await this.getProgressByUserAndGame(userId, gameId); + const progress = await this.findOrCreateByUserIdAndGameId(userId, gameId); - progress.state = progressDto.state; - if (progress.minutes_played > progressDto.minutes_played) { - throw new ConflictException( - `New value for "minutes_played" cannot be less than previous value: ${progress.minutes_played} minutes`, - ); - } - if (progress.minutes_played !== progressDto.minutes_played) { + if (updateProgressDto.state != null) { + progress.state = updateProgressDto.state; if ( - progress.state !== State.INFINITE && - progress.state !== State.COMPLETED + updateProgressDto.state === State.UNPLAYED && + progress.id && + !progress.minutes_played && + !updateProgressDto.minutes_played ) { - progress.state = State.PLAYING; + this.progressRepository.remove(progress); + this.logger.log( + `Deleted empty progress for user ${userId} and game ${gameId}`, + ); + return; + } + } + + if (updateProgressDto.minutes_played != null) { + if (progress.minutes_played > updateProgressDto.minutes_played) { + throw new ConflictException( + `New value for "minutes_played" cannot be less than previous value: ${progress.minutes_played} minutes`, + ); + } + if (progress.minutes_played !== updateProgressDto.minutes_played) { + if ( + progress.state !== State.INFINITE && + progress.state !== State.COMPLETED + ) { + progress.state = State.PLAYING; + } + progress.minutes_played = updateProgressDto.minutes_played; + progress.last_played_at = new Date(); } - progress.minutes_played = progressDto.minutes_played; - progress.last_played_at = new Date(); } this.logger.log(`Updated progress for user ${userId} and game ${gameId}`); return this.progressRepository.save(progress); } - public async incrementProgress( + public async increment( userId: number, gameId: number, executorUsername: string, @@ -192,7 +203,7 @@ export class ProgressService { userId, executorUsername, ); - const progress = await this.getProgressByUserAndGame(userId, gameId); + const progress = await this.findOrCreateByUserIdAndGameId(userId, gameId); if ( progress.state !== State.INFINITE && progress.state !== State.COMPLETED diff --git a/src/modules/providers/rawg/mapper.service.ts b/src/modules/providers/rawg/mapper.service.ts index 9ea1d20..4ea0c6a 100644 --- a/src/modules/providers/rawg/mapper.service.ts +++ b/src/modules/providers/rawg/mapper.service.ts @@ -61,7 +61,7 @@ export class RawgMapperService { for (const storeContainer of game.stores) { const store = storeContainer.store; entity.stores.push( - await this.storesService.getOrCreateStore(store.name, store.id), + await this.storesService.getOrCreate(store.name, store.id), ); } } catch (error) { @@ -86,7 +86,7 @@ export class RawgMapperService { if (!game.developers) return entity; for (const developer of game.developers) { entity.developers.push( - await this.developersService.getOrCreateDeveloper( + await this.developersService.getOrCreate( developer.name, developer.id, ), @@ -114,7 +114,7 @@ export class RawgMapperService { if (!game.publishers) return entity; for (const publisher of game.publishers) { entity.publishers.push( - await this.publishersService.getOrCreatePublisher( + await this.publishersService.getOrCreate( publisher.name, publisher.id, ), @@ -152,9 +152,7 @@ export class RawgMapperService { } else if (!isAlphanumeric) { this.logger.debug(`Skipping tag "${tag.name}" (invalid characters)`); } else { - entity.tags.push( - await this.tagService.getOrCreateTag(tag.name, tag.id), - ); + entity.tags.push(await this.tagService.getOrCreate(tag.name, tag.id)); } } } catch (error) { @@ -180,7 +178,7 @@ export class RawgMapperService { if (!game.genres) return entity; for (const genre of game.genres) { entity.genres.push( - await this.genreService.getOrCreateGenre(genre.name, genre.id), + await this.genreService.getOrCreate(genre.name, genre.id), ); } } catch (error) { @@ -204,11 +202,9 @@ export class RawgMapperService { if ( game.background_image && (!entity.background_image?.id || - !(await this.imagesService.isImageAvailable( - entity.background_image.id, - ))) + !(await this.imagesService.isAvailable(entity.background_image.id))) ) { - entity.background_image = await this.imagesService.downloadImageByUrl( + entity.background_image = await this.imagesService.downloadByUrl( game.background_image, ); } diff --git a/src/modules/providers/rawg/rawg.controller.ts b/src/modules/providers/rawg/rawg.controller.ts index 722cc88..e2fedb7 100644 --- a/src/modules/providers/rawg/rawg.controller.ts +++ b/src/modules/providers/rawg/rawg.controller.ts @@ -45,7 +45,7 @@ export class RawgController { }) @MinimumRole(Role.EDITOR) async searchRawg(@Query("query") query: string): Promise { - const rawgGames = await this.rawgService.getRawgGames(query); + const rawgGames = await this.rawgService.fetchMatching(query); // for each search result return a minimal gamevault game const games: MinimalGame[] = []; for (const rawgGame of rawgGames) { @@ -74,13 +74,16 @@ export class RawgController { }) @ApiOkResponse({ type: () => Game }) @MinimumRole(Role.EDITOR) - async recacheGame(@Param() params: IdDto): Promise { - let game = await this.gamesService.getGameById(Number(params.id)); + async recache(@Param() params: IdDto): Promise { + let game = await this.gamesService.findByIdOrFail(Number(params.id)); game.cache_date = null; - game = await this.gamesService.saveGame(game); - await this.rawgService.cacheGames([game]); - await this.boxartService.checkBoxArt(game); - return await this.gamesService.getGameById(Number(params.id), true); + game = await this.gamesService.save(game); + await this.rawgService.checkCache([game]); + await this.boxartService.check(game); + return await this.gamesService.findByIdOrFail(Number(params.id), { + loadDeletedEntities: true, + loadRelations: true, + }); } /** Manually triggers a recache from rawg-api for all games. */ @@ -93,14 +96,14 @@ export class RawgController { }) @ApiOkResponse({ type: () => Game, isArray: true }) @MinimumRole(Role.ADMIN) - async recacheAllGames(): Promise { - const gamesInDatabase = await this.gamesService.getAllGames(); + async recacheAll(): Promise { + const gamesInDatabase = await this.gamesService.getAll(); for (const game of gamesInDatabase) { game.cache_date = null; - await this.gamesService.saveGame(game); + await this.gamesService.save(game); } - await this.rawgService.cacheGames(gamesInDatabase); - await this.boxartService.checkBoxArts(gamesInDatabase); + await this.rawgService.checkCache(gamesInDatabase); + await this.boxartService.checkMultiple(gamesInDatabase); return "Recache successfuly completed"; } } diff --git a/src/modules/providers/rawg/rawg.service.ts b/src/modules/providers/rawg/rawg.service.ts index c07e801..1637011 100644 --- a/src/modules/providers/rawg/rawg.service.ts +++ b/src/modules/providers/rawg/rawg.service.ts @@ -36,7 +36,7 @@ export class RawgService { * @param games - An array of Game objects to check against the RAWG API. * @returns Returns a Promise with no return value. */ - public async cacheGames(games: Game[]): Promise { + public async checkCache(games: Game[]): Promise { if (configuration.TESTING.RAWG_API_DISABLED) { this.logger.warn( "Skipping RAWG Cache Check because RAWG API is disabled", @@ -55,7 +55,7 @@ export class RawgService { for (const game of games) { try { - await this.cacheGame(game); + await this.cache(game); this.logger.debug( { gameId: game.id, title: game.title }, `Game Cached Successfully`, @@ -84,7 +84,7 @@ export class RawgService { * @returns Returns a Promise with a mapped Game object that has been saved in * the database. */ - private async cacheGame(game: Game): Promise { + private async cache(game: Game): Promise { this.logger.debug(`Caching Game: "${game.title}"`); if (game.file_path.includes("(NC)")) { @@ -105,15 +105,15 @@ export class RawgService { let rawgEntry: RawgGame; if (game.rawg_id) { - rawgEntry = await this.getRawgGameById(game.rawg_id); + rawgEntry = await this.fetchById(game.rawg_id); } else { - rawgEntry = await this.getBestMatchingRawgGame( + rawgEntry = await this.getBestMatch( game.title, game.release_date?.getFullYear() || undefined, ); } const mappedGame = await this.mapper.mapRawgGameToGame(rawgEntry, game); - return await this.gamesService.saveGame(mappedGame); + return await this.gamesService.save(mappedGame); } /** @@ -126,11 +126,11 @@ export class RawgService { * @param releaseYear - The release year of the game to search for. * @returns Returns a Promise with the best matching RawgGame object. */ - private async getBestMatchingRawgGame( + private async getBestMatch( title: string, releaseYear?: number, ): Promise { - const sortedResults = await this.getRawgGames(title, releaseYear); + const sortedResults = await this.fetchMatching(title, releaseYear); const bestMatch = sortedResults[0]; if (bestMatch.probability != 1) { @@ -149,7 +149,7 @@ export class RawgService { this.logger.debug("-- END OF MATCHES --"); } - return this.getRawgGameById(bestMatch.id); + return this.fetchById(bestMatch.id); } /** @@ -162,7 +162,7 @@ export class RawgService { * representing search results. * @throws {NotFoundException} If no game is found in RAWG. */ - public async getRawgGames( + public async fetchMatching( title: string, releaseYear?: number, ): Promise { @@ -170,18 +170,18 @@ export class RawgService { // Step 1: Get games by title and release year (if provided) if (releaseYear) { - searchResults.push(...(await this.getGames(title, releaseYear)).results); + searchResults.push(...(await this.fetch(title, releaseYear)).results); } // Step 2: Get games by title only if Step 1 had no results or releaseYear is not provided if (searchResults.length === 0) { - searchResults.push(...(await this.getGames(title)).results); + searchResults.push(...(await this.fetch(title)).results); } // If no results found in both steps, try fuzzy search if (searchResults.length === 0) { searchResults.push( - ...(await this.getGames(title, undefined, false)).results, + ...(await this.fetch(title, undefined, false)).results, ); } @@ -194,7 +194,9 @@ export class RawgService { ); throw new NotFoundException( - `No game found in RAWG for "${title} (${releaseYear || "No Year"})"`, + `No game found in RAWG for "${title}" ${ + releaseYear ? `(${releaseYear})` : undefined + })`, ); } // Calculate and assign probabilities @@ -248,7 +250,7 @@ export class RawgService { * @param id - The RAWG ID of the game to retrieve. * @returns The RawgGame object associated with the specified ID. */ - private async getRawgGameById(id: number): Promise { + private async fetchById(id: number): Promise { try { const response = await firstValueFrom( this.httpService @@ -287,7 +289,7 @@ export class RawgService { * @throws {InternalServerErrorException} - Throws an error if the request to * the RAWG API fails. */ - private async getGames( + private async fetch( search?: string, releaseYear?: number, precise = true, diff --git a/src/modules/publishers/publishers.service.ts b/src/modules/publishers/publishers.service.ts index 84ee99e..c052394 100644 --- a/src/modules/publishers/publishers.service.ts +++ b/src/modules/publishers/publishers.service.ts @@ -15,10 +15,7 @@ export class PublishersService { * Returns the publisher with the specified RAWG ID, creating a new publisher * if one does not already exist. */ - async getOrCreatePublisher( - name: string, - rawg_id: number, - ): Promise { + async getOrCreate(name: string, rawg_id: number): Promise { const existingPublisher = await this.publisherRepository.findOneBy({ rawg_id, }); diff --git a/src/modules/stores/stores.service.ts b/src/modules/stores/stores.service.ts index 1b63df4..2c9fa44 100644 --- a/src/modules/stores/stores.service.ts +++ b/src/modules/stores/stores.service.ts @@ -15,7 +15,7 @@ export class StoresService { * Returns the store with the specified RAWG ID, creating a new store if one * does not already exist. */ - async getOrCreateStore(name: string, rawg_id: number): Promise { + async getOrCreate(name: string, rawg_id: number): Promise { const existingStore = await this.storeRepository.findOneBy({ rawg_id }); if (existingStore) return existingStore; diff --git a/src/modules/tags/tags.service.ts b/src/modules/tags/tags.service.ts index b26111c..58b1be6 100644 --- a/src/modules/tags/tags.service.ts +++ b/src/modules/tags/tags.service.ts @@ -16,7 +16,7 @@ export class TagsService { * Returns the tag with the specified RAWG ID, creating a new tag if one does * not already exist. */ - async getOrCreateTag(name: string, rawg_id: number): Promise { + async getOrCreate(name: string, rawg_id: number): Promise { const existingTag = await this.tagRepository.findOneBy({ rawg_id }); if (existingTag) return existingTag; diff --git a/src/modules/users/gamevault-user.entity.ts b/src/modules/users/gamevault-user.entity.ts index cd15121..4c75c14 100644 --- a/src/modules/users/gamevault-user.entity.ts +++ b/src/modules/users/gamevault-user.entity.ts @@ -1,4 +1,4 @@ -import { ApiProperty } from "@nestjs/swagger"; +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { Entity, Column, OneToMany, JoinColumn, ManyToOne } from "typeorm"; import { Image } from "../images/image.entity"; import { Progress } from "../progress/progress.entity"; @@ -25,11 +25,11 @@ export class GamevaultUser extends DatabaseEntity { orphanedRowAction: "soft-delete", }) @JoinColumn() - @ApiProperty({ + @ApiPropertyOptional({ type: () => Image, description: "the user's profile picture", }) - profile_picture: Image; + profile_picture?: Image; @ManyToOne(() => Image, { nullable: true, @@ -38,11 +38,11 @@ export class GamevaultUser extends DatabaseEntity { orphanedRowAction: "soft-delete", }) @JoinColumn() - @ApiProperty({ + @ApiPropertyOptional({ type: () => Image, description: "the user's profile art (background-picture)", }) - background_image: Image; + background_image?: Image; @Column({ unique: true, nullable: true }) @ApiProperty({ @@ -67,12 +67,12 @@ export class GamevaultUser extends DatabaseEntity { activated: boolean; @OneToMany(() => Progress, (progress) => progress.user) - @ApiProperty({ + @ApiPropertyOptional({ description: "progresses of the user", type: () => Progress, isArray: true, }) - progresses: Progress[]; + progresses?: Progress[]; @Column({ type: "simple-enum", @@ -89,10 +89,10 @@ export class GamevaultUser extends DatabaseEntity { role: Role; @OneToMany(() => Image, (image) => image.uploader) - @ApiProperty({ + @ApiPropertyOptional({ description: "images uploaded by this user", type: () => Image, isArray: true, }) - uploaded_images: Image[]; + uploaded_images?: Image[]; } diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index aa8172d..c7ed410 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -45,7 +45,7 @@ export class UsersController { @ApiOkResponse({ type: () => GamevaultUser, isArray: true }) @MinimumRole(Role.GUEST) async getUsers(): Promise { - return await this.usersService.getUsers(); + return await this.usersService.getAll(); } /** @@ -61,7 +61,7 @@ export class UsersController { }) @ApiOkResponse({ type: () => GamevaultUser, isArray: true }) async getAllUsers(): Promise { - return await this.usersService.getUsers(true, true); + return await this.usersService.getAll(true, true); } /** @@ -82,7 +82,7 @@ export class UsersController { async getMe( @Request() request: { gamevaultuser: GamevaultUser }, ): Promise { - return await this.usersService.getUserByUsernameOrFail( + return await this.usersService.findByUsernameOrFail( request.gamevaultuser.username, ); } @@ -108,7 +108,7 @@ export class UsersController { @Body() dto: UpdateUserDto, @Request() request: { gamevaultuser: GamevaultUser }, ): Promise { - const user = await this.usersService.getUserByUsernameOrFail( + const user = await this.usersService.findByUsernameOrFail( request.gamevaultuser.username, ); return await this.usersService.update( @@ -130,7 +130,7 @@ export class UsersController { @ApiOkResponse({ type: () => GamevaultUser }) @MinimumRole(Role.USER) async deleteMe(@Request() request): Promise { - const user = await this.usersService.getUserByUsernameOrFail( + const user = await this.usersService.findByUsernameOrFail( request.gamevaultuser.username, ); return await this.usersService.delete(user.id); @@ -152,7 +152,7 @@ export class UsersController { @MinimumRole(Role.GUEST) @ApiOkResponse({ type: () => GamevaultUser }) async getUserById(@Param() params: IdDto): Promise { - return await this.usersService.getUserByIdOrFail(Number(params.id)); + return await this.usersService.findByIdOrFail(Number(params.id)); } /** diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 54b3257..5e1fb42 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -24,6 +24,7 @@ import { GamevaultUser } from "./gamevault-user.entity"; import { ImagesService } from "../images/images.service"; import { UpdateUserDto } from "./models/update-user.dto"; import { Role } from "./models/role.enum"; +import { FindOptions } from "../../globals"; @Injectable() export class UsersService implements OnApplicationBootstrap { @@ -38,13 +39,13 @@ export class UsersService implements OnApplicationBootstrap { async onApplicationBootstrap() { try { - await this.setServerAdmin(); + await this.setAdmin(); } catch (error) { this.logger.error(error, "Error on FilesService Bootstrap"); } } - private async setServerAdmin() { + private async setAdmin() { try { if (!configuration.SERVER.ADMIN_USERNAME) { this.logger.warn( @@ -53,7 +54,7 @@ export class UsersService implements OnApplicationBootstrap { return; } - const user = await this.getUserByUsernameOrFail( + const user = await this.findByUsernameOrFail( configuration.SERVER.ADMIN_USERNAME, ); @@ -93,18 +94,20 @@ export class UsersService implements OnApplicationBootstrap { * @throws {NotFoundException} If the user with the specified ID does not * exist. */ - public async getUserByIdOrFail( + public async findByIdOrFail( id: number, - inludeDeletedUsers = false, + options: FindOptions = { loadRelations: true, loadDeletedEntities: true }, ): Promise { return await this.userRepository .findOneOrFail({ where: { id, - deleted_at: inludeDeletedUsers ? undefined : IsNull(), + deleted_at: options.loadDeletedEntities ? undefined : IsNull(), progresses: { deleted_at: IsNull() }, }, - relations: ["progresses", "progresses.game"], + relations: options.loadRelations + ? ["progresses", "progresses.game"] + : [], withDeleted: true, }) .catch(() => { @@ -120,17 +123,20 @@ export class UsersService implements OnApplicationBootstrap { * @throws {NotFoundException} - If the user with specified username is not * found */ - public async getUserByUsernameOrFail( + public async findByUsernameOrFail( username: string, + options: FindOptions = { loadRelations: true, loadDeletedEntities: true }, ): Promise { return await this.userRepository .findOneOrFail({ where: { username: ILike(username), - deleted_at: IsNull(), + deleted_at: options.loadDeletedEntities ? undefined : IsNull(), progresses: { deleted_at: IsNull() }, }, - relations: ["progresses", "progresses.game"], + relations: options.loadRelations + ? ["progresses", "progresses.game"] + : [], withDeleted: true, }) .catch(() => { @@ -145,7 +151,7 @@ export class UsersService implements OnApplicationBootstrap { * * @returns - Overview of all users */ - public async getUsers( + public async getAll( includeDeleted = false, includeDeactivated = false, ): Promise { @@ -167,7 +173,7 @@ export class UsersService implements OnApplicationBootstrap { * already exists */ public async register(dto: RegisterUserDto): Promise { - await this.throwIfUserAlreadyExists(dto.username, dto.email); + await this.throwIfAlreadyExists(dto.username, dto.email); const user = new GamevaultUser(); user.username = dto.username; user.password = hashSync(dto.password, 10); @@ -248,18 +254,18 @@ export class UsersService implements OnApplicationBootstrap { admin = false, executorUsername?: string, ): Promise { - const user = await this.getUserByIdOrFail(id); + const user = await this.findByIdOrFail(id); if (dto.username != null && dto.username !== user.username) { if (dto.username.toLowerCase() !== user.username.toLowerCase()) { - await this.throwIfUserAlreadyExists(dto.username, undefined); + await this.throwIfAlreadyExists(dto.username, undefined); } user.username = dto.username; } if (dto.email != null && dto.email !== user.email) { if (dto.email.toLowerCase() !== user.email.toLowerCase()) { - await this.throwIfUserAlreadyExists(undefined, dto.email); + await this.throwIfAlreadyExists(undefined, dto.email); } user.email = dto.email; } @@ -277,7 +283,7 @@ export class UsersService implements OnApplicationBootstrap { } if (dto.profile_picture_url != null) { - user.profile_picture = await this.imagesService.downloadImageByUrl( + user.profile_picture = await this.imagesService.downloadByUrl( dto.profile_picture_url, executorUsername, ); @@ -290,7 +296,7 @@ export class UsersService implements OnApplicationBootstrap { } if (dto.background_image_url != null) { - user.background_image = await this.imagesService.downloadImageByUrl( + user.background_image = await this.imagesService.downloadByUrl( dto.background_image_url, executorUsername, ); @@ -319,7 +325,7 @@ export class UsersService implements OnApplicationBootstrap { * @param id - The ID of the user to delete. */ public async delete(id: number): Promise { - const user = await this.getUserByIdOrFail(id); + const user = await this.findByIdOrFail(id); return this.userRepository.softRemove(user); } @@ -329,41 +335,17 @@ export class UsersService implements OnApplicationBootstrap { * @param id - The ID of the user to recover. */ public async recover(id: number): Promise { - const user = await this.getUserByIdOrFail(id, true); + const user = await this.findByIdOrFail(id); return this.userRepository.recover(user); } - /** - * Set profile picture of a user - * - * @deprecated - * @param id - The ID of the user whose profile picture to set - * @param url - The URL of the new profile picture - * @returns - The updated user object - * @throws {NotFoundException} - If the user with specified ID is not found - */ - public async setProfilePicture( - id: number, - url: string, - ): Promise { - const user = await this.getUserByIdOrFail(id); - user.profile_picture = await this.imagesService.downloadImageByUrl(url); - return await this.userRepository.save(user); - } - - /** - * Set profile art of a user - * - * @deprecated - * @param id - The ID of the user whose profile art to set - * @param url - The URL of the new profile art - * @returns - The updated user object - * @throws {NotFoundException} - If the user with specified ID is not found - */ - public async setProfileArt(id: number, url: string): Promise { - const user = await this.getUserByIdOrFail(id); - user.background_image = await this.imagesService.downloadImageByUrl(url); - return await this.userRepository.save(user); + public async checkIfUsernameIsAtLeastRole(username: string, role: Role) { + try { + const user = await this.findByUsernameOrFail(username); + return user.role >= role; + } catch { + return false; + } } /** @@ -388,7 +370,7 @@ export class UsersService implements OnApplicationBootstrap { if (!username) { throw new UnauthorizedException("No Authorization provided"); } - const user = await this.getUserByIdOrFail(userId); + const user = await this.findByIdOrFail(userId); if (user.role === Role.ADMIN) { return true; } @@ -405,7 +387,7 @@ export class UsersService implements OnApplicationBootstrap { return true; } - private async throwIfUserAlreadyExists( + private async throwIfAlreadyExists( username: string | undefined, email: string | undefined, ) { diff --git a/tsconfig.json b/tsconfig.json index a8276de..ae146a8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,6 +21,6 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, - "resolveJsonModule": true, + "resolveJsonModule": true } }