From 80e2eeee73c1ae29ebdcbbf13b19ba8931ff412b Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 1 Apr 2026 10:31:50 +0400 Subject: [PATCH 01/10] build: remove unused dependencies --- package-lock.json | 44 ++++++++++++++++++-------------------------- package.json | 3 --- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/package-lock.json b/package-lock.json index ba9d8872..15433820 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,6 @@ "hbs": "^4.2.0", "keyv": "^5.5.3", "moment": "^2.30.1", - "mysql2": "^3.15.2", "nestjs-typeorm-paginate": "^4.1.0", "pg": "^8.12.0", "reflect-metadata": "^0.2.2", @@ -65,10 +64,8 @@ "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", "prettier": "^3.0.0", - "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", - "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" @@ -5244,6 +5241,8 @@ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 6.0.0" } @@ -8265,6 +8264,8 @@ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "is-property": "^1.0.2" } @@ -9008,7 +9009,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/is-stream": { "version": "2.0.1", @@ -10227,6 +10230,8 @@ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "bun": ">=1.0.0", "deno": ">=1.30.0", @@ -10645,6 +10650,8 @@ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.4.tgz", "integrity": "sha512-RnfuK5tyIuaiPMWOCTTl4vQX/mQXqSA8eoIbwvWccadvPGvh+BYWWVecInMS5s7wcLUkze8LqJzwB/+A4uwuAA==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", @@ -10663,13 +10670,17 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" + "license": "Apache-2.0", + "optional": true, + "peer": true }, "node_modules/named-placeholders": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "lru.min": "^1.1.0" }, @@ -12353,6 +12364,8 @@ "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", "license": "MIT", + "optional": true, + "peer": true, "engines": { "bun": ">=1.0.0", "deno": ">=2.0.0", @@ -13137,27 +13150,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ts-loader": { - "version": "9.5.4", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", - "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "enhanced-resolve": "^5.0.0", - "micromatch": "^4.0.0", - "semver": "^7.3.4", - "source-map": "^0.7.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "typescript": "*", - "webpack": "^5.0.0" - } - }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", diff --git a/package.json b/package.json index a194afa0..e0a64b05 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "hbs": "^4.2.0", "keyv": "^5.5.3", "moment": "^2.30.1", - "mysql2": "^3.15.2", "nestjs-typeorm-paginate": "^4.1.0", "pg": "^8.12.0", "reflect-metadata": "^0.2.2", @@ -77,10 +76,8 @@ "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", "prettier": "^3.0.0", - "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", - "ts-loader": "^9.4.3", "ts-node": "^10.9.1", "tsconfig-paths": "^4.2.0", "typescript": "^5.1.3" From f21a7d93e4f81f2e4bae9d82bb4b386d50063c50 Mon Sep 17 00:00:00 2001 From: Nikita Date: Wed, 1 Apr 2026 10:36:38 +0400 Subject: [PATCH 02/10] build: run npm update --- package-lock.json | 1105 +++++++++++++++++---------------------------- 1 file changed, 426 insertions(+), 679 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15433820..ddbd65b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,9 +85,9 @@ } }, "node_modules/@aeternity/aepp-sdk": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@aeternity/aepp-sdk/-/aepp-sdk-14.1.0.tgz", - "integrity": "sha512-6WxqJDuhXAESWbv9Eg7j1xqP+sd2dQSnVQSRDh4uPlruTfQYfHkjAqk2fGu2cyw1SLbK3sNgmUK/hZJCusVpJw==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/@aeternity/aepp-sdk/-/aepp-sdk-14.1.1.tgz", + "integrity": "sha512-aZG2sUghldNqacJEvAFqggVJ9Plph0gcFKPAd8wneKI3Q4Ig10J4BxTcHCzNnBq2xsz9kCj7d4lIB7NhxBmBTg==", "license": "ISC", "dependencies": { "@aeternity/aepp-calldata": "^1.9.1", @@ -155,13 +155,13 @@ } }, "node_modules/@angular-devkit/core": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.19.tgz", - "integrity": "sha512-JbLL+4IMLMBgjLZlnPG4lYDfz4zGrJ/s6Aoon321NJKuw1Kb1k5KpFu9dUY0BqLIe8xPQ2UJBpI+xXdK5MXMHQ==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.22.tgz", + "integrity": "sha512-OqN/Ded+ZKypPZN5+qUFwtnKGl7FKpxJXYO2Vts5vLBojY5goCZd9SGW1CyXeuPnisRUW+vjqBQbWYuEUh36Tw==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", @@ -193,13 +193,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.19.tgz", - "integrity": "sha512-J4Jarr0SohdrHcb40gTL4wGPCQ952IMWF1G/MSAQfBAPvA9ZKApYhpxcY7PmehVePve+ujpus1dGsJ7dPxz8Kg==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.22.tgz", + "integrity": "sha512-tvfu5jhem1o8qidVxvXe5KfCij65ioMLCOFA947DD+zb3yTl5pJyDm2dqzbOehuQw0fmH4XPQukRJsCUy+UwaA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", + "@angular-devkit/core": "19.2.22", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -212,14 +212,14 @@ } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "19.2.19", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.19.tgz", - "integrity": "sha512-7q9UY6HK6sccL9F3cqGRUwKhM7b/XfD2YcVaZ2WD7VMaRlRm85v6mRjSrfKIAwxcQU0UK27kMc79NIIqaHjzxA==", + "version": "19.2.22", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.2.22.tgz", + "integrity": "sha512-6BvkxDz4nV8B6Ha4n/pYZ503vXgLxMaEpcKsFDao1sl0iSwrIOphlIS1yWprlGdCThIM3aJref1JU13ZvEcBCA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", + "@angular-devkit/core": "19.2.22", + "@angular-devkit/schematics": "19.2.22", "@inquirer/prompts": "7.3.2", "ansi-colors": "4.1.3", "symbol-observable": "4.0.0", @@ -311,9 +311,9 @@ } }, "node_modules/@apollo/server": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@apollo/server/-/server-5.4.0.tgz", - "integrity": "sha512-E0/2C5Rqp7bWCjaDh4NzYuEPDZ+dltTf2c0FI6GCKJA6GBetVferX3h1//1rS4+NxD36wrJsGGJK+xyT/M3ysg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@apollo/server/-/server-5.5.0.tgz", + "integrity": "sha512-vWtodBOK/SZwBTJzItECOmLfL8E8pn/IdvP7pnxN5g2tny9iW4+9sxdajE798wV1H2+PYp/rRcl/soSHIBKMPw==", "license": "MIT", "peer": true, "dependencies": { @@ -612,9 +612,9 @@ } }, "node_modules/@azure/core-rest-pipeline": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.22.2.tgz", - "integrity": "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==", + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.1.2", @@ -622,7 +622,7 @@ "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", - "@typespec/ts-http-runtime": "^0.3.0", + "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" }, "engines": { @@ -871,23 +871,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { @@ -1140,9 +1140,9 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.0.tgz", - "integrity": "sha512-TgUkdp71C9pIbBcHudc+gXZnihEDOjUAmXO1VO4HHGES7QLZcShR0stfKIxLSNIYx2fqhmJChOjm/wkF8wv4gA==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", + "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==", "license": "MIT", "dependencies": { "core-js-pure": "^3.48.0" @@ -1207,9 +1207,9 @@ "license": "MIT" }, "node_modules/@borewit/text-codec": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", - "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", "license": "MIT", "funding": { "type": "github", @@ -1217,45 +1217,45 @@ } }, "node_modules/@bull-board/api": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-6.19.0.tgz", - "integrity": "sha512-qOE9ho99W+MaYKTx8Od7nOIR/y6ySwD2QxR4Cx81EhZHSUifPhjTzHop3paQjxPnrAm/y8kcqebmmhKWEgjZGA==", + "version": "6.20.6", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-6.20.6.tgz", + "integrity": "sha512-B/9OlKWJ/He421Lyuc7htMpKN4R8hcNu5uoIRiyh9y9J0kMOfxfs82GKXdfR9R0595LvAcCOSvjn8EtWHXesTA==", "license": "MIT", "dependencies": { "redis-info": "^3.1.0" }, "peerDependencies": { - "@bull-board/ui": "6.19.0" + "@bull-board/ui": "6.20.6" } }, "node_modules/@bull-board/express": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-6.19.0.tgz", - "integrity": "sha512-YxSKMKlOsT+q4dJi9uKWNms85O8ksFzEiY60ejGaeuTyL4wqbIJDg9He8IApLoFS4tGFzUEyAnjaNoxjY4dh5w==", + "version": "6.20.6", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-6.20.6.tgz", + "integrity": "sha512-QBOjhi3jR4qIJvLCo++DdL0CyG4umiFXBCPJYr74DmRN41m27rDS3NSf7ceHdbQr9Ol8mw3xbVqGLn50x3QdWQ==", "license": "MIT", "dependencies": { - "@bull-board/api": "6.19.0", - "@bull-board/ui": "6.19.0", + "@bull-board/api": "6.20.6", + "@bull-board/ui": "6.20.6", "ejs": "^3.1.10", "express": "^5.2.1" } }, "node_modules/@bull-board/ui": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-6.19.0.tgz", - "integrity": "sha512-9sD0FzubAH2Kv7TGtQqulPzceOaOkTzVhOvktSEvZSWIWaf7kRSAP8+NQU7QfR07qgTeLlWBX7RstitmZJkf4Q==", + "version": "6.20.6", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-6.20.6.tgz", + "integrity": "sha512-nEXRgMHHd48ssINgvDqT7b4p/+57XRf3CUcTGQ7UIJb4F7n/kRRj8+ZQvQmMFsYGdNmbNU2eNheQ05vXSYiqdQ==", "license": "MIT", "dependencies": { - "@bull-board/api": "6.19.0" + "@bull-board/api": "6.20.6" } }, "node_modules/@cacheable/utils": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.4.tgz", - "integrity": "sha512-knwKUJEYgIfwShABS1BX6JyJJTglAFcEU7EXqzTdiGCXur4voqkiJkdgZIQtWNFhynzDWERcTYv/sETMu3uJWA==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", "license": "MIT", "dependencies": { - "hashery": "^1.3.0", + "hashery": "^1.5.1", "keyv": "^5.6.0" } }, @@ -1348,9 +1348,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1365,9 +1365,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1383,9 +1383,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1533,9 +1533,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -1544,9 +1544,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -1929,9 +1929,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", - "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -1999,12 +1999,12 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -2345,9 +2345,9 @@ } }, "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2378,9 +2378,9 @@ } }, "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2563,39 +2563,39 @@ "license": "MIT" }, "node_modules/@ledgerhq/devices": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.10.0.tgz", - "integrity": "sha512-ytT66KI8MizFX6dGJKthOzPDw5uNRmmg+RaMta62jbFePKYqfXtYTp6Wc0ErTXaL8nFS3IujHENwKthPmsj6jw==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/devices/-/devices-8.13.0.tgz", + "integrity": "sha512-hgGn1kpe/rT0EJ0Qs7rG+1TXA4g6HN2t3dB4DndRTqVqC9aSSbME+ajA0QWLZisxOD3zkwvO4Q0mJ2zARAKyag==", "license": "Apache-2.0", "dependencies": { - "@ledgerhq/errors": "^6.29.0", - "@ledgerhq/logs": "^6.14.0", + "@ledgerhq/errors": "^6.32.0", + "@ledgerhq/logs": "^6.16.0", "rxjs": "7.8.2", "semver": "7.7.3" } }, "node_modules/@ledgerhq/errors": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.29.0.tgz", - "integrity": "sha512-mmDsGN662zd0XGKyjzSKkg+5o1/l9pvV1HkVHtbzaydvHAtRypghmVoWMY9XAQDLXiUBXGIsLal84NgmGeuKWA==", + "version": "6.32.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/errors/-/errors-6.32.0.tgz", + "integrity": "sha512-BjjvhLM6UXYUbhllqAduo9PSneLt9FXZ3TBEUFQ3MMSZOCHt0gAgDySLwul99R8fdYWkXBza4DYQjUNckpN2lg==", "license": "Apache-2.0" }, "node_modules/@ledgerhq/hw-transport": { - "version": "6.32.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.32.0.tgz", - "integrity": "sha512-bf2nxzDQ21DV/bsmExfWI0tatoVeoqhu/ePWalD/nPgPnTn/ZIDq7VBU+TY5p0JZaE87NQwmRUAjm6C1Exe61A==", + "version": "6.34.1", + "resolved": "https://registry.npmjs.org/@ledgerhq/hw-transport/-/hw-transport-6.34.1.tgz", + "integrity": "sha512-Bg9Qk2vtm0m0cZn9prZV2Hbvh3b42KBh4uomO00derh+eiwsdg5AXBBptAJiREkew1RVtETRdWxrKchUJfeWvA==", "license": "Apache-2.0", "dependencies": { - "@ledgerhq/devices": "8.10.0", - "@ledgerhq/errors": "^6.29.0", - "@ledgerhq/logs": "^6.14.0", + "@ledgerhq/devices": "8.13.0", + "@ledgerhq/errors": "^6.32.0", + "@ledgerhq/logs": "^6.16.0", "events": "^3.3.0" } }, "node_modules/@ledgerhq/logs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.14.0.tgz", - "integrity": "sha512-kJFu1+asWQmU9XlfR1RM3lYR76wuEoPyZvkI/CNjpft78BQr3+MMf3Nu77ABzcKFnhIcmAkOLlDQ6B8L6hDXHA==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/@ledgerhq/logs/-/logs-6.16.0.tgz", + "integrity": "sha512-v/PLfb1dq1En35kkpbfRWp8jLYgbPUXxGhmd4pmvPSIe0nRGkNTomsZASmWQAv6pRonVGqHIBVlte7j1MBbOww==", "license": "Apache-2.0" }, "node_modules/@lukeed/csprng": { @@ -2677,9 +2677,9 @@ } }, "node_modules/@metamask/json-rpc-engine": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-10.2.2.tgz", - "integrity": "sha512-F8t3QDACD7Ll6ZdPIsIm/e62A7AXRGTpV1GPqnOZZVWyeUalDkX2Pnv9MaWKKwoquGh8xv/K4g/c3ryuYVBfMA==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@metamask/json-rpc-engine/-/json-rpc-engine-10.2.4.tgz", + "integrity": "sha512-voOnrTtkTwF2zcyNx2auFvZY9QTK7dADsE9xaCXO9tGpvfTEI3GIVyUryYay8t/lUiz5nhHGublTkhwntgvRHQ==", "license": "ISC", "dependencies": { "@metamask/rpc-errors": "^7.0.2", @@ -2974,15 +2974,15 @@ } }, "node_modules/@nestjs/cli": { - "version": "11.0.16", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.16.tgz", - "integrity": "sha512-P0H+Vcjki6P5160E5QnMt3Q0X5FTg4PZkP99Ig4lm/4JWqfw32j3EXv3YBTJ2DmxLwOQ/IS9F7dzKpMAgzKTGg==", + "version": "11.0.17", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.17.tgz", + "integrity": "sha512-tOMgoB9k+Zb2WdKYPhbhceROLcDR1BFQZWfkBOGMRgBTo8rnC125E65UvThEA77vp4w+zKjqiSIv0leT+wdpHg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.19", - "@angular-devkit/schematics": "19.2.19", - "@angular-devkit/schematics-cli": "19.2.19", + "@angular-devkit/core": "19.2.22", + "@angular-devkit/schematics": "19.2.22", + "@angular-devkit/schematics-cli": "19.2.22", "@inquirer/prompts": "7.10.1", "@nestjs/schematics": "^11.0.1", "ansis": "4.2.0", @@ -2990,13 +2990,13 @@ "cli-table3": "0.6.5", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "9.1.0", - "glob": "13.0.0", + "glob": "13.0.6", "node-emoji": "1.11.0", "ora": "5.4.1", "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.2.0", "typescript": "5.9.3", - "webpack": "5.104.1", + "webpack": "5.105.4", "webpack-node-externals": "3.0.0" }, "bin": { @@ -3018,147 +3018,13 @@ } } }, - "node_modules/@nestjs/cli/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/@nestjs/cli/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@nestjs/cli/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/@nestjs/cli/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@nestjs/cli/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@nestjs/cli/node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@nestjs/cli/node_modules/webpack": { - "version": "5.104.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", - "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, "node_modules/@nestjs/common": { - "version": "11.1.14", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.14.tgz", - "integrity": "sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==", + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.17.tgz", + "integrity": "sha512-hLODw5Abp8OQgA+mUO4tHou4krKgDtUcM9j5Ihxncst9XeyxYBTt2bwZm4e4EQr5E352S4Fyy6V3iFx9ggxKAg==", "license": "MIT", "dependencies": { - "file-type": "21.3.0", + "file-type": "21.3.2", "iterare": "1.2.1", "load-esm": "1.0.3", "tslib": "2.8.1", @@ -3199,9 +3065,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.1.14", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.14.tgz", - "integrity": "sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==", + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.17.tgz", + "integrity": "sha512-lD5mAYekTTurF3vDaa8C2OKPnjiz4tsfxIc5XlcSUzOhkwWf6Ay3HKvt6FmvuWQam6uIIHX52Clg+e6tAvf/cg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -3297,6 +3163,27 @@ } } }, + "node_modules/@nestjs/graphql/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@nestjs/mapped-types": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", @@ -3318,14 +3205,14 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.14", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.14.tgz", - "integrity": "sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==", + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.17.tgz", + "integrity": "sha512-mAf4eOsSBsTOn/VbrUO1gsjW6dVh91qqXPMXun4dN8SnNjf7PTQagM9o8d6ab8ZBpNe6UdZftdrZoDetU+n4Qg==", "license": "MIT", "dependencies": { "cors": "2.8.6", "express": "5.2.1", - "multer": "2.0.2", + "multer": "2.1.1", "path-to-regexp": "8.3.0", "tslib": "2.8.1" }, @@ -3339,9 +3226,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "11.1.14", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.14.tgz", - "integrity": "sha512-LLSIWkYz4FcvUhfepillYQboo9qbjq1YtQj8XC3zyex+EaqNXvxhZntx/1uJhAjc655pJts9HfZwWXei8jrRGw==", + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.1.17.tgz", + "integrity": "sha512-BSOAsENdmTtsnDL0hb4takbWzPy9WoPybjlM57ab3/rQgm0biMFYUupH2uzmCjmmIXJL/EFbAWznVl8xw2Sa6Q==", "license": "MIT", "dependencies": { "socket.io": "4.8.3", @@ -3371,15 +3258,15 @@ } }, "node_modules/@nestjs/schematics": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.9.tgz", - "integrity": "sha512-0NfPbPlEaGwIT8/TCThxLzrlz3yzDNkfRNpbL7FiplKq3w4qXpJg0JYwqgMEJnLQZm3L/L/5XjoyfJHUO3qX9g==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.10.tgz", + "integrity": "sha512-q9lr0wGwgBHLarD4uno3XiW4JX60WPlg2VTgbqPHl/6bT4u1IEEzj+q9Tad3bVnqL5mlDF3vrZ2tj+x13CJpmw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.17", - "@angular-devkit/schematics": "19.2.17", - "comment-json": "4.4.1", + "@angular-devkit/core": "19.2.23", + "@angular-devkit/schematics": "19.2.23", + "comment-json": "4.6.2", "jsonc-parser": "3.3.1", "pluralize": "8.0.0" }, @@ -3388,16 +3275,16 @@ } }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.17.tgz", - "integrity": "sha512-Ah008x2RJkd0F+NLKqIpA34/vUGwjlprRCkvddjDopAWRzYn6xCkz1Tqwuhn0nR1Dy47wTLKYD999TYl5ONOAQ==", + "version": "19.2.23", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.2.23.tgz", + "integrity": "sha512-RazHPQkUEsNU/OZ75w9UeHxGFMthRiuAW2B/uA7eXExBj/1meHrrBfoCA56ujW2GUxVjRtSrMjylKh4R4meiYA==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "8.17.1", + "ajv": "8.18.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", + "picomatch": "4.0.4", "rxjs": "7.8.1", "source-map": "0.7.4" }, @@ -3416,13 +3303,13 @@ } }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.17.tgz", - "integrity": "sha512-ADfbaBsrG8mBF6Mfs+crKA/2ykB8AJI50Cv9tKmZfwcUcyAdmTr+vVvhsBCfvUAEokigSsgqgpYxfkJVxhJYeg==", + "version": "19.2.23", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.2.23.tgz", + "integrity": "sha512-Jzs7YM4X6azmHU7Mw5tQSPMuvaqYS8SLnZOJbtiXCy1JyuW9bm/WBBecNHMiuZ8LHXKhvQ6AVX1tKrzF6uiDmw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.2.17", + "@angular-devkit/core": "19.2.23", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -3434,6 +3321,19 @@ "yarn": ">= 1.13.0" } }, + "node_modules/@nestjs/schematics/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@nestjs/schematics/node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -3478,9 +3378,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.1.14", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.14.tgz", - "integrity": "sha512-cQxX0ronsTbpfHz8/LYOVWXxoTxv6VoxrnuZoQaVX7QV2PSMqxWE7/9jSQR0GcqAFUEmFP34c6EJqfkjfX/k4Q==", + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.17.tgz", + "integrity": "sha512-lNffw+z+2USewmw4W0tsK+Rq94A2N4PiHbcqoRUu5y8fnqxQeIWGHhjo5BFCqj7eivqJBhT7WdRydxVq4rAHzg==", "dev": true, "license": "MIT", "dependencies": { @@ -3519,9 +3419,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.1.14", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.14.tgz", - "integrity": "sha512-fVP6RmmrmtLIitTXN9er7BUOIjjxcdIewN/zUtBlwgfng+qKBTxpNFOs3AXXbCu8bQr2xjzhjrBTfqri0Ske7w==", + "version": "11.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.1.17.tgz", + "integrity": "sha512-YbwQ0QfVj0lxkKQhdIIgk14ZSVWDqGk1J8nNSN6SLjf36sVv58Ma5ro+dtQua8wj3l2Ub7JJCVFixEhKtYc/rQ==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -3996,9 +3896,9 @@ } }, "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", "license": "MIT", "dependencies": { "@types/ms": "*" @@ -4133,9 +4033,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", "license": "MIT" }, "node_modules/@types/long": { @@ -4174,9 +4074,9 @@ } }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", "dev": true, "license": "MIT" }, @@ -4516,9 +4416,9 @@ } }, "node_modules/@typespec/ts-http-runtime": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.3.tgz", - "integrity": "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz", + "integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==", "license": "MIT", "dependencies": { "http-proxy-agent": "^7.0.0", @@ -4965,9 +4865,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -5099,9 +4999,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -5236,17 +5136,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -5421,9 +5310,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.13.tgz", + "integrity": "sha512-BL2sTuHOdy0YT1lYieUxTw/QMtPBC3pmlJC6xk8BBYVv6vcw3SGdKemQ+Xsx9ik2F/lYDO9tqsFQH1r9PFuHKw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5838,9 +5727,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5859,9 +5748,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -5879,11 +5768,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -6101,9 +5990,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001770", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", - "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "version": "1.0.30001784", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001784.tgz", + "integrity": "sha512-WU346nBTklUV9YfUl60fqRbU5ZqyXlqvo1SgigE1OAXK5bFL8LL9q1K7aap3N739l4BvNqnkm3YrGHiY9sfUQw==", "dev": true, "funding": [ { @@ -6228,14 +6117,14 @@ "license": "MIT" }, "node_modules/class-validator": { - "version": "0.14.3", - "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", - "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.4.tgz", + "integrity": "sha512-AwNusCCam51q703dW82x95tOqQp6oC9HNUl724KxJJOfnKscI8dOloXFgyez7LbTTKWuRBA37FScqVbJEoq8Yw==", "license": "MIT", "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", - "validator": "^13.15.20" + "validator": "^13.15.22" } }, "node_modules/cli-cursor": { @@ -6409,14 +6298,13 @@ } }, "node_modules/comment-json": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", - "integrity": "sha512-r1To31BQD5060QdkC+Iheai7gHwoSZobzunqkf2/kQ6xIAfJyrKNAFUwdKvkK7Qgu7pVTKQEa7ok7Ed3ycAJgg==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", + "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", "dev": true, "license": "MIT", "dependencies": { "array-timsort": "^1.0.3", - "core-util-is": "^1.0.3", "esprima": "^4.0.1" }, "engines": { @@ -6524,9 +6412,9 @@ "license": "MIT" }, "node_modules/core-js-pure": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.48.0.tgz", - "integrity": "sha512-1slJgk89tWC51HQ1AEqG+s2VuwpTRr8ocu4n20QUcH1v9lAN0RXen0Q0AABa/DK1I7RrNWLucplOHMx8hfTGTw==", + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -6534,13 +6422,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -6701,9 +6582,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, "node_modules/debug": { @@ -6724,9 +6605,9 @@ } }, "node_modules/dedent": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", - "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", + "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -6996,9 +6877,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.330", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.330.tgz", + "integrity": "sha512-jFNydB5kFtYUobh4IkWUnXeyDbjf/r9gcUEXe1xcrcUxIGfTdzPXA+ld6zBRbwvgIGVzDll/LTIiDztEtckSnA==", "dev": true, "license": "ISC" }, @@ -7031,13 +6912,14 @@ } }, "node_modules/engine.io": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", - "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.6.tgz", + "integrity": "sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==", "license": "MIT", "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", @@ -7124,9 +7006,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, "license": "MIT", "dependencies": { @@ -7400,9 +7282,9 @@ } }, "node_modules/eslint/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -7417,9 +7299,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -7435,9 +7317,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7861,9 +7743,9 @@ } }, "node_modules/file-type": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz", - "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==", + "version": "21.3.2", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.2.tgz", + "integrity": "sha512-DLkUvGwep3poOV2wpzbHCOnSKGk1LzyXTv+aHFgN2VFl96wnp8YA9YjO2qPzg5PuL8q/SW9Pdi6WTkYOIh995w==", "license": "MIT", "dependencies": { "@tokenizer/inflate": "^0.4.1", @@ -7879,18 +7761,18 @@ } }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -7975,9 +7857,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -8047,9 +7929,9 @@ } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -8058,9 +7940,9 @@ } }, "node_modules/fork-ts-checker-webpack-plugin/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -8259,17 +8141,6 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -8362,18 +8233,18 @@ } }, "node_modules/glob": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.0.tgz", - "integrity": "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -8400,36 +8271,36 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", - "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -8502,9 +8373,9 @@ "license": "MIT" }, "node_modules/graphql": { - "version": "16.12.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz", - "integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==", + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", "license": "MIT", "engines": { "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" @@ -8646,12 +8517,12 @@ "license": "ISC" }, "node_modules/hashery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.0.tgz", - "integrity": "sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", "license": "MIT", "dependencies": { - "hookified": "^1.14.0" + "hookified": "^1.15.0" }, "engines": { "node": ">=20" @@ -8863,12 +8734,12 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", - "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", "dependencies": { - "@ioredis/commands": "1.5.0", + "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -9005,14 +8876,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -9363,9 +9226,9 @@ } }, "node_modules/jest-config/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9396,9 +9259,9 @@ } }, "node_modules/jest-config/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -9670,27 +9533,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runner/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-runner/node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/jest-runtime": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", @@ -9726,9 +9568,9 @@ } }, "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -9759,9 +9601,9 @@ } }, "node_modules/jest-runtime/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -9822,9 +9664,9 @@ } }, "node_modules/jest-util/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10071,9 +9913,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.37", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.37.tgz", - "integrity": "sha512-rDU6bkpuMs8YRt/UpkuYEAsYSoNuDEbrE41I3KNvmXREGH6DGBJ8Wbak4by29wNOQ27zk4g4HL82zf0OGhwRuw==", + "version": "1.12.41", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.41.tgz", + "integrity": "sha512-lsmMmGXBxXIK/VMLEj0kL6MtUs1kBGj1nTCzi6zgQoG1DEwqwt2DQyHxcLykceIxAnfE3hya7NuIh6PpC6S3fA==", "license": "MIT" }, "node_modules/lines-and-columns": { @@ -10217,31 +10059,14 @@ "peer": true }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -10392,9 +10217,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "license": "MIT", "engines": { "node": ">=8.6" @@ -10517,15 +10342,15 @@ "license": "ISC" }, "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, "bin": { "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/moment": { @@ -10544,9 +10369,9 @@ "license": "MIT" }, "node_modules/msgpackr": { - "version": "1.11.8", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.8.tgz", - "integrity": "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==", + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz", + "integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==", "license": "MIT", "optionalDependencies": { "msgpackr-extract": "^3.0.2" @@ -10575,21 +10400,22 @@ } }, "node_modules/multer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", - "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", "license": "MIT", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", "concat-stream": "^2.0.0", - "mkdirp": "^0.5.6", - "object-assign": "^4.1.1", - "type-is": "^1.6.18", - "xtend": "^4.0.2" + "type-is": "^1.6.18" }, "engines": { "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/multer/node_modules/media-typer": { @@ -10645,49 +10471,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/mysql2": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.17.4.tgz", - "integrity": "sha512-RnfuK5tyIuaiPMWOCTTl4vQX/mQXqSA8eoIbwvWccadvPGvh+BYWWVecInMS5s7wcLUkze8LqJzwB/+A4uwuAA==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "aws-ssl-profiles": "^1.1.2", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.2", - "long": "^5.3.2", - "lru.min": "^1.1.4", - "named-placeholders": "^1.1.6", - "sql-escaper": "^1.3.3" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/mysql2/node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0", - "optional": true, - "peer": true - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -10803,9 +10586,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -11130,14 +10913,14 @@ } }, "node_modules/pg": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", - "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", "dependencies": { - "pg-connection-string": "^2.11.0", - "pg-pool": "^3.11.0", - "pg-protocol": "^1.11.0", + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -11164,9 +10947,9 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", - "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", "license": "MIT" }, "node_modules/pg-int8": { @@ -11179,18 +10962,18 @@ } }, "node_modules/pg-pool": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", - "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", - "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", "license": "MIT" }, "node_modules/pg-types": { @@ -11547,16 +11330,6 @@ ], "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -11792,9 +11565,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -11823,9 +11596,9 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -11937,9 +11710,9 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -12008,16 +11781,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -12257,9 +12020,9 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", - "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -12323,9 +12086,9 @@ } }, "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "license": "MIT", "dependencies": { @@ -12359,23 +12122,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sql-escaper": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", - "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "bun": ">=1.0.0", - "deno": ">=2.0.0", - "node": ">=12.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" - } - }, "node_modules/sql-highlight": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", @@ -12549,9 +12295,9 @@ } }, "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", "license": "MIT", "dependencies": { "@tokenizer/token": "^0.3.0" @@ -12717,9 +12463,9 @@ } }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, "license": "MIT", "engines": { @@ -12757,18 +12503,6 @@ "node": ">=8" } }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -12776,9 +12510,9 @@ "license": "ISC" }, "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -12795,16 +12529,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "engines": { @@ -12905,6 +12638,27 @@ "dev": true, "license": "MIT" }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -12921,9 +12675,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -12954,9 +12708,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -13106,9 +12860,9 @@ } }, "node_modules/ts-jest/node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -13483,12 +13237,12 @@ "license": "ISC" }, "node_modules/typeorm/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -13767,12 +13521,11 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.105.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.2.tgz", - "integrity": "sha512-dRXm0a2qcHPUBEzVk8uph0xWSjV/xZxenQQbLwnwP7caQCYpqG1qddwlyEkIDkYn0K8tvmcrZ+bOrzoQ3HxCDw==", + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -13780,11 +13533,11 @@ "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.19.0", + "enhanced-resolve": "^5.20.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -13796,9 +13549,9 @@ "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", + "terser-webpack-plugin": "^5.3.17", "watchpack": "^2.5.1", - "webpack-sources": "^3.3.3" + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -13842,7 +13595,6 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -13861,7 +13613,6 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -13876,7 +13627,6 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -13887,7 +13637,6 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -13898,7 +13647,6 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -13912,7 +13660,6 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -14101,9 +13848,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" From c70b6b43b14f842f4c3e5d42434b38cd47ed474c Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Tue, 31 Mar 2026 12:36:34 +0200 Subject: [PATCH 03/10] feat(accounts): implement getPortfolioPnlChart endpoint for SVG sparkline visualization of PnL --- .../controllers/accounts.controller.spec.ts | 79 +++++++++++++++++++ .../controllers/accounts.controller.ts | 74 +++++++++++++++++ .../services/portfolio.service.spec.ts | 68 ++++++++++++++++ src/account/services/portfolio.service.ts | 72 +++++++++++++++++ .../controllers/historical.controller.ts | 52 +----------- src/utils/sparkline.util.ts | 61 ++++++++++++++ 6 files changed, 357 insertions(+), 49 deletions(-) create mode 100644 src/utils/sparkline.util.ts diff --git a/src/account/controllers/accounts.controller.spec.ts b/src/account/controllers/accounts.controller.spec.ts index e2775a0b..55a85a57 100644 --- a/src/account/controllers/accounts.controller.spec.ts +++ b/src/account/controllers/accounts.controller.spec.ts @@ -1,4 +1,5 @@ import { AccountsController } from './accounts.controller'; +import { StreamableFile } from '@nestjs/common'; import { paginate } from 'nestjs-typeorm-paginate'; import { NotFoundException } from '@nestjs/common'; @@ -28,6 +29,8 @@ describe('AccountsController', () => { }; let portfolioService: { resolveAccountAddress: jest.Mock; + getPortfolioHistory: jest.Mock; + getPnlTimeSeries: jest.Mock; }; let bclPnlService: { calculateTradingStats: jest.Mock; @@ -48,6 +51,8 @@ describe('AccountsController', () => { }; portfolioService = { resolveAccountAddress: jest.fn().mockImplementation((a) => a), + getPortfolioHistory: jest.fn().mockResolvedValue([]), + getPnlTimeSeries: jest.fn().mockResolvedValue([]), }; bclPnlService = { calculateTradingStats: jest.fn(), @@ -115,4 +120,78 @@ describe('AccountsController', () => { NotFoundException, ); }); + + describe('getPortfolioPnlChart', () => { + it('returns a StreamableFile with SVG content type', async () => { + portfolioService.getPnlTimeSeries.mockResolvedValue([ + { gain: { ae: 1, usd: 2 } }, + { gain: { ae: 3, usd: 6 } }, + { gain: { ae: 2, usd: 4 } }, + ]); + + const result = await controller.getPortfolioPnlChart('ak_test'); + + expect(result).toBeInstanceOf(StreamableFile); + }); + + it('calls getPnlTimeSeries with daily interval for a multi-day range', async () => { + await controller.getPortfolioPnlChart( + 'ak_test', + '2026-01-01T00:00:00Z', + '2026-01-31T00:00:00Z', + ); + + const callArgs = portfolioService.getPnlTimeSeries.mock.calls[0][1]; + expect(callArgs.interval).toBe(86400); + }); + + it('calls getPnlTimeSeries with hourly interval for a single-day range', async () => { + await controller.getPortfolioPnlChart( + 'ak_test', + '2026-01-01T00:00:00Z', + '2026-01-01T23:59:59Z', + ); + + const callArgs = portfolioService.getPnlTimeSeries.mock.calls[0][1]; + expect(callArgs.interval).toBe(3600); + }); + + it('passes raw address to getPnlTimeSeries (resolution is handled inside)', async () => { + await controller.getPortfolioPnlChart('myname.chain'); + + // The controller no longer calls resolveAccountAddress itself — + // getPnlTimeSeries handles it internally. + expect(portfolioService.getPnlTimeSeries).toHaveBeenCalledWith( + 'myname.chain', + expect.anything(), + ); + }); + + it('uses usd gain values when convertTo=usd', async () => { + // AE series goes down, USD series goes up — green stroke proves the + // usd field was read rather than ae. + portfolioService.getPnlTimeSeries.mockResolvedValue([ + { gain: { ae: 5, usd: 1 } }, + { gain: { ae: 1, usd: 9 } }, + ]); + + const result = await controller.getPortfolioPnlChart( + 'ak_test', + undefined, + undefined, + 'usd', + ); + + const svgBuffer = (result as StreamableFile).getStream().read() as Buffer; + expect(svgBuffer.toString()).toContain('#2EB88A'); + }); + + it('returns empty SVG when no data points available', async () => { + const result = await controller.getPortfolioPnlChart('ak_test'); + + const svgBuffer = (result as StreamableFile).getStream().read() as Buffer; + expect(svgBuffer.toString()).toContain(' { + const start = startDate ? moment(startDate) : moment().subtract(30, 'days'); + const end = endDate ? moment(endDate) : moment(); + + // Use hourly points for a single-day range, daily otherwise + const rangeHours = end.diff(start, 'hours', true); + const interval = rangeHours <= 24 ? 3600 : 86400; + + // Use the lightweight PnL-only series instead of the full portfolio history. + // This skips AE-node balance calls, block-height resolution, and cumulative + // PnL queries — only calculateDailyPnlBatch (one SQL query) runs. + // resolveAccountAddress is handled inside getPnlTimeSeries. + const points = await this.portfolioService.getPnlTimeSeries(address, { + startDate: start, + endDate: end, + interval, + }); + + const values = points.map((p) => p.gain[convertTo]); + + const svg = buildSparklineSvg( + values, + Number(width), + Number(height), + sparklineStroke(values), + background, + ); + + return new StreamableFile(Buffer.from(svg), { + type: 'image/svg+xml', + disposition: 'inline; filename="pnl-chart.svg"', + }); + } + // Portfolio stats endpoint - MUST come before :address route to avoid route conflict @ApiOperation({ operationId: 'getPortfolioStats' }) @ApiParam({ name: 'address', type: 'string', description: 'Account address' }) diff --git a/src/account/services/portfolio.service.spec.ts b/src/account/services/portfolio.service.spec.ts index fdafc1c3..cc25f7d2 100644 --- a/src/account/services/portfolio.service.spec.ts +++ b/src/account/services/portfolio.service.spec.ts @@ -380,4 +380,72 @@ describe('PortfolioService', () => { undefined, ); }); + + describe('getPnlTimeSeries', () => { + it('calls calculateDailyPnlBatch and maps gain values', async () => { + const { service, bclPnlService } = createService(); + + const ts0 = moment('2026-01-01T00:00:00Z'); + const ts1 = moment('2026-01-02T00:00:00Z'); + const ts2 = moment('2026-01-03T00:00:00Z'); + + const pnlMap = new Map([ + [ts1.valueOf(), { ...basePnlResult, totalGainAe: 5, totalGainUsd: 10 }], + [ts2.valueOf(), { ...basePnlResult, totalGainAe: 3, totalGainUsd: 6 }], + ]); + bclPnlService.calculateDailyPnlBatch.mockResolvedValue(pnlMap); + + const result = await service.getPnlTimeSeries('ak_test', { + startDate: ts0, + endDate: ts2, + interval: 86400, + }); + + expect(bclPnlService.calculateDailyPnlBatch).toHaveBeenCalledTimes(1); + // Should NOT call any balance or block-height resolution + expect(bclPnlService.calculateTokenPnlsBatch).not.toHaveBeenCalled(); + + // ts0 has no entry in map → gain 0 + expect(result[0].gain).toEqual({ ae: 0, usd: 0 }); + expect(result[1].gain).toEqual({ ae: 5, usd: 10 }); + expect(result[2].gain).toEqual({ ae: 3, usd: 6 }); + }); + + it('builds correct daily windows from timestamps', async () => { + const { service, bclPnlService } = createService(); + bclPnlService.calculateDailyPnlBatch.mockResolvedValue(new Map()); + + const start = moment('2026-01-01T00:00:00Z'); + const end = moment('2026-01-03T00:00:00Z'); + + await service.getPnlTimeSeries('ak_test', { + startDate: start, + endDate: end, + interval: 86400, + }); + + const [, windows]: [string, DailyPnlWindow[]] = + bclPnlService.calculateDailyPnlBatch.mock.calls[0]; + + // First window: zero-width (no sells can fall in an empty range) + expect(windows[0].dayStartTs).toBe(windows[0].snapshotTs); + // Second window: covers [day0, day1) + expect(windows[1].dayStartTs).toBe(windows[0].snapshotTs); + expect(windows[1].snapshotTs).toBe(windows[1].dayStartTs + 86400 * 1000); + }); + + it('returns empty array when start is after end', async () => { + const { service, bclPnlService } = createService(); + bclPnlService.calculateDailyPnlBatch.mockResolvedValue(new Map()); + + const result = await service.getPnlTimeSeries('ak_test', { + startDate: moment('2026-01-10T00:00:00Z'), + endDate: moment('2026-01-01T00:00:00Z'), + interval: 86400, + }); + + expect(result).toEqual([]); + expect(bclPnlService.calculateDailyPnlBatch).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/account/services/portfolio.service.ts b/src/account/services/portfolio.service.ts index bb77f47a..eb7f1578 100644 --- a/src/account/services/portfolio.service.ts +++ b/src/account/services/portfolio.service.ts @@ -91,6 +91,18 @@ export interface GetPortfolioHistoryOptions { useRangeBasedPnl?: boolean; // If true, calculate PNL for range between timestamps; if false, use all previous transactions } +export interface PnlDataPoint { + timestamp: Moment; + gain: { ae: number; usd: number }; +} + +export interface GetPnlTimeSeriesOptions { + startDate?: Moment; + endDate?: Moment; + /** Seconds between data points. Defaults to 86400 (daily). */ + interval?: number; +} + @Injectable() export class PortfolioService { private readonly logger = new Logger(PortfolioService.name); @@ -378,6 +390,66 @@ export class PortfolioService { return data; } + /** + * Lightweight PnL time-series for charting. + * + * Only runs `calculateDailyPnlBatch` (one SQL query). Skips block-height + * resolution, cumulative PnL, and AE-node balance calls — making it + * significantly faster than `getPortfolioHistory` for sparkline use-cases. + * + * Each data point contains the realized gain for its own isolated window + * [previousTimestamp, currentTimestamp), keyed by `created_at` on the + * transactions (same as the daily PnL calendar). + */ + async getPnlTimeSeries( + address: string, + options: GetPnlTimeSeriesOptions = {}, + ): Promise { + const { startDate, endDate, interval = 86400 } = options; + + const now = moment(); + const start = startDate ?? moment().subtract(30, 'days'); + const end = endDate ? moment(endDate) : now; + const cappedEnd = moment.min(end, now); + const safeInterval = interval > 0 ? interval : 86400; + + const timestamps: Moment[] = []; + const current = moment(start); + const endMs = cappedEnd.valueOf(); + while (current.valueOf() <= endMs && timestamps.length < 10000) { + if (current.valueOf() > now.valueOf()) break; + timestamps.push(moment(current)); + const prev = current.valueOf(); + current.add(safeInterval, 'seconds'); + if (current.valueOf() <= prev) break; + } + + if (timestamps.length === 0) return []; + + const resolvedAddress = await this.resolveAccountAddress(address); + + const windows: DailyPnlWindow[] = timestamps.map((ts, i) => ({ + snapshotTs: ts.valueOf(), + dayStartTs: i > 0 ? timestamps[i - 1].valueOf() : ts.valueOf(), + })); + + const pnlMap = await this.bclPnlService.calculateDailyPnlBatch( + resolvedAddress, + windows, + ); + + return timestamps.map((ts) => { + const pnl = pnlMap.get(ts.valueOf()); + return { + timestamp: ts, + gain: { + ae: pnl?.totalGainAe ?? 0, + usd: pnl?.totalGainUsd ?? 0, + }, + }; + }); + } + async resolveAccountAddress(address: string): Promise { if (!address || address.startsWith('ak_') || !address.includes('.')) { return address; diff --git a/src/transactions/controllers/historical.controller.ts b/src/transactions/controllers/historical.controller.ts index 60b7cb52..4bb2fd53 100644 --- a/src/transactions/controllers/historical.controller.ts +++ b/src/transactions/controllers/historical.controller.ts @@ -14,6 +14,7 @@ import { import { ApiOperation, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; import { TokensService } from '@/tokens/tokens.service'; +import { buildSparklineSvg, sparklineStroke } from '@/utils/sparkline.util'; import moment from 'moment'; import { ITransactionPreview, @@ -188,18 +189,11 @@ export class HistoricalController { const values = preview.result.map((item) => Number(item.last_price)); - const COLOR_UP = '#2EB88A'; - const COLOR_DOWN = '#E14E4E'; - const stroke = - values.length >= 2 && values[values.length - 1] >= values[0] - ? COLOR_UP - : COLOR_DOWN; - - const svg = this.buildSparklineSvg( + const svg = buildSparklineSvg( values, Number(width), Number(height), - stroke, + sparklineStroke(values), background, ); @@ -209,46 +203,6 @@ export class HistoricalController { }); } - private buildSparklineSvg( - values: number[], - width: number, - height: number, - stroke: string, - background: string, - ): string { - const pad = 4; - const w = width; - const h = height; - - if (!values.length) { - return ``; - } - - const min = Math.min(...values); - const max = Math.max(...values); - const range = max - min || 1; - const n = values.length; - - const points = values.map((v, i) => { - const x = (i / (n - 1 || 1)) * w; - const y = h - pad - ((v - min) / range) * (h - pad * 2); - return [x, y] as [number, number]; - }); - - const d = points - .map( - ([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`, - ) - .join(' '); - - const bg = - background !== 'none' - ? `` - : ''; - - return `${bg}`; - } - private parseDate(value: string | number | undefined) { // if timestamp if (typeof value === 'number') { diff --git a/src/utils/sparkline.util.ts b/src/utils/sparkline.util.ts new file mode 100644 index 00000000..17f36d9e --- /dev/null +++ b/src/utils/sparkline.util.ts @@ -0,0 +1,61 @@ +export interface SparklineOptions { + width?: number; + height?: number; + stroke?: string; + background?: string; +} + +const COLOR_UP = '#2EB88A'; +const COLOR_DOWN = '#E14E4E'; + +/** + * Derive stroke color from the direction of the series (first vs last value). + */ +export function sparklineStroke(values: number[]): string { + return values.length >= 2 && values[values.length - 1] >= values[0] + ? COLOR_UP + : COLOR_DOWN; +} + +/** + * Build a minimal SVG sparkline path from a numeric series. + */ +export function buildSparklineSvg( + values: number[], + width = 160, + height = 60, + stroke = COLOR_UP, + background = 'none', +): string { + const pad = 4; + const w = width; + const h = height; + + if (!values.length) { + return ``; + } + + const min = Math.min(...values); + const max = Math.max(...values); + const range = max - min || 1; + const n = values.length; + + const points = values.map((v, i) => { + const x = (i / (n - 1 || 1)) * w; + const y = h - pad - ((v - min) / range) * (h - pad * 2); + return [x, y] as [number, number]; + }); + + const d = points + .map( + ([x, y], i) => `${i === 0 ? 'M' : 'L'} ${x.toFixed(2)} ${y.toFixed(2)}`, + ) + .join(' '); + + const bg = + background !== 'none' + ? `` + : ''; + + return `${bg}`; +} From 9b7bc071b1678f69577e7ced2905560504a38632 Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Wed, 1 Apr 2026 20:09:08 +0200 Subject: [PATCH 04/10] feat(sparkline): add CSS color sanitization for SVG attributes to prevent injection vulnerabilities --- src/utils/sparkline.util.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/utils/sparkline.util.ts b/src/utils/sparkline.util.ts index 17f36d9e..0d7f90af 100644 --- a/src/utils/sparkline.util.ts +++ b/src/utils/sparkline.util.ts @@ -8,6 +8,19 @@ export interface SparklineOptions { const COLOR_UP = '#2EB88A'; const COLOR_DOWN = '#E14E4E'; +/** + * Accepts only well-formed CSS color values so that user-supplied strings + * cannot break out of SVG attribute context and inject arbitrary markup. + * Anything that does not match falls back to the supplied default. + */ +const CSS_COLOR_RE = + /^(none|transparent|[a-zA-Z]+|#[0-9a-fA-F]{3,8}|(rgb|rgba|hsl|hsla)\([\d\s,%.\/]+\))$/; + +export function sanitizeCssColor(value: string, fallback = 'none'): string { + const trimmed = value.trim(); + return CSS_COLOR_RE.test(trimmed) ? trimmed : fallback; +} + /** * Derive stroke color from the direction of the series (first vs last value). */ @@ -27,12 +40,15 @@ export function buildSparklineSvg( stroke = COLOR_UP, background = 'none', ): string { + const safeStroke = sanitizeCssColor(stroke, COLOR_UP); + const safeBg = sanitizeCssColor(background, 'none'); + const pad = 4; const w = width; const h = height; if (!values.length) { - return ``; + return ``; } const min = Math.min(...values); @@ -53,9 +69,9 @@ export function buildSparklineSvg( .join(' '); const bg = - background !== 'none' - ? `` + safeBg !== 'none' + ? `` : ''; - return `${bg}`; + return `${bg}`; } From bc4224f35f7d50237e5883b4b225bb31b6866467 Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Wed, 1 Apr 2026 19:57:34 +0200 Subject: [PATCH 05/10] feat(coin-gecko): refactor CoinGeckoService to improve data fetching and caching; update AppService to utilize new syncAllFromApi method for initialization and cron job; adjust portfolio service tests to reflect method name changes --- .../services/portfolio.service.spec.ts | 14 +- src/account/services/portfolio.service.ts | 4 +- src/ae-pricing/ae-pricing.service.ts | 25 +- .../controllers/price-feed.controller.ts | 4 +- src/ae/coin-gecko.service.spec.ts | 16 +- src/ae/coin-gecko.service.ts | 480 +++++++++--------- src/app.service.ts | 20 +- 7 files changed, 287 insertions(+), 276 deletions(-) diff --git a/src/account/services/portfolio.service.spec.ts b/src/account/services/portfolio.service.spec.ts index cc25f7d2..8ccc06f6 100644 --- a/src/account/services/portfolio.service.spec.ts +++ b/src/account/services/portfolio.service.spec.ts @@ -30,11 +30,11 @@ describe('PortfolioService', () => { }, }; const coinGeckoService = { - fetchHistoricalPrice: jest.fn(), + getHistoricalPrice: jest.fn(), getPriceData: jest.fn(), }; const coinHistoricalPriceService = { - // Default: no DB data -> falls back to coinGeckoService.fetchHistoricalPrice + // Default: no DB data -> falls back to coinGeckoService.getHistoricalPrice getHistoricalPriceData: jest.fn().mockResolvedValue([]), }; const bclPnlService = { @@ -82,7 +82,7 @@ describe('PortfolioService', () => { }, ); - coinGeckoService.fetchHistoricalPrice.mockResolvedValue([ + coinGeckoService.getHistoricalPrice.mockResolvedValue([ [Date.UTC(2026, 0, 10), 10], [Date.UTC(2026, 0, 1), 1], ]); @@ -134,7 +134,7 @@ describe('PortfolioService', () => { }, ); - coinGeckoService.fetchHistoricalPrice.mockResolvedValue([ + coinGeckoService.getHistoricalPrice.mockResolvedValue([ [Date.UTC(2026, 0, 3), 3], [Date.UTC(2026, 0, 1), 1], [Date.UTC(2026, 0, 2), 2], @@ -176,7 +176,7 @@ describe('PortfolioService', () => { }, ); - coinGeckoService.fetchHistoricalPrice.mockResolvedValue([ + coinGeckoService.getHistoricalPrice.mockResolvedValue([ [Date.UTC(2026, 0, 4), 4], [Date.UTC(2026, 0, 1), 1], [Date.UTC(2026, 0, 2), 2], @@ -222,7 +222,7 @@ describe('PortfolioService', () => { return map; }, ); - coinGeckoService.fetchHistoricalPrice.mockResolvedValue([ + coinGeckoService.getHistoricalPrice.mockResolvedValue([ [Date.UTC(2026, 0, 3), 3], [Date.UTC(2026, 0, 1), 1], [Date.UTC(2026, 0, 2), 2], @@ -323,7 +323,7 @@ describe('PortfolioService', () => { }); expect(snapshots).toHaveLength(3); - expect(coinGeckoService.fetchHistoricalPrice).not.toHaveBeenCalled(); + expect(coinGeckoService.getHistoricalPrice).not.toHaveBeenCalled(); expect( coinHistoricalPriceService.getHistoricalPriceData, ).toHaveBeenCalledTimes(1); diff --git a/src/account/services/portfolio.service.ts b/src/account/services/portfolio.service.ts index eb7f1578..2dba7b35 100644 --- a/src/account/services/portfolio.service.ts +++ b/src/account/services/portfolio.service.ts @@ -220,11 +220,11 @@ export class PortfolioService { // expected by findClosestHistoricalPrice. aePriceHistory = dbPriceRows.reverse(); } else { - // DB has no data for this range; fall back to live CoinGecko fetch. + // DB has no data for this range; serve from cache / DB / fallback JSON. const daysNeeded = Math.ceil(now.diff(start, 'days', true)) + 3; const days = Math.min(365, Math.max(7, daysNeeded)); aePriceHistory = await this.coinGeckoService - .fetchHistoricalPrice(AETERNITY_COIN_ID, 'usd', days, 'daily') + .getHistoricalPrice(AETERNITY_COIN_ID, 'usd', days, 'daily') .then((prices) => prices.sort((a, b) => b[0] - a[0])); } // Resolve all block heights in a single batch query against the local key_blocks table. diff --git a/src/ae-pricing/ae-pricing.service.ts b/src/ae-pricing/ae-pricing.service.ts index 74800847..251c311a 100644 --- a/src/ae-pricing/ae-pricing.service.ts +++ b/src/ae-pricing/ae-pricing.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import BigNumber from 'bignumber.js'; import { CoinGeckoService } from '@/ae/coin-gecko.service'; -import { AETERNITY_COIN_ID, CURRENCIES } from '@/configs'; +import { CURRENCIES } from '@/configs'; import { IPriceDto } from '@/tokens/dto/price.dto'; import { Repository } from 'typeorm'; import { CoinPrice } from './entities/coin-price.entity'; @@ -17,9 +17,20 @@ export class AePricingService { private coinPriceRepository: Repository, ) {} + /** + * Reads the latest rates from the CoinGeckoService in-memory / Redis cache + * and persists a new snapshot to the coin_prices table. + * Does NOT call the CoinGecko API directly — syncAllFromApi() (cron) must have + * already populated the cache before this is called. + */ async pullAndSaveCoinCurrencyRates() { - const rates = - await this.coinGeckoService.fetchCoinCurrencyRates(AETERNITY_COIN_ID); + let rates: Record | null = null; + try { + rates = await this.coinGeckoService.getAeternityRates(); + } catch (_err) { + // Rates unavailable — fall back to latest DB row below + } + if (!rates) { this.latestRates = await this.coinPriceRepository.findOne({ where: {}, @@ -29,6 +40,7 @@ export class AePricingService { }); return this.latestRates; } + try { this.latestRates = await this.coinPriceRepository.save({ rates, @@ -46,11 +58,13 @@ export class AePricingService { /** * Retrieves the price data for a given amount of AE tokens. + * Reads from the coin_prices DB table (last saved rates snapshot). + * If no DB row exists yet, uses in-memory / Redis rates via CoinGeckoService. * @param price - The amount of AE tokens. * @returns An object containing the price data for AE and other currencies. */ async getPriceData(price: BigNumber): Promise { - let latestRates = null; + let latestRates: CoinPrice | null = null; try { latestRates = await this.coinPriceRepository.findOne({ where: {}, @@ -61,6 +75,8 @@ export class AePricingService { } catch (error) { // } + + // Populate latestRates from cache if not yet in DB (first startup before cron runs) if (!latestRates) { latestRates = await this.pullAndSaveCoinCurrencyRates(); } @@ -77,7 +93,6 @@ export class AePricingService { try { prices[code] = price.multipliedBy(this.latestRates.rates![code]) as any; } catch (error) { - // console.warn(`Failed to calculate price for ${code}`); prices[code] = null; } }); diff --git a/src/ae-pricing/controllers/price-feed.controller.ts b/src/ae-pricing/controllers/price-feed.controller.ts index f7a62917..57ff416b 100644 --- a/src/ae-pricing/controllers/price-feed.controller.ts +++ b/src/ae-pricing/controllers/price-feed.controller.ts @@ -268,8 +268,8 @@ export class PriceFeedController { } } - // Fetch data from CoinGecko - const data = await this.coinGeckoService.fetchHistoricalPrice( + // Serve from DB / Redis cache — never triggers a live CoinGecko API call + const data = await this.coinGeckoService.getHistoricalPrice( coinId, currency, finalDays, diff --git a/src/ae/coin-gecko.service.spec.ts b/src/ae/coin-gecko.service.spec.ts index cc80df2e..25b0f202 100644 --- a/src/ae/coin-gecko.service.spec.ts +++ b/src/ae/coin-gecko.service.spec.ts @@ -44,14 +44,10 @@ describe('CoinGeckoService', () => { expect(service).toBeDefined(); }); - it('should initialize and pull data on instantiation', async () => { - const pullDataSpy = jest - .spyOn(CoinGeckoService.prototype, 'pullData') - .mockResolvedValue(undefined as any); - + it('should not pull data on instantiation (syncAllFromApi drives all fetches)', () => { + (fetchJson as jest.Mock).mockResolvedValue(null); new CoinGeckoService(cacheManager as any); - - expect(pullDataSpy).toHaveBeenCalled(); + expect(fetchJson).not.toHaveBeenCalled(); }); it('should fetch coin currency rates correctly', async () => { @@ -60,7 +56,7 @@ describe('CoinGeckoService', () => { [AETERNITY_COIN_ID]: mockRates, }); - const rates = await service.fetchCoinCurrencyRates(AETERNITY_COIN_ID); + const rates = await (service as any).fetchCoinCurrencyRates(AETERNITY_COIN_ID); expect(fetchJson).toHaveBeenCalledWith( expect.stringContaining('/simple/price'), ); @@ -69,7 +65,7 @@ describe('CoinGeckoService', () => { it('should return null when fetching coin currency rates fails', async () => { (fetchJson as jest.Mock).mockRejectedValue(new Error('API Error')); - const rates = await service.fetchCoinCurrencyRates(AETERNITY_COIN_ID); + const rates = await (service as any).fetchCoinCurrencyRates(AETERNITY_COIN_ID); expect(rates).toBeNull(); }); @@ -106,7 +102,7 @@ describe('CoinGeckoService', () => { it('should fetch data from API correctly', async () => { (fetchJson as jest.Mock).mockResolvedValue({ data: 'mockData' }); - const result = await service.fetchFromApi('/market', { ids: 'ae' }); + const result = await (service as any).fetchFromApi('/market', { ids: 'ae' }); expect(fetchJson).toHaveBeenCalledWith( expect.stringContaining('/market?ids=ae'), ); diff --git a/src/ae/coin-gecko.service.ts b/src/ae/coin-gecko.service.ts index 350d86ac..995bf4db 100644 --- a/src/ae/coin-gecko.service.ts +++ b/src/ae/coin-gecko.service.ts @@ -63,76 +63,52 @@ export class CoinGeckoService { >(); private readonly marketCacheKeyPrefix = 'coingecko:market:v1'; - /** - * CoinGeckoService class responsible for pulling data at regular intervals. - */ constructor( @Inject(CACHE_MANAGER) private cacheManager: Cache, @Optional() @Inject(forwardRef(() => CoinHistoricalPriceService)) private historicalPriceService?: CoinHistoricalPriceService, ) { - // Periodic pull with error handling to avoid unhandled promise rejections - setInterval( - () => { - this.pullData().catch((error: unknown) => { - this.logger.error('Failed to pull CoinGecko data on interval', error); - }); - }, - 1000 * 60 * 5, - ); // 5 minutes - - // Initial pull with guarded error logging - this.pullData().catch((error: unknown) => { - this.logger.error('Failed to pull initial CoinGecko data', error); - }); + // Initial data warm-up is triggered by AppService.init() on startup via syncAllFromApi(). + // Periodic refresh is handled by the @Cron(EVERY_10_MINUTES) in AppService. } /** - * Fetches the coin currency rates for Aeternity and assigns them to the `rates` property. + * Called by the 10-minute cron job in AppService and on startup. + * Fetches fresh data from CoinGecko for all endpoints and populates memory / Redis / DB. + * This is the ONLY place that makes outbound CoinGecko HTTP calls. */ - async pullData() { - const rates = await this.fetchCoinCurrencyRates(AETERNITY_COIN_ID); - if (rates) { - this.rates = rates; - this.last_pull_time = moment(); - // Cache the rates for 10 minutes (600 seconds) - try { - await this.cacheManager.set('coingecko:rates', rates, 600 * 1000); - this.logger.debug('Cached currency rates'); - } catch (error) { - this.logger.warn('Failed to cache currency rates:', error); - } - } else { - // If API fails, try to use cached rates - try { - const cachedRates = - await this.cacheManager.get('coingecko:rates'); - if (cachedRates) { - this.logger.log('Using cached currency rates due to API failure'); - this.rates = cachedRates; - this.last_pull_time = moment(); // Update time to prevent immediate retry - } else { - // Try fallback from JSON file - const fallbackRates = this.getFallbackRates(); - if (fallbackRates) { - this.logger.log('Using fallback rates from JSON file'); - this.rates = fallbackRates; - this.last_pull_time = moment(); - } else { - this.logger.warn('No rates available from API, cache, or fallback'); - } - } - } catch (error) { - this.logger.warn('Failed to read cached rates:', error); - // Try fallback from JSON file - const fallbackRates = this.getFallbackRates(); - if (fallbackRates) { - this.logger.log('Using fallback rates from JSON file'); - this.rates = fallbackRates; - this.last_pull_time = moment(); - } + async syncAllFromApi(): Promise { + // 1. Fetch spot rates (/simple/price) → memory + Redis + await this.pullData(); + + // 2. Fetch market data (/coins/markets) → Redis + try { + const marketData = await this.fetchCoinMarketData(AETERNITY_COIN_ID, 'usd'); + if (marketData) { + type CachedMarket = { data: CoinGeckoMarketResponse; fetchedAt: number }; + const cacheKey = `${this.marketCacheKeyPrefix}:${AETERNITY_COIN_ID}:usd`; + const payload: CachedMarket = { data: marketData, fetchedAt: Date.now() }; + await this.cacheManager.set(cacheKey, payload, 24 * 60 * 60 * 1000); + this.logger.debug('Synced market data from CoinGecko'); } + } catch (error) { + this.logger.error('Failed to sync market data from CoinGecko:', error); + } + + // 3. Fetch historical price data (/coins/{id}/market_chart) → DB + Redis + try { + await this.refreshHistoricalPrice(AETERNITY_COIN_ID, 'usd', 365, 'daily'); + this.logger.debug('Synced 365d daily historical prices from CoinGecko'); + } catch (error) { + this.logger.error('Failed to sync 365d historical prices from CoinGecko:', error); + } + + try { + await this.refreshHistoricalPrice(AETERNITY_COIN_ID, 'usd', 7, 'daily'); + this.logger.debug('Synced 7d daily historical prices from CoinGecko'); + } catch (error) { + this.logger.error('Failed to sync 7d historical prices from CoinGecko:', error); } } @@ -142,36 +118,15 @@ export class CoinGeckoService { } /** - * Returns the best-available currency rates (prefers fresh in-memory, then refresh, - * then cached, then JSON fallback). Never returns an empty object. + * Returns the best-available currency rates from memory / Redis / fallback JSON. + * Never triggers a live CoinGecko API call — data is kept fresh by syncAllFromApi(). */ async getAeternityRates(): Promise { if (this.rates && !this.isPullTimeExpired()) { return this.rates; } - // Deduplicate concurrent refresh attempts - if (!this.ratesPullInFlight) { - this.ratesPullInFlight = this.pullData() - .catch((err: unknown) => { - // pullData already tries cache + fallback internally; this catch prevents unhandled rejections - this.logger.warn( - 'Rates refresh failed (will try cached/fallback)', - err, - ); - }) - .finally(() => { - this.ratesPullInFlight = null; - }); - } - - await this.ratesPullInFlight; - - if (this.rates) { - return this.rates; - } - - // As a last resort, try cache directly, then JSON fallback + // Try Redis cache try { const cachedRates = await this.cacheManager.get('coingecko:rates'); @@ -181,12 +136,10 @@ export class CoinGeckoService { return cachedRates; } } catch (error) { - this.logger.warn( - 'Failed to read cached rates in getAeternityRates:', - error, - ); + this.logger.warn('Failed to read cached rates in getAeternityRates:', error); } + // Try fallback JSON const fallbackRates = this.getFallbackRates(); if (fallbackRates) { this.rates = fallbackRates; @@ -199,59 +152,26 @@ export class CoinGeckoService { ); } - /** - * Gets fallback rates from the JSON file (uses latest USD price) - * @returns CurrencyRates object with USD rate, or null if unavailable - */ - private getFallbackRates(): CurrencyRates | null { - try { - const priceData = this.readFallbackPriceData(); - if (priceData && priceData.length > 0) { - // Get the latest price by finding the entry with the highest timestamp - const sortedByTime = [...priceData].sort((a, b) => b[0] - a[0]); - const latestPrice = sortedByTime[0][1]; - if (latestPrice && typeof latestPrice === 'number') { - this.logger.log(`Using fallback USD rate from JSON: ${latestPrice}`); - // Return rates object with USD rate - // Note: We only have USD rate from the JSON file, other currencies will be null - return { - usd: latestPrice, - } as CurrencyRates; - } - } - return null; - } catch (error) { - this.logger.error('Failed to get fallback rates:', error); - return null; - } - } - /** * Retrieves the price data for a given amount of AE tokens. + * Reads rates from memory / Redis / fallback — never triggers a live API call. * @param price - The amount of AE tokens. * @returns An object containing the price data for AE and other currencies. */ async getPriceData(price: BigNumber): Promise { + // Ensure we have rates from memory, Redis cache, or fallback if (this.rates === null || this.isPullTimeExpired()) { - await this.pullData(); - } - - // If rates are still null, try to get from cache or fallback - if (this.rates === null) { - this.logger.warn( - 'CoinGecko rates are null after pullData, trying cache and fallback', - ); try { const cachedRates = await this.cacheManager.get('coingecko:rates'); if (cachedRates) { - this.logger.log('Using cached rates as fallback'); this.rates = cachedRates; + this.last_pull_time = moment(); } else { const fallbackRates = this.getFallbackRates(); if (fallbackRates) { - this.logger.log('Using fallback rates from JSON file'); this.rates = fallbackRates; + this.last_pull_time = moment(); } } } catch (error) { @@ -259,13 +179,9 @@ export class CoinGeckoService { } } - // If still null, use fallback rates or return AE only if (this.rates === null) { this.logger.error('CoinGecko rates are null and no fallback available'); - // Return AE amount only, with null for other currencies - const prices: any = { - ae: price, - }; + const prices: any = { ae: price }; CURRENCIES.forEach(({ code }) => { prices[code] = null; }); @@ -299,13 +215,183 @@ export class CoinGeckoService { } /** - * Fetches data from the Coin Gecko API. - * - * @param path - The API endpoint path. - * @param searchParams - The search parameters to be included in the request. - * @returns A Promise that resolves to the fetched data. + * Returns market data from Redis cache only. + * Fresh data is kept populated by syncAllFromApi(). + * Returns stale cached data if cache exists; throws 503 if cache is empty. + */ + async getCoinMarketData( + coinId: string, + currencyCode: string, + maxAgeMs: number = DEFAULT_MARKET_DATA_MAX_AGE_MS, + ): Promise { + const cacheKey = `${this.marketCacheKeyPrefix}:${coinId}:${currencyCode}`; + + type CachedMarket = { data: CoinGeckoMarketResponse; fetchedAt: number }; + + let cached: CachedMarket | null = null; + try { + cached = (await this.cacheManager.get(cacheKey)) ?? null; + const fetchedAtOk = + cached && + typeof cached.fetchedAt === 'number' && + Number.isFinite(cached.fetchedAt); + if (cached?.data && fetchedAtOk) { + const ageMs = Date.now() - cached.fetchedAt; + if (ageMs > maxAgeMs) { + this.logger.debug( + `Market data cache is ${Math.round(ageMs / 1000)}s old (max ${Math.round(maxAgeMs / 1000)}s); serving stale until next cron sync`, + ); + } + return cached.data; + } + } catch (error) { + this.logger.warn(`Failed to read market cache (${cacheKey}):`, error); + } + + throw new ServiceUnavailableException( + 'Aeternity market data is temporarily unavailable', + ); + } + + /** + * Returns historical price data from Redis cache or DB only. + * Never calls the CoinGecko API — data is kept fresh by syncAllFromApi(). + * Falls back to the bundled ae-pricing.json if both cache and DB are empty. + */ + async getHistoricalPrice( + coinId: string, + vsCurrency: string, + days: number = 365, + interval?: 'daily' | 'hourly', + ): Promise> { + const cacheKey = `coingecko:historical:${coinId}:${vsCurrency}:${days}:${interval || 'none'}`; + + // 1. Redis cache + try { + const cached = + await this.cacheManager.get>(cacheKey); + if (cached) { + this.logger.debug( + `Using Redis cached historical price data for ${coinId} (${vsCurrency}, ${days}d, ${interval || 'none'})`, + ); + return cached; + } + } catch (error) { + this.logger.warn(`Cache read error for ${cacheKey}:`, error); + } + + // 2. DB + if (this.historicalPriceService) { + try { + const now = moment(); + const endTimeMs = now.valueOf(); + const startTimeMs = now.subtract(days, 'days').valueOf(); + + const dbData = await this.historicalPriceService.getHistoricalPriceData( + coinId, + vsCurrency, + startTimeMs, + endTimeMs, + ); + + if (dbData.length > 0) { + this.logger.debug( + `Serving ${dbData.length} historical price points from DB for ${coinId}`, + ); + try { + await this.cacheManager.set(cacheKey, dbData, 3600 * 1000); + } catch (cacheError) { + this.logger.warn(`Cache write error for ${cacheKey}:`, cacheError); + } + return dbData; + } + } catch (error) { + this.logger.warn('Failed to query DB for historical prices:', error); + } + } + + // 3. Fallback JSON file + this.logger.warn( + `No historical price data in cache or DB for ${coinId}/${vsCurrency}/${days}d; using fallback JSON`, + ); + return this.readFallbackPriceData(); + } + + /** + * Fetches the coin currency rates for Aeternity and assigns them to the `rates` property. + * Updates Redis cache. Called only by syncAllFromApi(). + */ + private async pullData() { + const rates = await this.fetchCoinCurrencyRates(AETERNITY_COIN_ID); + if (rates) { + this.rates = rates; + this.last_pull_time = moment(); + try { + await this.cacheManager.set('coingecko:rates', rates, 600 * 1000); + this.logger.debug('Cached currency rates'); + } catch (error) { + this.logger.warn('Failed to cache currency rates:', error); + } + } else { + // If API fails, try to use cached rates + try { + const cachedRates = + await this.cacheManager.get('coingecko:rates'); + if (cachedRates) { + this.logger.log('Using cached currency rates due to API failure'); + this.rates = cachedRates; + this.last_pull_time = moment(); + } else { + const fallbackRates = this.getFallbackRates(); + if (fallbackRates) { + this.logger.log('Using fallback rates from JSON file'); + this.rates = fallbackRates; + this.last_pull_time = moment(); + } else { + this.logger.warn('No rates available from API, cache, or fallback'); + } + } + } catch (error) { + this.logger.warn('Failed to read cached rates:', error); + const fallbackRates = this.getFallbackRates(); + if (fallbackRates) { + this.logger.log('Using fallback rates from JSON file'); + this.rates = fallbackRates; + this.last_pull_time = moment(); + } + } + } + } + + /** + * Gets fallback rates from the JSON file (uses latest USD price). + * @returns CurrencyRates object with USD rate, or null if unavailable + */ + private getFallbackRates(): CurrencyRates | null { + try { + const priceData = this.readFallbackPriceData(); + if (priceData && priceData.length > 0) { + const sortedByTime = [...priceData].sort((a, b) => b[0] - a[0]); + const latestPrice = sortedByTime[0][1]; + if (latestPrice && typeof latestPrice === 'number') { + this.logger.log(`Using fallback USD rate from JSON: ${latestPrice}`); + return { + usd: latestPrice, + } as CurrencyRates; + } + } + return null; + } catch (error) { + this.logger.error('Failed to get fallback rates:', error); + return null; + } + } + + /** + * Fetches data from the CoinGecko API. + * Private — only called from within this service. */ - fetchFromApi(path: string, searchParams: Record) { + private fetchFromApi(path: string, searchParams: Record) { const query = new URLSearchParams(searchParams).toString(); const url = `${COIN_GECKO_API_URL}${path}?${query}`; return fetchJson(url); @@ -313,8 +399,9 @@ export class CoinGeckoService { /** * Obtain all the coin rates for the currencies used in the app. + * Private — only called from pullData() → syncAllFromApi(). */ - async fetchCoinCurrencyRates(coinId: string): Promise { + private async fetchCoinCurrencyRates(coinId: string): Promise { try { const response = (await this.fetchFromApi('/simple/price', { ids: coinId, @@ -348,12 +435,13 @@ export class CoinGeckoService { } /** - * Obtain all the coin market data (price, market cap, volume, etc...) + * Obtain all the coin market data (price, market cap, volume, etc...). + * Private — only called from syncAllFromApi(). * @param coinId - The CoinGecko coin ID (e.g., 'aeternity') * @param currencyCode - The target currency code (e.g., 'usd') * @returns Market data response or null if fetch fails */ - async fetchCoinMarketData( + private async fetchCoinMarketData( coinId: string, currencyCode: string, ): Promise { @@ -393,86 +481,7 @@ export class CoinGeckoService { } /** - * Returns market data with a "soft TTL" cache: - * - returns cached data immediately if it's fresh enough - * - otherwise tries to refresh; on refresh failure, returns last cached data - * - if nothing cached and refresh fails, throws 503 - */ - async getCoinMarketData( - coinId: string, - currencyCode: string, - maxAgeMs: number = DEFAULT_MARKET_DATA_MAX_AGE_MS, - ): Promise { - const cacheKey = `${this.marketCacheKeyPrefix}:${coinId}:${currencyCode}`; - - type CachedMarket = { data: CoinGeckoMarketResponse; fetchedAt: number }; - - let cached: CachedMarket | null = null; - try { - cached = (await this.cacheManager.get(cacheKey)) ?? null; - const fetchedAtOk = - cached && - typeof cached.fetchedAt === 'number' && - Number.isFinite(cached.fetchedAt); - if ( - cached?.data && - fetchedAtOk && - Date.now() - cached.fetchedAt <= maxAgeMs - ) { - return cached.data; - } - } catch (error) { - this.logger.warn(`Failed to read market cache (${cacheKey}):`, error); - } - - // Deduplicate concurrent market fetches per key (prevents request stampede / rate limits) - const inflightKey = cacheKey; - const existing = this.marketDataInFlight.get(inflightKey); - if (existing) { - try { - return await existing; - } catch (err) { - // Fall through to cached fallback below - } - } - - const fetchPromise = (async () => { - const fresh = await this.fetchCoinMarketData(coinId, currencyCode); - if (!fresh) { - throw new Error('CoinGecko market data fetch returned null'); - } - try { - const payload: CachedMarket = { data: fresh, fetchedAt: Date.now() }; - // Keep long TTL so we can fall back to last-known-good even during outages/rate limits - await this.cacheManager.set(cacheKey, payload, 24 * 60 * 60 * 1000); - } catch (error) { - this.logger.warn(`Failed to write market cache (${cacheKey}):`, error); - } - return fresh; - })(); - - this.marketDataInFlight.set(inflightKey, fetchPromise); - - try { - return await fetchPromise; - } catch (error) { - if (cached?.data) { - this.logger.warn( - `Using cached market data due to refresh failure (${coinId}/${currencyCode})`, - error, - ); - return cached.data; - } - throw new ServiceUnavailableException( - 'Aeternity market data is temporarily unavailable', - ); - } finally { - this.marketDataInFlight.delete(inflightKey); - } - } - - /** - * Reads historical price data from the fallback JSON file + * Reads historical price data from the fallback JSON file. * @returns Array of [timestamp_ms, price] pairs from the JSON file */ private readFallbackPriceData(): Array<[number, number]> { @@ -502,20 +511,20 @@ export class CoinGeckoService { } /** - * Fetch historical price data for a coin (with database storage and caching) + * Fetches and persists historical price data from CoinGecko (DB + Redis). + * Private — only called from syncAllFromApi() to keep caches warm. * @param coinId - The CoinGecko coin ID (e.g., 'aeternity') * @param vsCurrency - The target currency (e.g., 'usd') * @param days - Number of days of history to fetch (1, 7, 14, 30, 90, 180, 365, max) - * @param interval - Interval for data points ('daily' or 'hourly'), defaults to 'daily'. If undefined, interval parameter is omitted from API call. + * @param interval - Interval for data points ('daily' or 'hourly'). If undefined, interval parameter is omitted from API call. * @returns Array of [timestamp_ms, price] pairs (never null) */ - async fetchHistoricalPrice( + private async refreshHistoricalPrice( coinId: string, vsCurrency: string, days: number = 365, interval?: 'daily' | 'hourly', ): Promise> { - // Create cache key based on coin, currency, days, and interval const cacheKey = `coingecko:historical:${coinId}:${vsCurrency}:${days}:${interval || 'none'}`; // Try to get from Redis cache first (for very recent requests, 1 hour TTL) @@ -559,13 +568,12 @@ export class CoinGeckoService { } // Determine if we need to fetch from CoinGecko - const isRecentData = days <= 7; // Recent data: last 7 days + const isRecentData = days <= 7; let needsFetch = false; let fetchDays = days; let fetchStartTime = startTimeMs; if (isRecentData && this.historicalPriceService) { - // For recent data: check for missing timestamps and fetch incrementally try { const latestTimestamp = await this.historicalPriceService.getLatestTimestamp( @@ -574,23 +582,19 @@ export class CoinGeckoService { ); if (latestTimestamp === null) { - // No data in database, fetch full range needsFetch = true; } else if (latestTimestamp < endTimeMs - 3600000) { - // Latest data is more than 1 hour old, fetch new data needsFetch = true; - // Calculate days needed: from latest timestamp to now const hoursSinceLatest = (endTimeMs - latestTimestamp) / (1000 * 60 * 60); fetchDays = Math.ceil(hoursSinceLatest / 24); - fetchStartTime = latestTimestamp + 1; // Start from after latest timestamp + fetchStartTime = latestTimestamp + 1; } } catch (error) { this.logger.warn(`Failed to check latest timestamp:`, error); - needsFetch = true; // On error, fetch full range + needsFetch = true; } } else { - // For older data: check if entire range exists if (this.historicalPriceService) { try { const missingRanges = @@ -603,7 +607,6 @@ export class CoinGeckoService { if (missingRanges.length > 0) { needsFetch = true; - // Fetch the largest missing range (usually covers everything) const largestRange = missingRanges.reduce((prev, curr) => { const prevSize = prev[1] - prev[0]; const currSize = curr[1] - curr[0]; @@ -617,10 +620,9 @@ export class CoinGeckoService { } } catch (error) { this.logger.warn(`Failed to check missing data ranges:`, error); - needsFetch = true; // On error, fetch full range + needsFetch = true; } } else { - // No database service available, fetch from CoinGecko needsFetch = true; } } @@ -630,7 +632,6 @@ export class CoinGeckoService { this.logger.log( `Using complete historical data from database: ${dbData.length} price points`, ); - // Cache in Redis for 1 hour to reduce DB queries try { await this.cacheManager.set(cacheKey, dbData, 3600 * 1000); } catch (error) { @@ -648,7 +649,6 @@ export class CoinGeckoService { days: String(fetchDays), }; - // Only add interval parameter if provided if (interval) { searchParams.interval = interval; } @@ -665,17 +665,14 @@ export class CoinGeckoService { status?: { error_code: number; error_message: string }; }; - // Check for CoinGecko API errors (e.g., rate limiting) if (response?.status?.error_code) { if (response.status.error_code === 429) { this.logger.warn( `CoinGecko rate limit hit (429). Using database data if available, or falling back to JSON file.`, ); - // Try to use database data even if incomplete if (dbData.length > 0) { return dbData; } - // Try stale cache try { const staleCache = await this.cacheManager.get>(cacheKey); @@ -702,13 +699,11 @@ export class CoinGeckoService { const prices = response?.prices || null; if (prices && prices.length > 0) { - // Filter to only include data in the requested range newData = prices.filter( ([timestamp]) => timestamp >= fetchStartTime && timestamp <= endTimeMs, ); - // Log first and last price points for debugging if (newData.length > 0) { const firstPrice = newData[0]; const lastPrice = newData[newData.length - 1]; @@ -717,7 +712,6 @@ export class CoinGeckoService { ); } - // Save new data to database if (this.historicalPriceService && newData.length > 0) { try { await this.historicalPriceService.savePriceData( @@ -730,7 +724,6 @@ export class CoinGeckoService { `Failed to save price data to database:`, error, ); - // Continue even if save fails } } } else { @@ -747,7 +740,6 @@ export class CoinGeckoService { `Failed to fetch historical price from CoinGecko:`, error, ); - // Use database data if available if (dbData.length > 0) { this.logger.log('Using database data due to CoinGecko fetch failure'); return dbData; @@ -762,7 +754,6 @@ export class CoinGeckoService { if (this.historicalPriceService) { mergedData = this.historicalPriceService.mergePriceData(dbData, newData); } else { - // If no service, just use new data mergedData = newData; } @@ -771,7 +762,6 @@ export class CoinGeckoService { ([timestamp]) => timestamp >= startTimeMs && timestamp <= endTimeMs, ); - // Cache merged result in Redis for 1 hour if (mergedData.length > 0) { try { await this.cacheManager.set(cacheKey, mergedData, 3600 * 1000); diff --git a/src/app.service.ts b/src/app.service.ts index 7a674bab..d000d67c 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -4,6 +4,7 @@ import { Cron, CronExpression } from '@nestjs/schedule'; import { Queue } from 'bull'; import moment, { Moment } from 'moment'; import { AePricingService } from './ae-pricing/ae-pricing.service'; +import { CoinGeckoService } from './ae/coin-gecko.service'; import { CommunityFactoryService } from './ae/community-factory.service'; import { DELETE_OLD_TOKENS_QUEUE } from './tokens/queues/constants'; @@ -13,6 +14,7 @@ export class AppService { constructor( private communityFactoryService: CommunityFactoryService, private aePricingService: AePricingService, + private coinGeckoService: CoinGeckoService, @InjectQueue(DELETE_OLD_TOKENS_QUEUE) private readonly deleteOldTokensQueue: Queue, @@ -22,18 +24,26 @@ export class AppService { } async init() { - await this.aePricingService.pullAndSaveCoinCurrencyRates(); - const factory = await this.communityFactoryService.getCurrentFactory(); await this.deleteOldTokensQueue.empty(); void this.deleteOldTokensQueue.add({ factories: [factory.address], }); + + // Warm all CoinGecko caches (rates, market data, historical) on startup + await this.coinGeckoService.syncAllFromApi(); + // Persist latest rates to the coin_prices DB table + await this.aePricingService.pullAndSaveCoinCurrencyRates(); + + } - @Cron(CronExpression.EVERY_HOUR) - syncAeCoinPricing() { - this.aePricingService.pullAndSaveCoinCurrencyRates(); + @Cron(CronExpression.EVERY_10_MINUTES) + async syncAeCoinPricing() { + // Fetch fresh data from CoinGecko and populate memory / Redis / DB caches + await this.coinGeckoService.syncAllFromApi(); + // Persist latest rates snapshot to the coin_prices DB table + await this.aePricingService.pullAndSaveCoinCurrencyRates(); } /** From 34a47cc679d573cadc46c07bd2026538a8067e9f Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Wed, 1 Apr 2026 20:10:52 +0200 Subject: [PATCH 06/10] refactor(coin-gecko): improve code readability by formatting function parameters and enhancing error logging; clean up unnecessary whitespace in AppService --- src/ae/coin-gecko.service.spec.ts | 12 ++++++++--- src/ae/coin-gecko.service.ts | 34 ++++++++++++++++++++++++------- src/app.service.ts | 2 -- 3 files changed, 36 insertions(+), 12 deletions(-) diff --git a/src/ae/coin-gecko.service.spec.ts b/src/ae/coin-gecko.service.spec.ts index 25b0f202..c744a80c 100644 --- a/src/ae/coin-gecko.service.spec.ts +++ b/src/ae/coin-gecko.service.spec.ts @@ -56,7 +56,9 @@ describe('CoinGeckoService', () => { [AETERNITY_COIN_ID]: mockRates, }); - const rates = await (service as any).fetchCoinCurrencyRates(AETERNITY_COIN_ID); + const rates = await (service as any).fetchCoinCurrencyRates( + AETERNITY_COIN_ID, + ); expect(fetchJson).toHaveBeenCalledWith( expect.stringContaining('/simple/price'), ); @@ -65,7 +67,9 @@ describe('CoinGeckoService', () => { it('should return null when fetching coin currency rates fails', async () => { (fetchJson as jest.Mock).mockRejectedValue(new Error('API Error')); - const rates = await (service as any).fetchCoinCurrencyRates(AETERNITY_COIN_ID); + const rates = await (service as any).fetchCoinCurrencyRates( + AETERNITY_COIN_ID, + ); expect(rates).toBeNull(); }); @@ -102,7 +106,9 @@ describe('CoinGeckoService', () => { it('should fetch data from API correctly', async () => { (fetchJson as jest.Mock).mockResolvedValue({ data: 'mockData' }); - const result = await (service as any).fetchFromApi('/market', { ids: 'ae' }); + const result = await (service as any).fetchFromApi('/market', { + ids: 'ae', + }); expect(fetchJson).toHaveBeenCalledWith( expect.stringContaining('/market?ids=ae'), ); diff --git a/src/ae/coin-gecko.service.ts b/src/ae/coin-gecko.service.ts index 995bf4db..13e9e747 100644 --- a/src/ae/coin-gecko.service.ts +++ b/src/ae/coin-gecko.service.ts @@ -84,11 +84,20 @@ export class CoinGeckoService { // 2. Fetch market data (/coins/markets) → Redis try { - const marketData = await this.fetchCoinMarketData(AETERNITY_COIN_ID, 'usd'); + const marketData = await this.fetchCoinMarketData( + AETERNITY_COIN_ID, + 'usd', + ); if (marketData) { - type CachedMarket = { data: CoinGeckoMarketResponse; fetchedAt: number }; + type CachedMarket = { + data: CoinGeckoMarketResponse; + fetchedAt: number; + }; const cacheKey = `${this.marketCacheKeyPrefix}:${AETERNITY_COIN_ID}:usd`; - const payload: CachedMarket = { data: marketData, fetchedAt: Date.now() }; + const payload: CachedMarket = { + data: marketData, + fetchedAt: Date.now(), + }; await this.cacheManager.set(cacheKey, payload, 24 * 60 * 60 * 1000); this.logger.debug('Synced market data from CoinGecko'); } @@ -101,14 +110,20 @@ export class CoinGeckoService { await this.refreshHistoricalPrice(AETERNITY_COIN_ID, 'usd', 365, 'daily'); this.logger.debug('Synced 365d daily historical prices from CoinGecko'); } catch (error) { - this.logger.error('Failed to sync 365d historical prices from CoinGecko:', error); + this.logger.error( + 'Failed to sync 365d historical prices from CoinGecko:', + error, + ); } try { await this.refreshHistoricalPrice(AETERNITY_COIN_ID, 'usd', 7, 'daily'); this.logger.debug('Synced 7d daily historical prices from CoinGecko'); } catch (error) { - this.logger.error('Failed to sync 7d historical prices from CoinGecko:', error); + this.logger.error( + 'Failed to sync 7d historical prices from CoinGecko:', + error, + ); } } @@ -136,7 +151,10 @@ export class CoinGeckoService { return cachedRates; } } catch (error) { - this.logger.warn('Failed to read cached rates in getAeternityRates:', error); + this.logger.warn( + 'Failed to read cached rates in getAeternityRates:', + error, + ); } // Try fallback JSON @@ -401,7 +419,9 @@ export class CoinGeckoService { * Obtain all the coin rates for the currencies used in the app. * Private — only called from pullData() → syncAllFromApi(). */ - private async fetchCoinCurrencyRates(coinId: string): Promise { + private async fetchCoinCurrencyRates( + coinId: string, + ): Promise { try { const response = (await this.fetchFromApi('/simple/price', { ids: coinId, diff --git a/src/app.service.ts b/src/app.service.ts index d000d67c..e602cc0d 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -34,8 +34,6 @@ export class AppService { await this.coinGeckoService.syncAllFromApi(); // Persist latest rates to the coin_prices DB table await this.aePricingService.pullAndSaveCoinCurrencyRates(); - - } @Cron(CronExpression.EVERY_10_MINUTES) From a4bec60dcb93fdc64617efb92e17479a8366f73d Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Wed, 1 Apr 2026 20:17:20 +0200 Subject: [PATCH 07/10] refactor(coin-gecko): enhance getPriceData method to clarify rate warming logic; ensure rates are only refreshed when null --- src/ae/coin-gecko.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ae/coin-gecko.service.ts b/src/ae/coin-gecko.service.ts index 13e9e747..f0910c15 100644 --- a/src/ae/coin-gecko.service.ts +++ b/src/ae/coin-gecko.service.ts @@ -173,12 +173,14 @@ export class CoinGeckoService { /** * Retrieves the price data for a given amount of AE tokens. * Reads rates from memory / Redis / fallback — never triggers a live API call. + * The cron (syncAllFromApi) keeps rates fresh; here we only warm up if rates are null. * @param price - The amount of AE tokens. * @returns An object containing the price data for AE and other currencies. */ async getPriceData(price: BigNumber): Promise { - // Ensure we have rates from memory, Redis cache, or fallback - if (this.rates === null || this.isPullTimeExpired()) { + // Only try to warm up when rates have never been populated yet. + // If rates exist but are stale the cron will refresh them shortly — use what we have. + if (this.rates === null) { try { const cachedRates = await this.cacheManager.get('coingecko:rates'); From 00ee79f400f02955d3eb8d7e6f2623e2eee691a0 Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Thu, 2 Apr 2026 09:14:07 +0200 Subject: [PATCH 08/10] feat(coin-gecko): add hourly historical price synchronization and improve cache management; streamline price feed controller by removing unused cache manager --- .../controllers/price-feed.controller.ts | 137 ++---------------- src/ae/coin-gecko.service.ts | 12 +- 2 files changed, 25 insertions(+), 124 deletions(-) diff --git a/src/ae-pricing/controllers/price-feed.controller.ts b/src/ae-pricing/controllers/price-feed.controller.ts index 57ff416b..9d8cf670 100644 --- a/src/ae-pricing/controllers/price-feed.controller.ts +++ b/src/ae-pricing/controllers/price-feed.controller.ts @@ -1,12 +1,10 @@ -import { Controller, Get, Query, Inject } from '@nestjs/common'; +import { Controller, Get, Query } from '@nestjs/common'; import { ApiOperation, ApiQuery, ApiTags, ApiOkResponse, } from '@nestjs/swagger'; -import { CACHE_MANAGER } from '@nestjs/cache-manager'; -import { Cache } from 'cache-manager'; import { CoinGeckoService, CoinGeckoMarketResponse, @@ -17,10 +15,7 @@ import { AETERNITY_COIN_ID } from '@/configs'; @Controller('coins') @ApiTags('Coins') export class PriceFeedController { - constructor( - private readonly coinGeckoService: CoinGeckoService, - @Inject(CACHE_MANAGER) private cacheManager: Cache, - ) {} + constructor(private readonly coinGeckoService: CoinGeckoService) {} @ApiOperation({ operationId: 'getCurrencyRates', @@ -75,10 +70,10 @@ export class PriceFeedController { @ApiQuery({ name: 'interval', type: 'string', - enum: ['daily', 'hourly', 'minute'], + enum: ['daily', 'hourly'], required: false, description: - 'Interval for historical data points (default: daily). Minute interval is only available for 1 day. Hourly interval is only available for 1-90 days. For days>90, only daily data is available.', + 'Interval for historical data points (default: daily). Hourly interval is only available for 1-90 days. For days>90, only daily data is available.', example: 'daily', }) @ApiOkResponse({ @@ -103,7 +98,7 @@ export class PriceFeedController { async getHistoricalPrice( @Query('currency') currency?: string, @Query('days') days?: string | number, - @Query('interval') interval?: 'daily' | 'hourly' | 'minute', + @Query('interval') interval?: 'daily' | 'hourly', ): Promise> { return this.getHistoricalPriceData( AETERNITY_COIN_ID, @@ -153,146 +148,42 @@ export class PriceFeedController { } /** - * Aggregates minute-level data into hourly buckets - * @param minuteData Array of [timestamp_ms, price] pairs with minute-level granularity - * @returns Array of [timestamp_ms, price] pairs aggregated to hourly intervals (24 data points for 1 day) - */ - private aggregateToHourly( - minuteData: Array<[number, number]>, - ): Array<[number, number]> { - if (minuteData.length === 0) { - return []; - } - - // Group data points by hour - const hourlyBuckets = new Map(); - - for (const [timestamp, price] of minuteData) { - // Round down to the start of the hour (in milliseconds) - const hourStart = - Math.floor(timestamp / (1000 * 60 * 60)) * (1000 * 60 * 60); - - if (!hourlyBuckets.has(hourStart)) { - hourlyBuckets.set(hourStart, []); - } - hourlyBuckets.get(hourStart)!.push(price); - } - - // Aggregate each hour's prices (using average) - const hourlyData: Array<[number, number]> = []; - const sortedHours = Array.from(hourlyBuckets.keys()).sort((a, b) => a - b); - - for (const hourStart of sortedHours) { - const prices = hourlyBuckets.get(hourStart)!; - // Calculate average price for this hour - const avgPrice = - prices.reduce((sum, price) => sum + price, 0) / prices.length; - hourlyData.push([hourStart, avgPrice]); - } - - return hourlyData; - } - - /** - * Helper method to get historical price data - * Used internally by /history endpoint + * Helper method to get historical price data. + * Used internally by /history endpoint. */ private async getHistoricalPriceData( coinId: string, currency: string = 'usd', days: string | number = '365', - interval: 'daily' | 'hourly' | 'minute' = 'daily', + interval: 'daily' | 'hourly' = 'daily', ): Promise> { - // Validate days parameter - supported values: 1, 7, 14, 30, 90, 180, 365, max const validDays = [1, 7, 14, 30, 90, 180, 365]; - // Handle 'max' string or convert to number let finalDays: number; if (days === 'max' || days === 'Max' || days === 'MAX') { - // For 'max', use 365 days (the service accepts number, not 'max' string) - // The service will handle the conversion internally if needed - finalDays = 365; // Using max available for now + finalDays = 365; } else { const daysValue = Number(days); if (isNaN(daysValue) || daysValue <= 0) { - finalDays = 365; // Default to 365 if invalid + finalDays = 365; } else if (validDays.includes(daysValue)) { finalDays = daysValue; } else { - // Find closest valid value finalDays = validDays.reduce((prev, curr) => Math.abs(curr - daysValue) < Math.abs(prev - daysValue) ? curr : prev, ); } } - // Handle interval parameter based on days value - let finalInterval: 'daily' | 'hourly' | undefined; - let shouldAggregateToHourly = false; + // Hourly is only meaningful for 1–90 days; beyond that force daily. + const finalInterval: 'daily' | 'hourly' = + interval === 'hourly' && finalDays <= 90 ? 'hourly' : 'daily'; - // For days=1: minute-level data (~5 min intervals) is returned automatically when interval is omitted - if (finalDays === 1) { - if (interval === 'hourly') { - // For days=1 with hourly, fetch minute-level data and aggregate to 24 hourly data points - shouldAggregateToHourly = true; - finalInterval = undefined; // Omit interval to get minute-level data - } else if (interval === 'minute') { - // For days=1 with minute, return minute-level data as-is - finalInterval = undefined; - } else { - // For days=1 with daily, use daily (returns 2 data points: start/end of day) - finalInterval = 'daily'; - } - } else if (interval === 'minute') { - // Minute interval only works for days=1, so force days=1 and omit interval - finalDays = 1; - finalInterval = undefined; // CoinGecko returns minute data for days=1 automatically - } else { - // For other cases, use the requested interval or default to daily - finalInterval = interval === 'hourly' ? 'hourly' : 'daily'; - } - - // If we need aggregated hourly data, check cache first - if (shouldAggregateToHourly && finalDays === 1) { - const aggregatedCacheKey = `coingecko:historical:aggregated:hourly:${coinId}:${currency}:${finalDays}`; - try { - const cached = - await this.cacheManager.get>( - aggregatedCacheKey, - ); - if (cached) { - return cached; - } - } catch (error) { - // If cache read fails, continue to fetch and aggregate - } - } - - // Serve from DB / Redis cache — never triggers a live CoinGecko API call - const data = await this.coinGeckoService.getHistoricalPrice( + return this.coinGeckoService.getHistoricalPrice( coinId, currency, finalDays, finalInterval, ); - - // If we need to aggregate to hourly, do it now and cache the result - if (shouldAggregateToHourly && finalDays === 1) { - const aggregatedData = this.aggregateToHourly(data); - const aggregatedCacheKey = `coingecko:historical:aggregated:hourly:${coinId}:${currency}:${finalDays}`; - try { - // Cache aggregated hourly data for 1 hour (same as raw data) - await this.cacheManager.set( - aggregatedCacheKey, - aggregatedData, - 3600 * 1000, - ); - } catch (error) { - // If cache write fails, still return the aggregated data - } - return aggregatedData; - } - - return data; } } diff --git a/src/ae/coin-gecko.service.ts b/src/ae/coin-gecko.service.ts index f0910c15..dec422e5 100644 --- a/src/ae/coin-gecko.service.ts +++ b/src/ae/coin-gecko.service.ts @@ -125,6 +125,16 @@ export class CoinGeckoService { error, ); } + + try { + await this.refreshHistoricalPrice(AETERNITY_COIN_ID, 'usd', 1, 'hourly'); + this.logger.debug('Synced 1d hourly historical prices from CoinGecko'); + } catch (error) { + this.logger.error( + 'Failed to sync 1d hourly historical prices from CoinGecko:', + error, + ); + } } isPullTimeExpired() { @@ -549,7 +559,7 @@ export class CoinGeckoService { ): Promise> { const cacheKey = `coingecko:historical:${coinId}:${vsCurrency}:${days}:${interval || 'none'}`; - // Try to get from Redis cache first (for very recent requests, 1 hour TTL) + // Try to get from Redis cache first (1 hour TTL) try { const cached = await this.cacheManager.get>(cacheKey); From 7c238ce5e8d9685916656b281308660eb2f216e2 Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Thu, 2 Apr 2026 09:32:11 +0200 Subject: [PATCH 09/10] feat(accounts): add getTokensPnlHistory endpoint to retrieve token PnL history with optional date range and interval; enhance portfolio service to support per-token PnL breakdown --- .../controllers/accounts.controller.ts | 34 +++++++++++++++++++ src/account/services/portfolio.service.ts | 7 ++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/account/controllers/accounts.controller.ts b/src/account/controllers/accounts.controller.ts index f6da37aa..094d323a 100644 --- a/src/account/controllers/accounts.controller.ts +++ b/src/account/controllers/accounts.controller.ts @@ -198,6 +198,40 @@ export class AccountsController { }); } + @ApiOperation({ operationId: 'getTokensPnlHistory' }) + @ApiParam({ name: 'address', type: 'string', description: 'Account address' }) + @ApiOkResponse({ type: [PortfolioHistorySnapshotDto] }) + @CacheTTL(60 * 10) + @Get(':address/portfolio/tokens/history') + async getTokensPnlHistory( + @Param('address') address: string, + @Query() query: GetPortfolioHistoryQueryDto, + ) { + const start = query.startDate ? moment(query.startDate) : undefined; + const end = query.endDate ? moment(query.endDate) : undefined; + const includeFields = query.include + ? query.include.split(',').map((f) => f.trim()) + : []; + + const minimumInterval = this.getMinimumInterval(start, end); + const requestedInterval = query.interval || 86400; + const finalInterval = Math.max(requestedInterval, minimumInterval); + + const includePnl = + includeFields.includes('pnl') || includeFields.includes('pnl-range'); + const useRangeBasedPnl = includeFields.includes('pnl-range'); + + return await this.portfolioService.getPortfolioHistory(address, { + startDate: start, + endDate: end, + interval: finalInterval, + convertTo: query.convertTo || 'ae', + includePnl, + useRangeBasedPnl, + includeTokensPnl: true, + }); + } + // Portfolio PnL sparkline — MUST come before :address route to avoid route conflict @ApiOperation({ operationId: 'getPortfolioPnlChart', diff --git a/src/account/services/portfolio.service.ts b/src/account/services/portfolio.service.ts index 2dba7b35..48763c38 100644 --- a/src/account/services/portfolio.service.ts +++ b/src/account/services/portfolio.service.ts @@ -89,6 +89,7 @@ export interface GetPortfolioHistoryOptions { | 'xau'; includePnl?: boolean; // Whether to include PNL data useRangeBasedPnl?: boolean; // If true, calculate PNL for range between timestamps; if false, use all previous transactions + includeTokensPnl?: boolean; // Whether to include per-token PNL breakdown (large payload) } export interface PnlDataPoint { @@ -141,6 +142,7 @@ export class PortfolioService { interval = 86400, // Default daily (24 hours) includePnl = false, useRangeBasedPnl = false, + includeTokensPnl = false, } = options; // Calculate date range @@ -379,8 +381,9 @@ export class PortfolioService { }; } - // Include individual token PNL data (use range-based if available, otherwise cumulative) - result.tokens_pnl = pnlData.pnls; + if (includeTokensPnl) { + result.tokens_pnl = pnlData.pnls; + } } return result; From bd7837d25e6a4e86449cdcd85b70eec003038950 Mon Sep 17 00:00:00 2001 From: Badi Ifaoui Date: Thu, 2 Apr 2026 09:41:21 +0200 Subject: [PATCH 10/10] feat(accounts): enhance portfolio history endpoints with detailed summaries and descriptions; update query DTO for clarity on included fields --- .../controllers/accounts.controller.ts | 21 ++++++++++++++----- .../dto/get-portfolio-history-query.dto.ts | 5 ++++- .../dto/portfolio-history-response.dto.ts | 3 ++- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/account/controllers/accounts.controller.ts b/src/account/controllers/accounts.controller.ts index 094d323a..9b76eaa5 100644 --- a/src/account/controllers/accounts.controller.ts +++ b/src/account/controllers/accounts.controller.ts @@ -161,7 +161,14 @@ export class AccountsController { } // Portfolio history endpoint - MUST come before :address route to avoid route conflict - @ApiOperation({ operationId: 'getPortfolioHistory' }) + @ApiOperation({ + operationId: 'getPortfolioHistory', + summary: 'Portfolio value history snapshots', + description: + 'Returns portfolio value over time (AE balance, token values, total value). ' + + 'Pass include=pnl or include=pnl-range to add aggregate total_pnl data. ' + + 'Per-token PnL breakdown (tokens_pnl) is available via the dedicated :address/portfolio/tokens/history endpoint.', + }) @ApiParam({ name: 'address', type: 'string', description: 'Account address' }) @ApiOkResponse({ type: [PortfolioHistorySnapshotDto] }) @CacheTTL(60 * 10) // 10 minutes @@ -198,7 +205,13 @@ export class AccountsController { }); } - @ApiOperation({ operationId: 'getTokensPnlHistory' }) + @ApiOperation({ + operationId: 'getTokensPnlHistory', + summary: 'Per-token PnL history snapshots', + description: + 'Returns portfolio history snapshots that include the per-token PnL breakdown (tokens_pnl). ' + + 'PnL data is always included; pass include=pnl-range to use range-based (daily window) PnL instead of cumulative.', + }) @ApiParam({ name: 'address', type: 'string', description: 'Account address' }) @ApiOkResponse({ type: [PortfolioHistorySnapshotDto] }) @CacheTTL(60 * 10) @@ -217,8 +230,6 @@ export class AccountsController { const requestedInterval = query.interval || 86400; const finalInterval = Math.max(requestedInterval, minimumInterval); - const includePnl = - includeFields.includes('pnl') || includeFields.includes('pnl-range'); const useRangeBasedPnl = includeFields.includes('pnl-range'); return await this.portfolioService.getPortfolioHistory(address, { @@ -226,7 +237,7 @@ export class AccountsController { endDate: end, interval: finalInterval, convertTo: query.convertTo || 'ae', - includePnl, + includePnl: true, useRangeBasedPnl, includeTokensPnl: true, }); diff --git a/src/account/dto/get-portfolio-history-query.dto.ts b/src/account/dto/get-portfolio-history-query.dto.ts index 008ba6a8..db0bdc57 100644 --- a/src/account/dto/get-portfolio-history-query.dto.ts +++ b/src/account/dto/get-portfolio-history-query.dto.ts @@ -62,7 +62,10 @@ export class GetPortfolioHistoryQueryDto { name: 'include', type: 'string', required: false, - description: 'Comma-separated list of fields to include (e.g., "pnl")', + description: + 'Comma-separated list of fields to include. ' + + '"pnl" adds aggregate total_pnl, "pnl-range" uses daily-window PnL instead of cumulative. ' + + 'Per-token breakdown (tokens_pnl) is only available via the /portfolio/tokens/history endpoint.', example: 'pnl', }) @IsOptional() diff --git a/src/account/dto/portfolio-history-response.dto.ts b/src/account/dto/portfolio-history-response.dto.ts index 287578de..fffde21e 100644 --- a/src/account/dto/portfolio-history-response.dto.ts +++ b/src/account/dto/portfolio-history-response.dto.ts @@ -73,7 +73,8 @@ export class PortfolioHistorySnapshotDto { @ApiProperty({ description: - 'PNL breakdown per token (included if requested), keyed by token sale_address', + 'PNL breakdown per token, keyed by token sale_address. ' + + 'Only returned by the dedicated /portfolio/tokens/history endpoint.', type: Object, required: false, })