-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],
})
"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",
"tsx": "^4.21.0",
"typescript": "~5.9.3",
"vue-eslint-parser": "^10.4.0"
+ },
+ "pnpm": {
+ "onlyBuiltDependencies": [
+ "better-sqlite3"
+ ]
}
}
.:
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
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'
'@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'}
'@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'}
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==}
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==}
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==}
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}
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'}
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==}
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==}
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'}
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'}
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==}
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'}
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==}
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==}
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==}
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'}
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'}
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'}
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.
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'}
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'}
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==}
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'}
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}
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'}
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}
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==}
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==}
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'}
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'}
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'}
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==}
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'}
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'}
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'}
'@exodus/bytes@1.15.0': {}
- '@gar/promisify@1.1.3':
- optional: true
-
'@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.7':
'@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
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
- 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
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
'@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':
transitivePeerDependencies:
- encoding
- '@tootallnate/once@1.1.2':
- optional: true
-
'@ts-morph/common@0.28.1':
dependencies:
minimatch: 10.2.4
jsonparse: 1.3.1
through: 2.3.8
- abbrev@1.1.1:
- optional: true
-
abbrev@2.0.0: {}
abort-controller@3.0.0:
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
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: {}
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
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:
chownr@1.1.4: {}
- chownr@2.0.0:
- optional: true
-
chownr@3.0.0: {}
ci-info@2.0.0: {}
console-browserify@1.2.0: {}
- console-control-strings@1.1.0:
- optional: true
-
constants-browserify@1.0.0: {}
conventional-changelog-angular@8.2.0:
delayed-stream@1.0.0: {}
- delegates@1.0.0:
- optional: true
-
denque@2.1.0: {}
depd@2.0.0: {}
environment@1.1.0: {}
- err-code@2.0.3:
- optional: true
-
error-ex@1.3.4:
dependencies:
is-arrayish: 0.2.1
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:
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
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
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
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:
indent-string@4.0.0: {}
- infer-owner@1.0.4:
- optional: true
-
inflight@1.0.6:
dependencies:
once: 1.4.0
is-interactive@1.0.0: {}
- is-lambda@1.0.1:
- optional: true
-
is-map@2.0.3: {}
is-negative-zero@2.0.3: {}
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
tarn: 3.0.2
tildify: 2.0.0
optionalDependencies:
- sqlite3: 5.1.7
+ better-sqlite3: 11.10.0
transitivePeerDependencies:
- supports-color
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:
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:
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
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
dependencies:
semver: 7.7.4
- node-addon-api@7.1.1: {}
-
node-exports-info@1.6.0:
dependencies:
array.prototype.flatmap: 1.3.3
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
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
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
retimer@3.0.0: {}
- retry@0.12.0:
- optional: true
-
reusify@1.1.0: {}
rfdc@1.4.1: {}
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
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: {}
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: {}
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
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
yallist@3.1.1: {}
- yallist@4.0.0:
- optional: true
-
yallist@5.0.0: {}
yaml@2.8.2: {}
performanceStorageConfiguration.uri!,
this.logPrefix()
)
- await this.storage?.open()
+ await this.storage.open()
}
this.uiServer.setChargingStationTemplates(
Configuration.getStationTemplateUrls()?.map(stationTemplateUrl =>
// 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'
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) {
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'
})
}
- 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
}
}
// 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'
public async open (): Promise<void> {
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)
public async storePerformanceStatistics (performanceStatistics: Statistics): Promise<void> {
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,
}
}
+ 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:
return {
clientUrl: this.getClientUrl(),
dbName: this.dbName,
- entities: ['./dist/types/orm/entities/*.js'],
- entitiesTs: ['./src/types/orm/entities/*.ts'],
+ entities: [PerformanceRecord],
}
}
}
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<void> {
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)
public async open (): Promise<void> {
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)
this.setPerformanceStatistics(performanceStatistics)
this.checkDBConnection()
await this.client
- ?.db(this.dbName)
+ .db(this.dbName)
.collection<Statistics>(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,
}
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`
)
}
}
-// Copyright Jerome Benoit. 2021-2024. All Rights Reserved.
+// Copyright Jerome Benoit. 2021-2025. All Rights Reserved.
import type { Statistics } from '../../types/index.js'
// 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 {
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:
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) {
}
}
+ protected serializePerformanceStatistics (
+ performanceStatistics: Statistics
+ ): Record<string, unknown> {
+ 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)
}
// 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:
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
}
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<StatisticsData>[]
+ @Property({ type: 'json' })
+ statisticsData!: Record<string, unknown>[]
- @Property()
+ @Property({ nullable: true, type: 'datetime' })
updatedAt?: Date
- @Property()
+ @Property({ type: 'string' })
uri!: string
}
--- /dev/null
+/**
+ * @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<unknown>
+}
+
+interface MockOrm {
+ close: () => Promise<void>
+ em: MockEntityManager
+ schema: {
+ updateSchema: () => Promise<void>
+ }
+}
+
+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<string, unknown>; entity: unknown }
+ expect(call.entity).toBe(PerformanceRecord)
+ const statsArray = call.data.statisticsData as Record<string, unknown>[]
+ 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<string, unknown> }
+ const statsArray = call.data.statisticsData as Record<string, unknown>[]
+ 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<string, unknown>)
+
+ // Act
+ await storage.storePerformanceStatistics(stats)
+
+ // Assert
+ const call = upsertCalls[0] as { data: Record<string, unknown> }
+ const statsArray = call.data.statisticsData as Record<string, unknown>[]
+ 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()
+ }
+ })
+ })
+})
--- /dev/null
+/**
+ * @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<string, unknown>,
+ replacement: unknown,
+ options: Record<string, unknown>
+ ) => Promise<unknown>
+}
+
+interface MockDb {
+ collection: (name: string) => MockCollection
+}
+
+interface MockMongoClient {
+ close: () => Promise<void>
+ connect: () => Promise<void>
+ 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<string, unknown>
+ options: Record<string, unknown>
+ replacement: unknown
+ }[]
+} {
+ const replaceOneCalls: {
+ filter: Record<string, unknown>
+ options: Record<string, unknown>
+ replacement: unknown
+ }[] = []
+ const collectionCalls: { collectionName: string }[] = []
+ const mockCollection: MockCollection = {
+ replaceOne: (
+ filter: Record<string, unknown>,
+ replacement: unknown,
+ options: Record<string, unknown>
+ ) => {
+ 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<string, unknown>
+ const statsArray = replacement.statisticsData as Record<string, unknown>[]
+ 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<string, unknown>
+ const statsArray = replacement.statisticsData as Record<string, unknown>[]
+ 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<string, unknown>)
+
+ // Act
+ await storage.storePerformanceStatistics(stats)
+
+ // Assert
+ const replacement = replaceOneCalls[0].replacement as Record<string, unknown>
+ const statsArray = replacement.statisticsData as Record<string, unknown>[]
+ 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)
+ })
+ })
+})
--- /dev/null
+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<string, Record<string, unknown>>()
+ 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
+}