From 86668b5fa65be93c682bc91b6fba5b139385a3ed Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Fri, 6 Mar 2026 15:48:07 +0100 Subject: [PATCH] fix(performance-storage): make MikroORM integration work (#1701) * fix(performance-storage): make MikroORM integration work Fix MikroORM performance storage integration by addressing: - Fork EntityManager per operation (prevents identity map accumulation) - Pass entity class to em.upsert() (MikroORM v6 API) - Add schema generation in open() (table creation) - Fix entity: add tableName, JSON type for statisticsData - Add connection validation before operations - Use direct entity class import instead of fragile glob paths - Ensure SQLite directory exists before ORM init - Fix type-only import to value import (verbatimModuleSyntax) Closes #42 * fix(performance-storage): address PR review feedback - Swap setPerformanceStatistics/checkDBConnection call order to match MongoDBStorage pattern (in-memory stats updated first so UI stays current even when DB is down) - Close ORM connection on schema updateSchema() failure to prevent resource leak when init succeeds but schema generation fails * test(performance-storage): add unit tests for MikroOrmStorage Add 18 tests covering constructor, close, storePerformanceStatistics, and error handling. Uses mock ORM to avoid sqlite3 native binding dependency in CI (--ignore-scripts). Explicit decorator types on PerformanceRecord for tsx/esbuild compatibility. * refactor(tests): harmonize MikroOrmStorage tests with project conventions Use TestableMikroOrmStorage subclass with Reflect.get/set (matches UIHttpServer pattern). Replace toBeGreaterThan with exact toBe counts. Use .mock.calls.length consistently. Remove constructor tests that tested implementation details. * refactor(performance-storage): migrate from @mikro-orm/sqlite to @mikro-orm/better-sqlite The sqlite3 native bindings used by @mikro-orm/sqlite are broken on Node v24 (no prebuilt binaries for ABI v137, node-gyp@8.4.1 cannot compile with Python 3.13). Replace with better-sqlite3 which provides an identical MikroORM API surface while using a more portable native binding. * test(performance-storage): add integration tests for MikroOrmStorage Add 6 integration tests that exercise real SQLite database operations (open, persist, upsert, JSON serialization, close/reopen, multi-record). Tests detect better-sqlite3 availability at module load and skip gracefully via { skip: SKIP_SQLITE } when the native binding is not built (e.g. CI with --ignore-scripts). * fix(performance-storage): use caret version for @mikro-orm/better-sqlite and fix test JSDoc order Use ^6.6.8 (caret) consistently across all @mikro-orm/* packages to prevent version skew when updating dependencies. MikroORM requires all packages be on the same version. Also move @file JSDoc block before all imports in the test file to match project conventions. * refactor(performance-storage): extract ensureDBDirectory into Storage base class and audit fixes Move duplicated directory creation logic from JsonFileStorage and MikroOrmStorage into a shared protected method in the abstract Storage base class. Place the call inside the existing switch case instead of a separate if block. Also use ^6.6.8 (caret) consistently for @mikro-orm/better-sqlite and fix test JSDoc ordering to match project conventions. * refactor(performance-storage): factorize serialization into base class and fix MongoDBStorage - Extract serializePerformanceStatistics() into Storage base class (DRY) - Update MikroOrmStorage to use shared serialization method - Fix MongoDBStorage: remove unnecessary optional client, use shared serialization for CircularBuffer, remove eslint-disable comments, rename connected to opened for lifecycle clarity - Add 20 unit tests for MongoDBStorage mirroring MikroOrmStorage pattern * refactor(performance-storage): harmonize storage implementations - Remove 2 eslint-disable comments in Storage.handleDBStorageError by using nullish coalescing and proper string typing - Simplify handleDBStorageError params default spread - Refactor JsonFileStorage.checkPerformanceRecordsFile to return fd, eliminating non-null assertion and eslint-disable - Remove eslint-disable in StorageFactory by casting enum to string - Fix StorageFactory.getStorage return type: Storage (not | undefined) - Update None.ts copyright year to 2025 for consistency * refactor(tests): extract buildTestStatistics into shared StorageTestHelpers * fix(lint): remove unnecessary optional chain after StorageFactory return type fix --- mikro-orm.config-template.ts | 6 +- package.json | 7 +- pnpm-lock.yaml | 540 ++---------------- src/charging-station/Bootstrap.ts | 2 +- src/performance/storage/JsonFileStorage.ts | 15 +- src/performance/storage/MikroOrmStorage.ts | 46 +- src/performance/storage/MongoDBStorage.ts | 39 +- src/performance/storage/None.ts | 2 +- src/performance/storage/Storage.ts | 37 +- src/performance/storage/StorageFactory.ts | 9 +- src/types/orm/entities/PerformanceRecord.ts | 36 +- .../storage/MikroOrmStorage.test.ts | 446 +++++++++++++++ .../storage/MongoDBStorage.test.ts | 411 +++++++++++++ .../performance/storage/StorageTestHelpers.ts | 31 + 14 files changed, 1031 insertions(+), 596 deletions(-) create mode 100644 tests/performance/storage/MikroOrmStorage.test.ts create mode 100644 tests/performance/storage/MongoDBStorage.test.ts create mode 100644 tests/performance/storage/StorageTestHelpers.ts diff --git a/mikro-orm.config-template.ts b/mikro-orm.config-template.ts index d2796255..d96aac08 100644 --- a/mikro-orm.config-template.ts +++ b/mikro-orm.config-template.ts @@ -1,10 +1,10 @@ -import { defineConfig } from '@mikro-orm/sqlite' +import { defineConfig } from '@mikro-orm/better-sqlite' +import { PerformanceRecord } from './src/types/orm/entities/PerformanceRecord.js' import { Constants } from './src/utils/index.js' export default defineConfig({ dbName: `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db`, debug: true, - entities: ['./dist/types/orm/entities/*.js'], - entitiesTs: ['./src/types/orm/entities/*.ts'], + entities: [PerformanceRecord], }) diff --git a/package.json b/package.json index f760d7cf..fad6e6c3 100644 --- a/package.json +++ b/package.json @@ -75,10 +75,10 @@ "sea": "pnpm exec rimraf ./dist/evse-simulator ./dist/evse-simulator.blob && node --experimental-sea-config sea-config.json && pnpm dlx ncp $(volta which node || n which lts || nvm which node || command -v node) ./dist/evse-simulator && pnpm dlx postject ./dist/evse-simulator NODE_SEA_BLOB ./dist/evse-simulator.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 && pnpm exec rimraf ./dist/evse-simulator.blob" }, "dependencies": { + "@mikro-orm/better-sqlite": "^6.6.9", "@mikro-orm/core": "^6.6.9", "@mikro-orm/mariadb": "^6.6.9", "@mikro-orm/reflection": "^6.6.9", - "@mikro-orm/sqlite": "^6.6.9", "ajv": "^8.18.0", "ajv-formats": "^3.0.1", "basic-ftp": "^5.2.0", @@ -127,5 +127,10 @@ "tsx": "^4.21.0", "typescript": "~5.9.3", "vue-eslint-parser": "^10.4.0" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60dc9ec8..2fe38c20 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,18 +21,18 @@ importers: .: dependencies: + '@mikro-orm/better-sqlite': + specifier: ^6.6.9 + version: 6.6.9(@mikro-orm/core@6.6.9)(mariadb@3.4.5) '@mikro-orm/core': specifier: ^6.6.9 version: 6.6.9 '@mikro-orm/mariadb': specifier: ^6.6.9 - version: 6.6.9(@mikro-orm/core@6.6.9) + version: 6.6.9(@mikro-orm/core@6.6.9)(better-sqlite3@11.10.0) '@mikro-orm/reflection': specifier: ^6.6.9 version: 6.6.9(@mikro-orm/core@6.6.9) - '@mikro-orm/sqlite': - specifier: ^6.6.9 - version: 6.6.9(@mikro-orm/core@6.6.9)(mariadb@3.4.5) ajv: specifier: ^8.18.0 version: 8.18.0 @@ -90,7 +90,7 @@ importers: version: 9.39.3 '@mikro-orm/cli': specifier: ^6.6.9 - version: 6.6.9(mariadb@3.4.5) + version: 6.6.9(better-sqlite3@11.10.0)(mariadb@3.4.5) '@std/expect': specifier: npm:@jsr/std__expect@^1.0.18 version: '@jsr/std__expect@1.0.18' @@ -964,9 +964,6 @@ packages: '@noble/hashes': optional: true - '@gar/promisify@1.1.3': - resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1031,6 +1028,12 @@ packages: '@keyv/serialize@1.1.1': resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@mikro-orm/better-sqlite@6.6.9': + resolution: {integrity: sha512-vd3VF1n6FWbkR2dbDH1CR+rdM3MLgWUPMkL9nTLM3hOSLzhFQFbuVGxfSTjDMPvjZoJN6eSXMVI8mAdWjFLu2g==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@mikro-orm/core': ^6.0.0 + '@mikro-orm/cli@6.6.9': resolution: {integrity: sha512-CjYTVoVds82hTsvahq5pZedW3Wf3owt6qCbH8En3DtZCH64iSoG1XQ9LSAGJbyer+YIodeHwBv1aRALc6VoTUA==} engines: {node: '>= 18.12.0'} @@ -1068,12 +1071,6 @@ packages: peerDependencies: '@mikro-orm/core': ^6.0.0 - '@mikro-orm/sqlite@6.6.9': - resolution: {integrity: sha512-kl9rHS0DH7hWg7Qz6rNzcmsHQmzmmxsl1u3luwPuGTvez7dTM0f3Z+36NxFyQA1gcyHagyZO4pxIn0ZyiU+E4Q==} - engines: {node: '>= 18.12.0'} - peerDependencies: - '@mikro-orm/core': ^6.0.0 - '@mongodb-js/saslprep@1.4.6': resolution: {integrity: sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==} @@ -1093,14 +1090,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@npmcli/fs@1.1.1': - resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} - - '@npmcli/move-file@1.1.2': - resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} - engines: {node: '>=10'} - deprecated: This functionality has been moved to @npmcli/fs - '@one-ini/wasm@0.1.1': resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} @@ -1293,10 +1282,6 @@ packages: resolution: {integrity: sha512-YSfsswOqWfd+M4bXIhT3hwtAb+IV8+ODwIxwdFR/7jTAPZP1wMVnSlpKnXHAN64HFOiP+Tm3HmKusEZ0+09A0w==} engines: {yarn: '>= 1.3.2'} - '@tootallnate/once@1.1.2': - resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==} - engines: {node: '>= 6'} - '@ts-morph/common@0.28.1': resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} @@ -1567,9 +1552,6 @@ packages: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true - abbrev@1.1.1: - resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} - abbrev@2.0.0: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -1604,18 +1586,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - agentkeepalive@4.6.0: - resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} - engines: {node: '>= 8.0.0'} - aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -1696,18 +1670,10 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - aproba@2.1.0: - resolution: {integrity: sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==} - are-docs-informative@0.0.2: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} - are-we-there-yet@3.0.1: - resolution: {integrity: sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. - arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -1851,6 +1817,9 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + better-sqlite3@11.10.0: + resolution: {integrity: sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==} + bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} @@ -1969,10 +1938,6 @@ packages: resolution: {integrity: sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==} engines: {node: '>=20'} - cacache@15.3.0: - resolution: {integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==} - engines: {node: '>= 10'} - cacheable-lookup@7.0.0: resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==} engines: {node: '>=14.16'} @@ -2057,10 +2022,6 @@ packages: chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} @@ -2238,9 +2199,6 @@ packages: console-browserify@1.2.0: resolution: {integrity: sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==} - console-control-strings@1.1.0: - resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} - constants-browserify@1.0.0: resolution: {integrity: sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==} @@ -2551,9 +2509,6 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} - delegates@1.0.0: - resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} - denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -2700,9 +2655,6 @@ packages: resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} engines: {node: '>=18'} - err-code@2.0.3: - resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} - error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -3132,10 +3084,6 @@ packages: resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==} engines: {node: '>=14.14'} - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} @@ -3154,11 +3102,6 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - gauge@4.0.4: - resolution: {integrity: sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. - generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} @@ -3411,10 +3354,6 @@ packages: http-parser-js@0.5.10: resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} - http-proxy-agent@4.0.1: - resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} - engines: {node: '>= 6'} - http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3433,10 +3372,6 @@ packages: https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -3445,9 +3380,6 @@ packages: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} engines: {node: '>=8.12.0'} - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -3500,9 +3432,6 @@ packages: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - infer-owner@1.0.4: - resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} - inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3666,9 +3595,6 @@ packages: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} - is-lambda@1.0.1: - resolution: {integrity: sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==} - is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -4060,10 +3986,6 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - macos-release@2.5.1: resolution: {integrity: sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==} engines: {node: '>=6'} @@ -4095,10 +4017,6 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - make-fetch-happen@9.1.0: - resolution: {integrity: sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==} - engines: {node: '>= 10'} - manage-path@2.0.0: resolution: {integrity: sha512-NJhyB+PJYTpxhxZJ3lecIGgh4kwIY2RAh44XvAz9UlqthlQwtPBf62uBVR8XaD8CRuSjQ6TnZH2lNJkbLPZM2A==} @@ -4209,38 +4127,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass-collect@1.0.2: - resolution: {integrity: sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==} - engines: {node: '>= 8'} - - minipass-fetch@1.4.1: - resolution: {integrity: sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==} - engines: {node: '>=8'} - - minipass-flush@1.0.5: - resolution: {integrity: sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==} - engines: {node: '>= 8'} - - minipass-pipeline@1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} - - minipass-sized@1.0.3: - resolution: {integrity: sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==} - engines: {node: '>=8'} - - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} @@ -4398,10 +4288,6 @@ packages: ndarray@1.0.19: resolution: {integrity: sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==} - negotiator@0.6.4: - resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} - engines: {node: '>= 0.6'} - neostandard@0.13.0: resolution: {integrity: sha512-R3iglFr+Dla/8qFBqsMxBvcYBOgP6rAGw7uRHKMpM3bUP0wLDRzUstxtEI9RfEwn7xszE/UUnh8H090Ru4Z52A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} @@ -4419,9 +4305,6 @@ packages: resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} engines: {node: '>=10'} - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} @@ -4439,19 +4322,9 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-gyp@8.4.1: - resolution: {integrity: sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==} - engines: {node: '>= 10.12.0'} - hasBin: true - node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} - nopt@5.0.0: - resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} - engines: {node: '>=6'} - hasBin: true - nopt@7.2.1: resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -4473,11 +4346,6 @@ packages: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} - npmlog@6.0.2: - resolution: {integrity: sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - deprecated: This package is no longer supported. - nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -4817,18 +4685,6 @@ packages: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - promise-inflight@1.0.1: - resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} - peerDependencies: - bluebird: '*' - peerDependenciesMeta: - bluebird: - optional: true - - promise-retry@2.0.1: - resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} - engines: {node: '>=10'} - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -5020,10 +4876,6 @@ packages: retimer@3.0.0: resolution: {integrity: sha512-WKE0j11Pa0ZJI5YIk0nflGI7SQsfl2ljihVy7ogh7DeQSeYAUi0ubZ/yEueGtDfUPk6GH5LRw1hBdLq4IwUBWA==} - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -5227,10 +5079,6 @@ packages: resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} engines: {node: '>= 18'} - socks-proxy-agent@6.2.1: - resolution: {integrity: sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==} - engines: {node: '>= 10'} - socks@2.8.7: resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} @@ -5273,9 +5121,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - sqlite3@5.1.7: - resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} - sqlstring-sqlite@0.1.1: resolution: {integrity: sha512-9CAYUJ0lEUPYJrswqiqdINNSfq3jqWo/bFJ7tufdoNeSK0Fy+d1kFTxjqO9PIqza0Kri+ZtYMfPVf1aZaFOvrQ==} engines: {node: '>= 0.6'} @@ -5289,10 +5134,6 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - ssri@8.0.1: - resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} - engines: {node: '>= 8'} - stack-trace@0.0.10: resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} @@ -5733,12 +5574,6 @@ packages: uniq@1.0.1: resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} - unique-filename@1.1.1: - resolution: {integrity: sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==} - - unique-slug@2.0.2: - resolution: {integrity: sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==} - unique-string@2.0.0: resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} engines: {node: '>=8'} @@ -6000,9 +5835,6 @@ packages: engines: {node: '>=8'} hasBin: true - wide-align@1.1.5: - resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} - widest-line@3.1.0: resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} engines: {node: '>=8'} @@ -6096,9 +5928,6 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -6990,9 +6819,6 @@ snapshots: '@exodus/bytes@1.15.0': {} - '@gar/promisify@1.1.3': - optional: true - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -7067,11 +6893,29 @@ snapshots: '@keyv/serialize@1.1.1': {} - '@mikro-orm/cli@6.6.9(mariadb@3.4.5)': + '@mikro-orm/better-sqlite@6.6.9(@mikro-orm/core@6.6.9)(mariadb@3.4.5)': + dependencies: + '@mikro-orm/core': 6.6.9 + '@mikro-orm/knex': 6.6.9(@mikro-orm/core@6.6.9)(better-sqlite3@11.10.0)(mariadb@3.4.5) + better-sqlite3: 11.10.0 + fs-extra: 11.3.3 + sqlstring-sqlite: 0.1.1 + transitivePeerDependencies: + - libsql + - mariadb + - mysql + - mysql2 + - pg + - pg-native + - sqlite3 + - supports-color + - tedious + + '@mikro-orm/cli@6.6.9(better-sqlite3@11.10.0)(mariadb@3.4.5)': dependencies: '@jercle/yargonaut': 1.1.5 '@mikro-orm/core': 6.6.9 - '@mikro-orm/knex': 6.6.9(@mikro-orm/core@6.6.9)(mariadb@3.4.5)(sqlite3@5.1.7) + '@mikro-orm/knex': 6.6.9(@mikro-orm/core@6.6.9)(better-sqlite3@11.10.0)(mariadb@3.4.5) fs-extra: 11.3.3 tsconfig-paths: 4.2.0 yargs: 17.7.2 @@ -7097,13 +6941,14 @@ snapshots: mikro-orm: 6.6.9 reflect-metadata: 0.2.2 - '@mikro-orm/knex@6.6.9(@mikro-orm/core@6.6.9)(mariadb@3.4.5)(sqlite3@5.1.7)': + '@mikro-orm/knex@6.6.9(@mikro-orm/core@6.6.9)(better-sqlite3@11.10.0)(mariadb@3.4.5)': dependencies: '@mikro-orm/core': 6.6.9 fs-extra: 11.3.3 - knex: 3.1.0(sqlite3@5.1.7) + knex: 3.1.0(better-sqlite3@11.10.0) sqlstring: 2.3.3 optionalDependencies: + better-sqlite3: 11.10.0 mariadb: 3.4.5 transitivePeerDependencies: - mysql @@ -7114,10 +6959,10 @@ snapshots: - supports-color - tedious - '@mikro-orm/mariadb@6.6.9(@mikro-orm/core@6.6.9)': + '@mikro-orm/mariadb@6.6.9(@mikro-orm/core@6.6.9)(better-sqlite3@11.10.0)': dependencies: '@mikro-orm/core': 6.6.9 - '@mikro-orm/knex': 6.6.9(@mikro-orm/core@6.6.9)(mariadb@3.4.5)(sqlite3@5.1.7) + '@mikro-orm/knex': 6.6.9(@mikro-orm/core@6.6.9)(better-sqlite3@11.10.0)(mariadb@3.4.5) mariadb: 3.4.5 transitivePeerDependencies: - better-sqlite3 @@ -7136,25 +6981,6 @@ snapshots: globby: 11.1.0 ts-morph: 27.0.2 - '@mikro-orm/sqlite@6.6.9(@mikro-orm/core@6.6.9)(mariadb@3.4.5)': - dependencies: - '@mikro-orm/core': 6.6.9 - '@mikro-orm/knex': 6.6.9(@mikro-orm/core@6.6.9)(mariadb@3.4.5)(sqlite3@5.1.7) - fs-extra: 11.3.3 - sqlite3: 5.1.7 - sqlstring-sqlite: 0.1.1 - transitivePeerDependencies: - - better-sqlite3 - - bluebird - - libsql - - mariadb - - mysql - - mysql2 - - pg - - pg-native - - supports-color - - tedious - '@mongodb-js/saslprep@1.4.6': dependencies: sparse-bitfield: 3.0.3 @@ -7176,18 +7002,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@npmcli/fs@1.1.1': - dependencies: - '@gar/promisify': 1.1.3 - semver: 7.7.4 - optional: true - - '@npmcli/move-file@1.1.2': - dependencies: - mkdirp: 1.0.4 - rimraf: 3.0.2 - optional: true - '@one-ini/wasm@0.1.1': {} '@pkgjs/parseargs@0.11.0': @@ -7320,9 +7134,6 @@ snapshots: transitivePeerDependencies: - encoding - '@tootallnate/once@1.1.2': - optional: true - '@ts-morph/common@0.28.1': dependencies: minimatch: 10.2.4 @@ -7676,9 +7487,6 @@ snapshots: jsonparse: 1.3.1 through: 2.3.8 - abbrev@1.1.1: - optional: true - abbrev@2.0.0: {} abort-controller@3.0.0: @@ -7705,20 +7513,8 @@ snapshots: acorn@8.16.0: {} - agent-base@6.0.2: - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - optional: true - agent-base@7.1.4: {} - agentkeepalive@4.6.0: - dependencies: - humanize-ms: 1.2.1 - optional: true - aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -7785,17 +7581,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - aproba@2.1.0: - optional: true - are-docs-informative@0.0.2: {} - are-we-there-yet@3.0.1: - dependencies: - delegates: 1.0.0 - readable-stream: 3.6.2 - optional: true - arg@4.1.3: {} argparse@2.0.1: {} @@ -7966,6 +7753,11 @@ snapshots: dependencies: tweetnacl: 0.14.5 + better-sqlite3@11.10.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + bidi-js@1.0.3: dependencies: require-from-string: 2.0.2 @@ -8168,30 +7960,6 @@ snapshots: byte-counter@0.1.0: {} - cacache@15.3.0: - dependencies: - '@npmcli/fs': 1.1.1 - '@npmcli/move-file': 1.1.2 - chownr: 2.0.0 - fs-minipass: 2.1.0 - glob: 7.2.3 - infer-owner: 1.0.4 - lru-cache: 6.0.0 - minipass: 3.3.6 - minipass-collect: 1.0.2 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - mkdirp: 1.0.4 - p-map: 4.0.0 - promise-inflight: 1.0.1 - rimraf: 3.0.2 - ssri: 8.0.1 - tar: 7.5.10 - unique-filename: 1.1.1 - transitivePeerDependencies: - - bluebird - optional: true - cacheable-lookup@7.0.0: {} cacheable-request@13.0.18: @@ -8285,9 +8053,6 @@ snapshots: chownr@1.1.4: {} - chownr@2.0.0: - optional: true - chownr@3.0.0: {} ci-info@2.0.0: {} @@ -8497,9 +8262,6 @@ snapshots: console-browserify@1.2.0: {} - console-control-strings@1.1.0: - optional: true - constants-browserify@1.0.0: {} conventional-changelog-angular@8.2.0: @@ -8871,9 +8633,6 @@ snapshots: delayed-stream@1.0.0: {} - delegates@1.0.0: - optional: true - denque@2.1.0: {} depd@2.0.0: {} @@ -9021,9 +8780,6 @@ snapshots: environment@1.1.0: {} - err-code@2.0.3: - optional: true - error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -9638,11 +9394,6 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - optional: true - fs.realpath@1.0.0: {} fsevents@2.3.3: @@ -9661,18 +9412,6 @@ snapshots: functions-have-names@1.2.3: {} - gauge@4.0.4: - dependencies: - aproba: 2.1.0 - color-support: 1.1.3 - console-control-strings: 1.1.0 - has-unicode: 2.0.1 - signal-exit: 3.0.7 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wide-align: 1.1.5 - optional: true - generate-function@2.3.1: dependencies: is-property: 1.0.2 @@ -9944,15 +9683,6 @@ snapshots: http-parser-js@0.5.10: {} - http-proxy-agent@4.0.1: - dependencies: - '@tootallnate/once': 1.1.2 - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - optional: true - http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -9975,14 +9705,6 @@ snapshots: https-browserify@1.0.0: {} - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - optional: true - https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -9992,11 +9714,6 @@ snapshots: human-signals@1.1.1: {} - humanize-ms@1.2.1: - dependencies: - ms: 2.1.3 - optional: true - husky@9.1.7: {} hyperid@3.3.0: @@ -10038,9 +9755,6 @@ snapshots: indent-string@4.0.0: {} - infer-owner@1.0.4: - optional: true - inflight@1.0.6: dependencies: once: 1.4.0 @@ -10220,9 +9934,6 @@ snapshots: is-interactive@1.0.0: {} - is-lambda@1.0.1: - optional: true - is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -10448,7 +10159,7 @@ snapshots: dependencies: '@keyv/serialize': 1.1.1 - knex@3.1.0(sqlite3@5.1.7): + knex@3.1.0(better-sqlite3@11.10.0): dependencies: colorette: 2.0.19 commander: 10.0.1 @@ -10465,7 +10176,7 @@ snapshots: tarn: 3.0.2 tildify: 2.0.0 optionalDependencies: - sqlite3: 5.1.7 + better-sqlite3: 11.10.0 transitivePeerDependencies: - supports-color @@ -10597,11 +10308,6 @@ snapshots: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - optional: true - macos-release@2.5.1: {} magic-string-ast@1.0.3: @@ -10636,29 +10342,6 @@ snapshots: make-error@1.3.6: {} - make-fetch-happen@9.1.0: - dependencies: - agentkeepalive: 4.6.0 - cacache: 15.3.0 - http-cache-semantics: 4.2.0 - http-proxy-agent: 4.0.1 - https-proxy-agent: 5.0.1 - is-lambda: 1.0.1 - lru-cache: 6.0.0 - minipass: 3.3.6 - minipass-collect: 1.0.2 - minipass-fetch: 1.4.1 - minipass-flush: 1.0.5 - minipass-pipeline: 1.2.4 - negotiator: 0.6.4 - promise-retry: 2.0.1 - socks-proxy-agent: 6.2.1 - ssri: 8.0.1 - transitivePeerDependencies: - - bluebird - - supports-color - optional: true - manage-path@2.0.0: {} mariadb@3.4.5: @@ -10752,48 +10435,8 @@ snapshots: minimist@1.2.8: {} - minipass-collect@1.0.2: - dependencies: - minipass: 3.3.6 - optional: true - - minipass-fetch@1.4.1: - dependencies: - minipass: 3.3.6 - minipass-sized: 1.0.3 - minizlib: 2.1.2 - optionalDependencies: - encoding: 0.1.13 - optional: true - - minipass-flush@1.0.5: - dependencies: - minipass: 3.3.6 - optional: true - - minipass-pipeline@1.2.4: - dependencies: - minipass: 3.3.6 - optional: true - - minipass-sized@1.0.3: - dependencies: - minipass: 3.3.6 - optional: true - - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - optional: true - minipass@7.1.3: {} - minizlib@2.1.2: - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - optional: true - minizlib@3.1.0: dependencies: minipass: 7.1.3 @@ -10975,9 +10618,6 @@ snapshots: iota-array: 1.0.0 is-buffer: 1.1.6 - negotiator@0.6.4: - optional: true - neostandard@0.13.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3): dependencies: '@humanwhocodes/gitignore-to-minimatch': 1.0.2 @@ -11004,8 +10644,6 @@ snapshots: dependencies: semver: 7.7.4 - node-addon-api@7.1.1: {} - node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -11022,30 +10660,8 @@ snapshots: node-gyp-build@4.8.4: optional: true - node-gyp@8.4.1: - dependencies: - env-paths: 2.2.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - make-fetch-happen: 9.1.0 - nopt: 5.0.0 - npmlog: 6.0.2 - rimraf: 3.0.2 - semver: 7.7.4 - tar: 7.5.10 - which: 2.0.2 - transitivePeerDependencies: - - bluebird - - supports-color - optional: true - node-releases@2.0.36: {} - nopt@5.0.0: - dependencies: - abbrev: 1.1.1 - optional: true - nopt@7.2.1: dependencies: abbrev: 2.0.0 @@ -11060,14 +10676,6 @@ snapshots: dependencies: path-key: 3.1.1 - npmlog@6.0.2: - dependencies: - are-we-there-yet: 3.0.1 - console-control-strings: 1.1.0 - gauge: 4.0.4 - set-blocking: 2.0.0 - optional: true - nth-check@2.1.1: dependencies: boolbase: 1.0.0 @@ -11410,15 +11018,6 @@ snapshots: progress@2.0.3: {} - promise-inflight@1.0.1: - optional: true - - promise-retry@2.0.1: - dependencies: - err-code: 2.0.3 - retry: 0.12.0 - optional: true - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -11659,9 +11258,6 @@ snapshots: retimer@3.0.0: {} - retry@0.12.0: - optional: true - reusify@1.1.0: {} rfdc@1.4.1: {} @@ -11918,15 +11514,6 @@ snapshots: smol-toml@1.6.0: {} - socks-proxy-agent@6.2.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.3 - socks: 2.8.7 - transitivePeerDependencies: - - supports-color - optional: true - socks@2.8.7: dependencies: ip-address: 10.1.0 @@ -11966,18 +11553,6 @@ snapshots: split2@4.2.0: {} - sqlite3@5.1.7: - dependencies: - bindings: 1.5.0 - node-addon-api: 7.1.1 - prebuild-install: 7.1.3 - tar: 7.5.10 - optionalDependencies: - node-gyp: 8.4.1 - transitivePeerDependencies: - - bluebird - - supports-color - sqlstring-sqlite@0.1.1: {} sqlstring@2.3.3: {} @@ -11994,11 +11569,6 @@ snapshots: safer-buffer: 2.1.2 tweetnacl: 0.14.5 - ssri@8.0.1: - dependencies: - minipass: 3.3.6 - optional: true - stack-trace@0.0.10: {} stackback@0.0.2: {} @@ -12513,16 +12083,6 @@ snapshots: uniq@1.0.1: {} - unique-filename@1.1.1: - dependencies: - unique-slug: 2.0.2 - optional: true - - unique-slug@2.0.2: - dependencies: - imurmurhash: 0.1.4 - optional: true - unique-string@2.0.0: dependencies: crypto-random-string: 2.0.0 @@ -12811,11 +12371,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wide-align@1.1.5: - dependencies: - string-width: 4.2.3 - optional: true - widest-line@3.1.0: dependencies: string-width: 4.2.3 @@ -12910,9 +12465,6 @@ snapshots: yallist@3.1.1: {} - yallist@4.0.0: - optional: true - yallist@5.0.0: {} yaml@2.8.2: {} diff --git a/src/charging-station/Bootstrap.ts b/src/charging-station/Bootstrap.ts index 65831ac5..39bf5fbc 100644 --- a/src/charging-station/Bootstrap.ts +++ b/src/charging-station/Bootstrap.ts @@ -219,7 +219,7 @@ export class Bootstrap extends EventEmitter { performanceStorageConfiguration.uri!, this.logPrefix() ) - await this.storage?.open() + await this.storage.open() } this.uiServer.setChargingStationTemplates( Configuration.getStationTemplateUrls()?.map(stationTemplateUrl => diff --git a/src/performance/storage/JsonFileStorage.ts b/src/performance/storage/JsonFileStorage.ts index 6103235e..ee145879 100644 --- a/src/performance/storage/JsonFileStorage.ts +++ b/src/performance/storage/JsonFileStorage.ts @@ -1,7 +1,6 @@ // Copyright Jerome Benoit. 2021-2025. All Rights Reserved. -import { closeSync, existsSync, mkdirSync, openSync, writeSync } from 'node:fs' -import { dirname } from 'node:path' +import { closeSync, openSync, writeSync } from 'node:fs' import { BaseError } from '../../exception/index.js' import { FileType, MapStringifyFormat, type Statistics } from '../../types/index.js' @@ -36,9 +35,7 @@ export class JsonFileStorage extends Storage { public open (): void { try { if (this.fd == null) { - if (!existsSync(dirname(this.dbName))) { - mkdirSync(dirname(this.dbName), { recursive: true }) - } + this.ensureDBDirectory() this.fd = openSync(this.dbName, 'w') } } catch (error) { @@ -53,11 +50,10 @@ export class JsonFileStorage extends Storage { public storePerformanceStatistics (performanceStatistics: Statistics): void { this.setPerformanceStatistics(performanceStatistics) - this.checkPerformanceRecordsFile() + const fd = this.checkPerformanceRecordsFile() AsyncLock.runExclusive(AsyncLockType.performance, () => { writeSync( - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.fd!, + fd, JSONStringify([...this.getPerformanceStatistics()], 2, MapStringifyFormat.object), 0, 'utf8' @@ -72,11 +68,12 @@ export class JsonFileStorage extends Storage { }) } - private checkPerformanceRecordsFile (): void { + private checkPerformanceRecordsFile (): number { if (this.fd == null) { throw new BaseError( `${this.logPrefix} Performance records '${this.dbName}' file descriptor not found` ) } + return this.fd } } diff --git a/src/performance/storage/MikroOrmStorage.ts b/src/performance/storage/MikroOrmStorage.ts index 1a1d0d97..42da83d3 100644 --- a/src/performance/storage/MikroOrmStorage.ts +++ b/src/performance/storage/MikroOrmStorage.ts @@ -1,9 +1,10 @@ // Copyright Jerome Benoit. 2021-2025. All Rights Reserved. +import { type Options as SqliteOptions, MikroORM as SqliteORM } from '@mikro-orm/better-sqlite' import { type Options as MariaDbOptions, MikroORM as MariaDbORM } from '@mikro-orm/mariadb' -import { type Options as SqliteOptions, MikroORM as SqliteORM } from '@mikro-orm/sqlite' -import { type PerformanceRecord, type Statistics, StorageType } from '../../types/index.js' +import { BaseError } from '../../exception/index.js' +import { PerformanceRecord, type Statistics, StorageType } from '../../types/index.js' import { Constants } from '../../utils/index.js' import { Storage } from './Storage.js' @@ -32,15 +33,26 @@ export class MikroOrmStorage extends Storage { public async open (): Promise { try { if (this.orm == null) { + let orm: MariaDbORM | SqliteORM | undefined switch (this.storageType) { case StorageType.MARIA_DB: case StorageType.MYSQL: - this.orm = await MariaDbORM.init(this.getOptions() as MariaDbOptions) + orm = await MariaDbORM.init(this.getOptions() as MariaDbOptions) break case StorageType.SQLITE: - this.orm = await SqliteORM.init(this.getOptions() as SqliteOptions) + this.ensureDBDirectory() + orm = await SqliteORM.init(this.getOptions() as SqliteOptions) break } + if (orm != null) { + try { + await orm.schema.updateSchema() + } catch (error) { + await orm.close() + throw error + } + this.orm = orm + } } } catch (error) { this.handleDBStorageError(this.storageType, error as Error) @@ -50,15 +62,12 @@ export class MikroOrmStorage extends Storage { public async storePerformanceStatistics (performanceStatistics: Statistics): Promise { try { this.setPerformanceStatistics(performanceStatistics) - await this.orm?.em.upsert({ - ...performanceStatistics, - statisticsData: Array.from(performanceStatistics.statisticsData, ([name, value]) => ({ - ...value, - measurementTimeSeries: - value.measurementTimeSeries != null ? [...value.measurementTimeSeries] : undefined, - name, - })), - } satisfies PerformanceRecord) + this.checkDBConnection() + const em = this.orm?.em.fork() + await em?.upsert( + PerformanceRecord, + this.serializePerformanceStatistics(performanceStatistics) + ) } catch (error) { this.handleDBStorageError( this.storageType, @@ -68,6 +77,14 @@ export class MikroOrmStorage extends Storage { } } + private checkDBConnection (): void { + if (this.orm == null) { + throw new BaseError( + `${this.logPrefix} ${this.getDBNameFromStorageType(this.storageType) ?? 'Unknown'} ORM not initialized while trying to issue a request` + ) + } + } + private getClientUrl (): string | undefined { switch (this.storageType) { case StorageType.MARIA_DB: @@ -88,8 +105,7 @@ export class MikroOrmStorage extends Storage { return { clientUrl: this.getClientUrl(), dbName: this.dbName, - entities: ['./dist/types/orm/entities/*.js'], - entitiesTs: ['./src/types/orm/entities/*.ts'], + entities: [PerformanceRecord], } } } diff --git a/src/performance/storage/MongoDBStorage.ts b/src/performance/storage/MongoDBStorage.ts index f11e8f25..0dc033d2 100644 --- a/src/performance/storage/MongoDBStorage.ts +++ b/src/performance/storage/MongoDBStorage.ts @@ -8,22 +8,22 @@ import { Constants } from '../../utils/index.js' import { Storage } from './Storage.js' export class MongoDBStorage extends Storage { - private readonly client?: MongoClient - private connected: boolean + private readonly client: MongoClient + private opened: boolean constructor (storageUri: string, logPrefix: string) { super(storageUri, logPrefix) this.client = new MongoClient(this.storageUri.toString()) - this.connected = false + this.opened = false this.dbName = this.storageUri.pathname.replace(/(?:^\/)|(?:\/$)/g, '') } public async close (): Promise { this.clearPerformanceStatistics() try { - if (this.connected && this.client != null) { + if (this.opened) { await this.client.close() - this.connected = false + this.opened = false } } catch (error) { this.handleDBStorageError(StorageType.MONGO_DB, error as Error) @@ -32,9 +32,9 @@ export class MongoDBStorage extends Storage { public async open (): Promise { try { - if (!this.connected && this.client != null) { + if (!this.opened) { await this.client.connect() - this.connected = true + this.opened = true } } catch (error) { this.handleDBStorageError(StorageType.MONGO_DB, error as Error) @@ -46,11 +46,13 @@ export class MongoDBStorage extends Storage { this.setPerformanceStatistics(performanceStatistics) this.checkDBConnection() await this.client - ?.db(this.dbName) + .db(this.dbName) .collection(Constants.PERFORMANCE_RECORDS_TABLE) - .replaceOne({ id: performanceStatistics.id }, performanceStatistics, { - upsert: true, - }) + .replaceOne( + { id: performanceStatistics.id }, + this.serializePerformanceStatistics(performanceStatistics) as unknown as Statistics, + { upsert: true } + ) } catch (error) { this.handleDBStorageError( StorageType.MONGO_DB, @@ -61,20 +63,9 @@ export class MongoDBStorage extends Storage { } private checkDBConnection (): void { - if (this.client == null) { + if (!this.opened) { throw new BaseError( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${this.logPrefix} ${this.getDBNameFromStorageType( - StorageType.MONGO_DB - )} client initialization failed while trying to issue a request` - ) - } - if (!this.connected) { - throw new BaseError( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${this.logPrefix} ${this.getDBNameFromStorageType( - StorageType.MONGO_DB - )} connection not opened while trying to issue a request` + `${this.logPrefix} ${this.getDBNameFromStorageType(StorageType.MONGO_DB) ?? 'Unknown'} connection not opened while trying to issue a request` ) } } diff --git a/src/performance/storage/None.ts b/src/performance/storage/None.ts index ceda1ab5..4c03bdcf 100644 --- a/src/performance/storage/None.ts +++ b/src/performance/storage/None.ts @@ -1,4 +1,4 @@ -// Copyright Jerome Benoit. 2021-2024. All Rights Reserved. +// Copyright Jerome Benoit. 2021-2025. All Rights Reserved. import type { Statistics } from '../../types/index.js' diff --git a/src/performance/storage/Storage.ts b/src/performance/storage/Storage.ts index b7711f21..22b91990 100644 --- a/src/performance/storage/Storage.ts +++ b/src/performance/storage/Storage.ts @@ -1,5 +1,7 @@ // Copyright Jerome Benoit. 2021-2025. All Rights Reserved. +import { existsSync, mkdirSync } from 'node:fs' +import { dirname } from 'node:path' import { URL } from 'node:url' import { @@ -38,6 +40,12 @@ export abstract class Storage { Storage.performanceStatistics.clear() } + protected ensureDBDirectory (): void { + if (!existsSync(dirname(this.dbName))) { + mkdirSync(dirname(this.dbName), { recursive: true }) + } + } + protected getDBNameFromStorageType (type: StorageType): DBName | undefined { switch (type) { case StorageType.MARIA_DB: @@ -60,20 +68,9 @@ export abstract class Storage { throwError: false, } ): void { - params = { - ...{ - consoleOut: false, - throwError: false, - }, - ...params, - } - const inTableOrCollectionStr = table != null && ` in table or collection '${table}'` + const inTableOrCollectionStr = table != null ? ` in table or collection '${table}'` : '' logger.error( - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `${this.logPrefix} ${this.getDBNameFromStorageType(type)} error '${ - error.message - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - }'${inTableOrCollectionStr}:`, + `${this.logPrefix} ${this.getDBNameFromStorageType(type) ?? 'Unknown'} error '${error.message}'${inTableOrCollectionStr}:`, error ) if (params.throwError === true) { @@ -81,6 +78,20 @@ export abstract class Storage { } } + protected serializePerformanceStatistics ( + performanceStatistics: Statistics + ): Record { + return { + ...performanceStatistics, + statisticsData: Array.from(performanceStatistics.statisticsData, ([name, value]) => ({ + ...value, + measurementTimeSeries: + value.measurementTimeSeries != null ? [...value.measurementTimeSeries] : undefined, + name, + })), + } + } + protected setPerformanceStatistics (performanceStatistics: Statistics): void { Storage.performanceStatistics.set(performanceStatistics.id, performanceStatistics) } diff --git a/src/performance/storage/StorageFactory.ts b/src/performance/storage/StorageFactory.ts index c7d24373..f6e0bd9a 100644 --- a/src/performance/storage/StorageFactory.ts +++ b/src/performance/storage/StorageFactory.ts @@ -15,11 +15,7 @@ export class StorageFactory { // This is intentional } - public static getStorage ( - type: StorageType, - connectionUri: string, - logPrefix: string - ): Storage | undefined { + public static getStorage (type: StorageType, connectionUri: string, logPrefix: string): Storage { let storageInstance: Storage switch (type) { case StorageType.JSON_FILE: @@ -37,8 +33,7 @@ export class StorageFactory { storageInstance = new None() break default: - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - throw new BaseError(`${logPrefix} Unknown storage type: ${type}`) + throw new BaseError(`${logPrefix} Unknown storage type: ${type as string}`) } return storageInstance } diff --git a/src/types/orm/entities/PerformanceRecord.ts b/src/types/orm/entities/PerformanceRecord.ts index 8f9b9204..360cb2b2 100644 --- a/src/types/orm/entities/PerformanceRecord.ts +++ b/src/types/orm/entities/PerformanceRecord.ts @@ -1,42 +1,22 @@ import { Entity, PrimaryKey, Property } from '@mikro-orm/core' -interface StatisticsData { - avgTimeMeasurement: number - currentTimeMeasurement: number - errorCount: number - maxTimeMeasurement: number - measurementTimeSeries: { - timestamp: number - value: number - }[] - medTimeMeasurement: number - minTimeMeasurement: number - name: string - ninetyFiveThPercentileTimeMeasurement: number - requestCount: number - responseCount: number - stdTimeMeasurement: number - timeMeasurementCount: number - totalTimeMeasurement: number -} - -@Entity() +@Entity({ tableName: 'performance_records' }) export class PerformanceRecord { - @Property() + @Property({ type: 'datetime' }) createdAt!: Date - @PrimaryKey() + @PrimaryKey({ type: 'string' }) id!: string - @Property() + @Property({ type: 'string' }) name!: string - @Property() - statisticsData!: Partial[] + @Property({ type: 'json' }) + statisticsData!: Record[] - @Property() + @Property({ nullable: true, type: 'datetime' }) updatedAt?: Date - @Property() + @Property({ type: 'string' }) uri!: string } diff --git a/tests/performance/storage/MikroOrmStorage.test.ts b/tests/performance/storage/MikroOrmStorage.test.ts new file mode 100644 index 00000000..1f92edb9 --- /dev/null +++ b/tests/performance/storage/MikroOrmStorage.test.ts @@ -0,0 +1,446 @@ +/** + * @file Tests for MikroOrmStorage + * @description Unit and integration tests for MikroORM-based performance storage + */ +import { MikroORM } from '@mikro-orm/better-sqlite' +import { expect } from '@std/expect' +import { existsSync, rmSync } from 'node:fs' +import { afterEach, beforeEach, describe, it } from 'node:test' + +import { MikroOrmStorage } from '../../../src/performance/storage/MikroOrmStorage.js' +import { StorageType } from '../../../src/types/index.js' +import { PerformanceRecord } from '../../../src/types/orm/entities/PerformanceRecord.js' +import { Constants } from '../../../src/utils/index.js' +import { logger } from '../../../src/utils/Logger.js' +import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js' +import { buildTestStatistics } from './StorageTestHelpers.js' + +const TEST_LOG_PREFIX = '[MikroOrmStorage Test]' +const TEST_STORAGE_URI = 'file:performance/e-mobility-charging-stations-simulator.db' +const TEST_DB_PATH = `${Constants.DEFAULT_PERFORMANCE_DIRECTORY}/${Constants.DEFAULT_PERFORMANCE_RECORDS_DB_NAME}.db` + +let sqliteAvailable = false +try { + const orm = await MikroORM.init({ + dbName: ':memory:', + discovery: { warnWhenNoEntities: false }, + entities: [], + }) + await orm.close() + sqliteAvailable = true +} catch { + // better-sqlite3 native binding not available (CI --ignore-scripts) +} + +const SKIP_SQLITE = !sqliteAvailable ? 'better-sqlite3 native binding not available' : undefined + +interface MockEntityManager { + fork: () => MockEntityManager + upsert: (entity: unknown, data: unknown) => Promise +} + +interface MockOrm { + close: () => Promise + em: MockEntityManager + schema: { + updateSchema: () => Promise + } +} + +class TestableMikroOrmStorage extends MikroOrmStorage { + public getOrm (): unknown { + return Reflect.get(this, 'orm') + } + + public setOrm (orm: MockOrm): void { + Reflect.set(this, 'orm', orm) + } +} + +/** + * @returns Mock ORM instance and captured upsert calls + */ +function buildMockOrm (): { mockOrm: MockOrm; upsertCalls: unknown[] } { + const upsertCalls: unknown[] = [] + const mockEm: MockEntityManager = { + fork: () => mockEm, + upsert: (_entity: unknown, data: unknown) => { + upsertCalls.push({ data, entity: _entity }) + return Promise.resolve(data) + }, + } + const mockOrm: MockOrm = { + close: () => Promise.resolve(), + em: mockEm, + schema: { + updateSchema: () => Promise.resolve(), + }, + } + return { mockOrm, upsertCalls } +} + +await describe('MikroOrmStorage', async () => { + let storage: TestableMikroOrmStorage + + beforeEach(() => { + storage = new TestableMikroOrmStorage(TEST_STORAGE_URI, TEST_LOG_PREFIX, StorageType.SQLITE) + }) + + afterEach(async () => { + try { + await storage.close() + } catch { + // Storage may not have been opened + } + if (existsSync(TEST_DB_PATH)) { + rmSync(TEST_DB_PATH) + } + standardCleanup() + }) + + await describe('close', async () => { + await it('should clear cached performance statistics', async () => { + // Arrange + const { mockOrm } = buildMockOrm() + storage.setOrm(mockOrm) + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + expect([...storage.getPerformanceStatistics()].length).toBe(1) + + // Act + await storage.close() + + // Assert + expect([...storage.getPerformanceStatistics()].length).toBe(0) + }) + + await it('should call orm.close when ORM is initialized', async t => { + // Arrange + const { mockOrm } = buildMockOrm() + const closeMock = t.mock.method(mockOrm, 'close') + storage.setOrm(mockOrm) + + // Act + await storage.close() + + // Assert + expect(closeMock.mock.calls.length).toBe(1) + }) + + await it('should delete orm reference after closing', async () => { + // Arrange + const { mockOrm } = buildMockOrm() + storage.setOrm(mockOrm) + + // Act + await storage.close() + + // Assert + expect(storage.getOrm()).toBeUndefined() + }) + + await it('should not fail when closing without prior open', async () => { + await storage.close() + await storage.close() + }) + + await it('should log error when orm.close throws', async t => { + // Arrange + const errorMock = t.mock.method(logger, 'error') + const failingOrm: MockOrm = { + close: () => Promise.reject(new Error('close failed')), + em: { fork: () => ({}) as MockEntityManager, upsert: () => Promise.resolve({}) }, + schema: { updateSchema: () => Promise.resolve() }, + } + storage.setOrm(failingOrm) + + // Act + await storage.close() + + // Assert + expect(errorMock.mock.calls.length).toBe(1) + }) + }) + + await describe('storePerformanceStatistics', async () => { + await it('should transform statisticsData map to array with name field', async () => { + // Arrange + const { mockOrm, upsertCalls } = buildMockOrm() + storage.setOrm(mockOrm) + const stats = buildTestStatistics('station-1') + + // Act + await storage.storePerformanceStatistics(stats) + + // Assert + expect(upsertCalls.length).toBe(1) + const call = upsertCalls[0] as { data: Record; entity: unknown } + expect(call.entity).toBe(PerformanceRecord) + const statsArray = call.data.statisticsData as Record[] + expect(Array.isArray(statsArray)).toBe(true) + expect(statsArray.length).toBe(1) + expect(statsArray[0].name).toBe('Heartbeat') + expect(statsArray[0].requestCount).toBe(100) + expect(statsArray[0].avgTimeMeasurement).toBe(10.5) + }) + + await it('should spread measurementTimeSeries into plain array', async () => { + // Arrange + const { mockOrm, upsertCalls } = buildMockOrm() + storage.setOrm(mockOrm) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const call = upsertCalls[0] as { data: Record } + const statsArray = call.data.statisticsData as Record[] + const timeSeries = statsArray[0].measurementTimeSeries as unknown[] + expect(Array.isArray(timeSeries)).toBe(true) + expect(timeSeries.length).toBe(2) + }) + + await it('should call upsert with PerformanceRecord entity class', async () => { + // Arrange + const { mockOrm, upsertCalls } = buildMockOrm() + storage.setOrm(mockOrm) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const call = upsertCalls[0] as { entity: unknown } + expect(call.entity).toBe(PerformanceRecord) + }) + + await it('should cache statistics in memory after store', async () => { + // Arrange + const { mockOrm } = buildMockOrm() + storage.setOrm(mockOrm) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const cached = [...storage.getPerformanceStatistics()] + expect(cached.length).toBe(1) + expect(cached[0].id).toBe('station-1') + }) + + await it('should handle multiple distinct records', async () => { + // Arrange + const { mockOrm, upsertCalls } = buildMockOrm() + storage.setOrm(mockOrm) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + await storage.storePerformanceStatistics(buildTestStatistics('station-2')) + await storage.storePerformanceStatistics(buildTestStatistics('station-3')) + + // Assert + expect(upsertCalls.length).toBe(3) + const cached = [...storage.getPerformanceStatistics()] + expect(cached.length).toBe(3) + }) + + await it('should handle statisticsData entry without measurementTimeSeries', async () => { + // Arrange + const { mockOrm, upsertCalls } = buildMockOrm() + storage.setOrm(mockOrm) + const stats = buildTestStatistics('station-1') + stats.statisticsData.set('StatusNotification', { + requestCount: 50, + responseCount: 50, + } as unknown as Record) + + // Act + await storage.storePerformanceStatistics(stats) + + // Assert + const call = upsertCalls[0] as { data: Record } + const statsArray = call.data.statisticsData as Record[] + expect(statsArray.length).toBe(2) + const statusEntry = statsArray.find(e => e.name === 'StatusNotification') + expect(statusEntry).toBeDefined() + expect(statusEntry?.measurementTimeSeries).toBeUndefined() + }) + }) + + await describe('Error Handling', async () => { + await it('should log error when storing without open', async t => { + // Arrange + const errorMock = t.mock.method(logger, 'error') + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + expect(errorMock.mock.calls.length).toBe(1) + }) + + await it('should still cache statistics even when store fails', async () => { + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const cached = [...storage.getPerformanceStatistics()] + expect(cached.length).toBe(1) + expect(cached[0].id).toBe('station-1') + }) + + await it('should log error when upsert throws', async t => { + // Arrange + const errorMock = t.mock.method(logger, 'error') + const failingEm: MockEntityManager = { + fork: () => failingEm, + upsert: () => Promise.reject(new Error('upsert failed')), + } + const failingOrm: MockOrm = { + close: () => Promise.resolve(), + em: failingEm, + schema: { updateSchema: () => Promise.resolve() }, + } + storage.setOrm(failingOrm) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + expect(errorMock.mock.calls.length).toBe(1) + }) + }) + + await describe('Integration (SQLite)', async () => { + await it('should open database and create schema', { skip: SKIP_SQLITE }, async () => { + await storage.open() + + expect(existsSync(TEST_DB_PATH)).toBe(true) + }) + + await it('should persist record to SQLite database', { skip: SKIP_SQLITE }, async () => { + // Arrange + await storage.open() + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const verifyOrm = await MikroORM.init({ + dbName: TEST_DB_PATH, + entities: [PerformanceRecord], + }) + try { + const record = await verifyOrm.em.fork().findOne(PerformanceRecord, { id: 'station-1' }) + expect(record).toBeDefined() + expect(record?.name).toBe('cs-station-1') + expect(record?.uri).toBe('ws://localhost:8080') + } finally { + await verifyOrm.close() + } + }) + + await it('should upsert existing record with same id', { skip: SKIP_SQLITE }, async () => { + // Arrange + await storage.open() + await storage.storePerformanceStatistics(buildTestStatistics('station-1', 'original')) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1', 'updated')) + + // Assert + const verifyOrm = await MikroORM.init({ + dbName: TEST_DB_PATH, + entities: [PerformanceRecord], + }) + try { + const records = await verifyOrm.em.fork().findAll(PerformanceRecord) + expect(records.length).toBe(1) + expect(records[0].name).toBe('updated') + } finally { + await verifyOrm.close() + } + }) + + await it( + 'should serialize statisticsData as JSON array with name field', + { skip: SKIP_SQLITE }, + async () => { + // Arrange + await storage.open() + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const verifyOrm = await MikroORM.init({ + dbName: TEST_DB_PATH, + entities: [PerformanceRecord], + }) + try { + const record = await verifyOrm.em.fork().findOne(PerformanceRecord, { id: 'station-1' }) + expect(record).toBeDefined() + expect(Array.isArray(record?.statisticsData)).toBe(true) + expect(record?.statisticsData.length).toBe(1) + const entry = record?.statisticsData[0] + expect(entry).toBeDefined() + expect(entry?.name).toBe('Heartbeat') + expect(entry?.requestCount).toBe(100) + } finally { + await verifyOrm.close() + } + } + ) + + await it('should persist data across close and reopen', { skip: SKIP_SQLITE }, async () => { + // Arrange + await storage.open() + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + await storage.close() + + // Act + const freshStorage = new TestableMikroOrmStorage( + TEST_STORAGE_URI, + TEST_LOG_PREFIX, + StorageType.SQLITE + ) + await freshStorage.open() + + // Assert + const verifyOrm = await MikroORM.init({ + dbName: TEST_DB_PATH, + entities: [PerformanceRecord], + }) + try { + const record = await verifyOrm.em.fork().findOne(PerformanceRecord, { id: 'station-1' }) + expect(record).toBeDefined() + expect(record?.name).toBe('cs-station-1') + } finally { + await verifyOrm.close() + await freshStorage.close() + } + }) + + await it('should store multiple distinct records', { skip: SKIP_SQLITE }, async () => { + // Arrange + await storage.open() + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + await storage.storePerformanceStatistics(buildTestStatistics('station-2')) + await storage.storePerformanceStatistics(buildTestStatistics('station-3')) + + // Assert + const verifyOrm = await MikroORM.init({ + dbName: TEST_DB_PATH, + entities: [PerformanceRecord], + }) + try { + const records = await verifyOrm.em.fork().findAll(PerformanceRecord) + expect(records.length).toBe(3) + const ids = records.map(r => r.id).sort() + expect(ids).toStrictEqual(['station-1', 'station-2', 'station-3']) + } finally { + await verifyOrm.close() + } + }) + }) +}) diff --git a/tests/performance/storage/MongoDBStorage.test.ts b/tests/performance/storage/MongoDBStorage.test.ts new file mode 100644 index 00000000..1ca2984d --- /dev/null +++ b/tests/performance/storage/MongoDBStorage.test.ts @@ -0,0 +1,411 @@ +/** + * @file Tests for MongoDBStorage + * @description Unit tests for MongoDB-based performance storage + */ +import { expect } from '@std/expect' +import { afterEach, beforeEach, describe, it } from 'node:test' + +import { MongoDBStorage } from '../../../src/performance/storage/MongoDBStorage.js' +import { Constants } from '../../../src/utils/index.js' +import { logger } from '../../../src/utils/Logger.js' +import { standardCleanup } from '../../helpers/TestLifecycleHelpers.js' +import { buildTestStatistics } from './StorageTestHelpers.js' + +const TEST_LOG_PREFIX = '[MongoDBStorage Test]' +const TEST_STORAGE_URI = 'mongodb://localhost:27017/e-mobility-test' + +interface MockCollection { + replaceOne: ( + filter: Record, + replacement: unknown, + options: Record + ) => Promise +} + +interface MockDb { + collection: (name: string) => MockCollection +} + +interface MockMongoClient { + close: () => Promise + connect: () => Promise + db: (name: string) => MockDb +} + +class TestableMongoDBStorage extends MongoDBStorage { + public getClient (): unknown { + return Reflect.get(this, 'client') + } + + public getOpened (): boolean { + return Reflect.get(this, 'opened') as boolean + } + + public setClient (client: MockMongoClient): void { + Reflect.set(this, 'client', client) + } + + public setOpened (opened: boolean): void { + Reflect.set(this, 'opened', opened) + } +} + +/** + * @returns Mock MongoDB client and captured call logs + */ +function buildMockMongoClient (): { + collectionCalls: { collectionName: string }[] + mockClient: MockMongoClient + replaceOneCalls: { + filter: Record + options: Record + replacement: unknown + }[] +} { + const replaceOneCalls: { + filter: Record + options: Record + replacement: unknown + }[] = [] + const collectionCalls: { collectionName: string }[] = [] + const mockCollection: MockCollection = { + replaceOne: ( + filter: Record, + replacement: unknown, + options: Record + ) => { + replaceOneCalls.push({ filter, options, replacement }) + return Promise.resolve({ acknowledged: true, modifiedCount: 1 }) + }, + } + const mockDb: MockDb = { + collection: (name: string) => { + collectionCalls.push({ collectionName: name }) + return mockCollection + }, + } + const mockClient: MockMongoClient = { + close: () => Promise.resolve(), + connect: () => Promise.resolve(), + db: (_name: string) => mockDb, + } + return { collectionCalls, mockClient, replaceOneCalls } +} + +await describe('MongoDBStorage', async () => { + let storage: TestableMongoDBStorage + + beforeEach(() => { + storage = new TestableMongoDBStorage(TEST_STORAGE_URI, TEST_LOG_PREFIX) + }) + + afterEach(async () => { + try { + await storage.close() + } catch { + // Storage may not have been opened + } + standardCleanup() + }) + + await describe('constructor', async () => { + await it('should extract database name from URI path', () => { + const dbName = Reflect.get(storage, 'dbName') as string + expect(dbName).toBe('e-mobility-test') + }) + + await it('should initialize with opened set to false', () => { + expect(storage.getOpened()).toBe(false) + }) + }) + + await describe('close', async () => { + await it('should clear cached performance statistics', async () => { + // Arrange + const { mockClient } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + expect([...storage.getPerformanceStatistics()].length).toBe(1) + + // Act + await storage.close() + + // Assert + expect([...storage.getPerformanceStatistics()].length).toBe(0) + }) + + await it('should call client.close when opened', async t => { + // Arrange + const { mockClient } = buildMockMongoClient() + const closeMock = t.mock.method(mockClient, 'close') + storage.setClient(mockClient) + storage.setOpened(true) + + // Act + await storage.close() + + // Assert + expect(closeMock.mock.calls.length).toBe(1) + }) + + await it('should set opened to false after closing', async () => { + // Arrange + const { mockClient } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + + // Act + await storage.close() + + // Assert + expect(storage.getOpened()).toBe(false) + }) + + await it('should not call client.close when not opened', async t => { + // Arrange + const { mockClient } = buildMockMongoClient() + const closeMock = t.mock.method(mockClient, 'close') + storage.setClient(mockClient) + + // Act + await storage.close() + + // Assert + expect(closeMock.mock.calls.length).toBe(0) + }) + + await it('should log error when client.close throws', async t => { + // Arrange + const errorMock = t.mock.method(logger, 'error') + const failingClient: MockMongoClient = { + close: () => Promise.reject(new Error('close failed')), + connect: () => Promise.resolve(), + db: () => ({}) as MockDb, + } + storage.setClient(failingClient) + storage.setOpened(true) + + // Act + await storage.close() + + // Assert + expect(errorMock.mock.calls.length).toBe(1) + }) + }) + + await describe('open', async () => { + await it('should call client.connect', async t => { + // Arrange + const { mockClient } = buildMockMongoClient() + const connectMock = t.mock.method(mockClient, 'connect') + storage.setClient(mockClient) + + // Act + await storage.open() + + // Assert + expect(connectMock.mock.calls.length).toBe(1) + expect(storage.getOpened()).toBe(true) + }) + + await it('should not connect when already opened', async t => { + // Arrange + const { mockClient } = buildMockMongoClient() + const connectMock = t.mock.method(mockClient, 'connect') + storage.setClient(mockClient) + storage.setOpened(true) + + // Act + await storage.open() + + // Assert + expect(connectMock.mock.calls.length).toBe(0) + }) + + await it('should log error when connect throws', async t => { + // Arrange + const errorMock = t.mock.method(logger, 'error') + const failingClient: MockMongoClient = { + close: () => Promise.resolve(), + connect: () => Promise.reject(new Error('connect failed')), + db: () => ({}) as MockDb, + } + storage.setClient(failingClient) + + // Act + await storage.open() + + // Assert + expect(errorMock.mock.calls.length).toBe(1) + }) + }) + + await describe('storePerformanceStatistics', async () => { + await it('should serialize statisticsData map to array with name field', async () => { + // Arrange + const { mockClient, replaceOneCalls } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + const stats = buildTestStatistics('station-1') + + // Act + await storage.storePerformanceStatistics(stats) + + // Assert + expect(replaceOneCalls.length).toBe(1) + const replacement = replaceOneCalls[0].replacement as Record + const statsArray = replacement.statisticsData as Record[] + expect(Array.isArray(statsArray)).toBe(true) + expect(statsArray.length).toBe(1) + expect(statsArray[0].name).toBe('Heartbeat') + expect(statsArray[0].requestCount).toBe(100) + expect(statsArray[0].avgTimeMeasurement).toBe(10.5) + }) + + await it('should spread measurementTimeSeries into plain array', async () => { + // Arrange + const { mockClient, replaceOneCalls } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const replacement = replaceOneCalls[0].replacement as Record + const statsArray = replacement.statisticsData as Record[] + const timeSeries = statsArray[0].measurementTimeSeries as unknown[] + expect(Array.isArray(timeSeries)).toBe(true) + expect(timeSeries.length).toBe(2) + }) + + await it('should call replaceOne with upsert and correct filter', async () => { + // Arrange + const { mockClient, replaceOneCalls } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + expect(replaceOneCalls.length).toBe(1) + expect(replaceOneCalls[0].filter).toStrictEqual({ id: 'station-1' }) + expect(replaceOneCalls[0].options).toStrictEqual({ upsert: true }) + }) + + await it('should use correct collection name', async () => { + // Arrange + const { collectionCalls, mockClient } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + expect(collectionCalls.length).toBe(1) + expect(collectionCalls[0].collectionName).toBe(Constants.PERFORMANCE_RECORDS_TABLE) + }) + + await it('should cache statistics in memory after store', async () => { + // Arrange + const { mockClient } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const cached = [...storage.getPerformanceStatistics()] + expect(cached.length).toBe(1) + expect(cached[0].id).toBe('station-1') + }) + + await it('should handle multiple distinct records', async () => { + // Arrange + const { mockClient, replaceOneCalls } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + await storage.storePerformanceStatistics(buildTestStatistics('station-2')) + await storage.storePerformanceStatistics(buildTestStatistics('station-3')) + + // Assert + expect(replaceOneCalls.length).toBe(3) + const cached = [...storage.getPerformanceStatistics()] + expect(cached.length).toBe(3) + }) + + await it('should handle statisticsData entry without measurementTimeSeries', async () => { + // Arrange + const { mockClient, replaceOneCalls } = buildMockMongoClient() + storage.setClient(mockClient) + storage.setOpened(true) + const stats = buildTestStatistics('station-1') + stats.statisticsData.set('StatusNotification', { + requestCount: 50, + responseCount: 50, + } as unknown as Record) + + // Act + await storage.storePerformanceStatistics(stats) + + // Assert + const replacement = replaceOneCalls[0].replacement as Record + const statsArray = replacement.statisticsData as Record[] + expect(statsArray.length).toBe(2) + const statusEntry = statsArray.find(e => e.name === 'StatusNotification') + expect(statusEntry).toBeDefined() + expect(statusEntry?.measurementTimeSeries).toBeUndefined() + }) + }) + + await describe('Error Handling', async () => { + await it('should log error when storing without open', async t => { + // Arrange + const errorMock = t.mock.method(logger, 'error') + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + expect(errorMock.mock.calls.length).toBe(1) + }) + + await it('should still cache statistics even when store fails', async () => { + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + const cached = [...storage.getPerformanceStatistics()] + expect(cached.length).toBe(1) + expect(cached[0].id).toBe('station-1') + }) + + await it('should log error when replaceOne throws', async t => { + // Arrange + const errorMock = t.mock.method(logger, 'error') + const failingCollection: MockCollection = { + replaceOne: () => Promise.reject(new Error('replaceOne failed')), + } + const failingClient: MockMongoClient = { + close: () => Promise.resolve(), + connect: () => Promise.resolve(), + db: () => ({ collection: () => failingCollection }) as unknown as MockDb, + } + storage.setClient(failingClient) + storage.setOpened(true) + + // Act + await storage.storePerformanceStatistics(buildTestStatistics('station-1')) + + // Assert + expect(errorMock.mock.calls.length).toBe(1) + }) + }) +}) diff --git a/tests/performance/storage/StorageTestHelpers.ts b/tests/performance/storage/StorageTestHelpers.ts new file mode 100644 index 00000000..79240f09 --- /dev/null +++ b/tests/performance/storage/StorageTestHelpers.ts @@ -0,0 +1,31 @@ +import type { Statistics } from '../../../src/types/index.js' + +/** + * @param id - Performance record identifier + * @param name - Charging station name + * @returns Statistics object with sample measurement data + */ +export function buildTestStatistics (id: string, name?: string): Statistics { + const statsData = new Map>() + statsData.set('Heartbeat', { + avgTimeMeasurement: 10.5, + currentTimeMeasurement: 12, + maxTimeMeasurement: 20, + measurementTimeSeries: [ + { timestamp: 1000, value: 10 }, + { timestamp: 2000, value: 12 }, + ], + minTimeMeasurement: 5, + requestCount: 100, + responseCount: 99, + timeMeasurementCount: 100, + totalTimeMeasurement: 1050, + }) + return { + createdAt: new Date('2025-01-01T00:00:00.000Z'), + id, + name: name ?? `cs-${id}`, + statisticsData: statsData, + uri: 'ws://localhost:8080', + } as unknown as Statistics +} -- 2.43.0