From 5edb0b4bf17f5ae46dc0c5a16e5bd39a53a25148 Mon Sep 17 00:00:00 2001 From: Tim Fabian Date: Sun, 4 Jan 2026 03:10:08 +0100 Subject: [PATCH 1/2] added automatic entity generation, now uses native tsc and includes src so errors lead back to the actual code --- .npmignore | 5 + docs/data-source.md | 2 +- eslint.config.mjs | 10 +- package-lock.json | 1886 +++++------------ package.json | 17 +- sandbox/src/data-sources/db/db.data-source.ts | 9 +- sandbox/src/index.ts | 2 +- sandbox/src/models/generated/index.ts | 1 + .../src/models/generated/petstore/index.ts | 8 + .../petstore/petstore.api-response.model.ts | 13 + .../petstore/petstore.category.model.ts | 10 + .../petstore/petstore.order.model.ts | 22 + .../generated/petstore/petstore.pet.model.ts | 24 + .../generated/petstore/petstore.tag.model.ts | 10 + .../petstore.update-pet-with-form.model.ts | 10 + .../petstore/petstore.upload-file.model.ts | 10 + .../generated/petstore/petstore.user.model.ts | 28 + sandbox/src/models/generated/providers.ts | 3 + sandbox/src/models/index.ts | 3 +- sandbox/webpack.config.js | 19 +- .../mocks/entities/child.entity.ts | 2 +- .../mocks/entities/parent.entity.ts | 2 +- .../mocks/entities/profile.entity.ts | 2 +- src/__testing__/mocks/entities/role.entity.ts | 2 +- src/__testing__/mocks/entities/user.entity.ts | 2 +- src/application-options.model.ts | 2 +- src/application.ts | 15 +- src/auth/strategies/jwt/jwt.auth-strategy.ts | 5 +- src/backup/backup-service.test.ts | 2 +- src/data-source/data-source.service.ts | 17 +- .../models/options/count-options.model.ts | 14 + .../models/options/find-all-options.model.ts | 25 +- .../models/options/find-one-options.model.ts | 5 +- src/data-source/models/options/index.ts | 3 +- .../generate-entity-file.function.ts | 230 ++ ...rate-entity-files-for-provider.function.ts | 194 ++ ...generate-entity-files-for-provider.test.ts | 474 +++++ .../generate-entity-files.function.ts | 63 + .../get-entity-file-name.function.ts | 11 + src/entity/generation/index.ts | 2 + .../entity-generation-provider.interface.ts | 19 + src/entity/generation/providers/index.ts | 3 + .../providers/open-api-file.provider.ts | 19 + .../providers/open-api-to-v3.function.ts | 37 + .../providers/open-api-url.provider.ts | 25 + src/entity/index.ts | 3 +- .../errors/missing-entities.error.ts | 2 +- src/http-client/http-client-response.model.ts | 5 +- src/http-client/http-client.interface.ts | 81 +- src/http-client/http-client.ts | 59 +- src/open-api/decorators/response.decorator.ts | 14 +- src/open-api/open-api.model.ts | 37 + src/open-api/open-api.service.ts | 21 +- src/open-api/pagination-result.model.ts | 24 +- src/plugin/invoicing/invoicing.plugin.ts | 9 +- .../services/invoice-number.service.ts | 3 +- src/routing/decorators/body.decorator.ts | 12 +- src/routing/models/crud-controller.model.ts | 4 +- src/routing/router.ts | 2 + .../add-import-statement.function.ts | 55 + src/utilities/index.ts | 5 +- src/utilities/to-camel-case.function.ts | 24 + src/utilities/to-kebab-case.function.ts | 19 + src/utilities/to-pascal-case.function.ts | 12 + src/validation/validation.service.ts | 26 +- .../decorators/websocket-body.decorator.ts | 2 + .../models/websocket-message.model.ts | 2 +- tsconfig.eslint.json | 4 + tsconfig.json | 13 +- tsup.config.ts | 14 - 70 files changed, 2165 insertions(+), 1554 deletions(-) create mode 100644 .npmignore create mode 100644 sandbox/src/models/generated/index.ts create mode 100644 sandbox/src/models/generated/petstore/index.ts create mode 100644 sandbox/src/models/generated/petstore/petstore.api-response.model.ts create mode 100644 sandbox/src/models/generated/petstore/petstore.category.model.ts create mode 100644 sandbox/src/models/generated/petstore/petstore.order.model.ts create mode 100644 sandbox/src/models/generated/petstore/petstore.pet.model.ts create mode 100644 sandbox/src/models/generated/petstore/petstore.tag.model.ts create mode 100644 sandbox/src/models/generated/petstore/petstore.update-pet-with-form.model.ts create mode 100644 sandbox/src/models/generated/petstore/petstore.upload-file.model.ts create mode 100644 sandbox/src/models/generated/petstore/petstore.user.model.ts create mode 100644 sandbox/src/models/generated/providers.ts create mode 100644 src/data-source/models/options/count-options.model.ts create mode 100644 src/entity/generation/generate-entity-file.function.ts create mode 100644 src/entity/generation/generate-entity-files-for-provider.function.ts create mode 100644 src/entity/generation/generate-entity-files-for-provider.test.ts create mode 100644 src/entity/generation/generate-entity-files.function.ts create mode 100644 src/entity/generation/get-entity-file-name.function.ts create mode 100644 src/entity/generation/index.ts create mode 100644 src/entity/generation/providers/entity-generation-provider.interface.ts create mode 100644 src/entity/generation/providers/index.ts create mode 100644 src/entity/generation/providers/open-api-file.provider.ts create mode 100644 src/entity/generation/providers/open-api-to-v3.function.ts create mode 100644 src/entity/generation/providers/open-api-url.provider.ts create mode 100644 src/utilities/add-import-statement.function.ts create mode 100644 src/utilities/to-camel-case.function.ts create mode 100644 src/utilities/to-kebab-case.function.ts create mode 100644 src/utilities/to-pascal-case.function.ts create mode 100644 tsconfig.eslint.json delete mode 100644 tsup.config.ts diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..d4c16d1 --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +src/**/*.hbs.ts +src/di/default/temp +src/__testing__ +src/jest.setup.ts +src/**/*.test.* \ No newline at end of file diff --git a/docs/data-source.md b/docs/data-source.md index 2bb389c..5c06753 100644 --- a/docs/data-source.md +++ b/docs/data-source.md @@ -51,7 +51,7 @@ export class Test extends BaseEntity { } ``` -An entity to be included in a data source needs to extend `BaseEntity`, which defines an id, createdAt and updatedAt properties. +An entity to be included in a data source needs to extend `BaseEntity`, which defines an id property. In order for the data source to map the properties, you need to decorate them with the `@Property` decorator. This is also used for validation. diff --git a/eslint.config.mjs b/eslint.config.mjs index 243ffbc..eabfeed 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,15 @@ import { configs } from 'eslint-config-service-soft'; /** @type {import('eslint').Linter.Config} */ export default [ ...configs, - { ignores: ['tsconfig.json', 'tsup.config.ts', 'sandbox', 'docs', 'src/di/default/temp'] }, + { + files: ['**/*.ts'], + languageOptions: { + parserOptions: { + project: ['tsconfig.eslint.json'] + } + } + }, + { ignores: ['tsconfig.json', 'tsup.config.ts', 'sandbox', 'docs', 'src/di/default/temp', '**/__testing__/file-output/**'] }, { files: ['**/__testing__/**/*.ts'], rules: { diff --git a/package-lock.json b/package-lock.json index 2dc5296..b6667b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zibri", - "version": "2.1.3", + "version": "2.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zibri", - "version": "2.1.3", + "version": "2.1.4", "license": "MIT", "dependencies": { "@fastify/busboy": "^3.2.0", @@ -19,6 +19,7 @@ "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "swagger-ui-express": "^5.0.1", + "swagger2openapi": "^7.0.8", "systeminformation": "^5.27.10", "typeorm": "^0.3.27" }, @@ -34,13 +35,13 @@ "@types/nodemailer": "^7.0.1", "@types/pdfmake": "^0.2.11", "@types/swagger-ui-express": "^4.1.8", + "@types/swagger2openapi": "^7.0.4", "eslint": "^9.36.0", "eslint-config-service-soft": "^2.1.2", "jest": "^30.1.3", "openapi3-ts": "^4.5.0", "testcontainers": "^11.6.0", "ts-jest": "^29.4.4", - "tsup": "^8.5.0", "typedoc": "^0.27.9", "typescript": "^5.8.3" }, @@ -2265,628 +2266,192 @@ "node": ">=20.11.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", - "cpu": [ - "ppc64" - ], + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", - "cpu": [ - "arm" - ], + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=18" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/compat": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz", + "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.40 || 9" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", - "cpu": [ - "x64" - ], + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", - "cpu": [ - "arm" - ], + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", - "cpu": [ - "arm64" - ], + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", - "cpu": [ - "ia32" - ], + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, "engines": { - "node": ">=18" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT" }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", - "cpu": [ - "mips64el" - ], + "node_modules/@faker-js/faker": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz", + "integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", - "cpu": [ - "ppc64" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } ], - "dev": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/compat": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz", - "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "peerDependencies": { - "eslint": "^8.40 || 9" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@faker-js/faker": { - "version": "9.9.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz", - "integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], - "license": "MIT", - "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" + "node": ">=18.0.0", + "npm": ">=9.0.0" } }, "node_modules/@fastify/busboy": { @@ -3905,394 +3470,86 @@ "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/pkgr" - } - }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", - "cpu": [ - "riscv64" - ], + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", - "cpu": [ - "s390x" - ], + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", - "cpu": [ - "x64" - ], + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", - "cpu": [ - "x64" - ], + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", - "cpu": [ - "arm64" - ], + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", - "cpu": [ - "arm64" - ], + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", - "cpu": [ - "ia32" - ], + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", - "cpu": [ - "x64" - ], + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "BSD-3-Clause" }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", - "cpu": [ - "x64" - ], + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@rtsao/scc": { "version": "1.1.0", @@ -5620,6 +4877,17 @@ "@types/serve-static": "*" } }, + "node_modules/@types/swagger2openapi": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@types/swagger2openapi/-/swagger2openapi-7.0.4.tgz", + "integrity": "sha512-ffMqzciTDihOKH4Q//9Ond1yb5JP1P5FC/aFPsLK4blea1Fwk2aYctiNCkAh5etDYFswFXS+5LV/vuGkf+PU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "openapi-types": "^12.1.0" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -6434,13 +5702,6 @@ "node": ">=14" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, - "license": "MIT" - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -7367,22 +6628,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/bundle-require": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", - "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.3" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.18" - } - }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -7402,16 +6647,6 @@ "node": ">= 0.8" } }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -7459,6 +6694,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -7534,22 +6775,6 @@ "node": ">=10" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", @@ -7763,16 +6988,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/comment-json": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.4.1.tgz", @@ -7822,23 +7037,6 @@ "dev": true, "license": "MIT" }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -8839,47 +8037,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" - } + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "license": "MIT" }, "node_modules/escalade": { "version": "3.2.0", @@ -9907,6 +9069,12 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -10058,18 +9226,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fix-dts-default-cjs-exports": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", - "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.17", - "mlly": "^1.7.4", - "rollup": "^4.34.8" - } - }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -10702,6 +9858,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/http2-client": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "license": "MIT" + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -12229,16 +11391,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/joycon": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", - "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/jpeg-exif": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/jpeg-exif/-/jpeg-exif-1.1.4.tgz", @@ -12565,19 +11717,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -12595,16 +11734,6 @@ "uc.micro": "^2.0.0" } }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -13026,36 +12155,11 @@ "dev": true, "license": "MIT" }, - "node_modules/mlly": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", - "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.15.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.1" - } - }, "node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/mz": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", - "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nan": { "version": "2.24.0", @@ -13112,6 +12216,38 @@ "node": ">=6.0.0" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-h2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", + "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "license": "MIT", + "dependencies": { + "http2-client": "^1.2.5" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -13119,6 +12255,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "license": "MIT", + "dependencies": { + "es6-promise": "^3.2.1" + } + }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -13158,6 +12303,103 @@ "node": ">=8" } }, + "node_modules/oas-kit-common": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", + "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "license": "BSD-3-Clause", + "dependencies": { + "fast-safe-stringify": "^2.0.7" + } + }, + "node_modules/oas-linter": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", + "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@exodus/schemasafe": "^1.0.0-rc.2", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-linter/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-resolver": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", + "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-h2": "^2.3.0", + "oas-kit-common": "^1.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "resolve": "resolve.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-resolver/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oas-schema-walker": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", + "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", + "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "oas-kit-common": "^1.0.8", + "oas-linter": "^3.2.2", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "reftools": "^1.1.9", + "should": "^13.2.1", + "yaml": "^1.10.0" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/oas-validator/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -13316,6 +12558,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "dev": true, + "license": "MIT" + }, "node_modules/openapi3-ts": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.5.0.tgz", @@ -13596,13 +12845,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/pdfmake": { "version": "0.2.20", "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.2.20.tgz", @@ -13820,18 +13062,6 @@ "node": ">=8" } }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -13857,49 +13087,6 @@ "node": ">= 0.4" } }, - "node_modules/postcss-load-config": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", - "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "lilconfig": "^3.1.1" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "jiti": ">=1.21.0", - "postcss": ">=8.0.9", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - }, - "postcss": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -14159,9 +13346,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -14275,20 +13462,6 @@ "node": ">=10" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/refa": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", @@ -14331,6 +13504,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reftools": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", + "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "license": "BSD-3-Clause", + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, "node_modules/regexp-ast-analysis": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", @@ -14531,48 +13713,6 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", - "fsevents": "~2.3.2" - } - }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -14868,6 +14008,60 @@ "node": ">=8" } }, + "node_modules/should": { + "version": "13.2.3", + "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", + "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "license": "MIT", + "dependencies": { + "should-equal": "^2.0.0", + "should-format": "^3.0.3", + "should-type": "^1.4.0", + "should-type-adaptors": "^1.0.1", + "should-util": "^1.0.0" + } + }, + "node_modules/should-equal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", + "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "license": "MIT", + "dependencies": { + "should-type": "^1.4.0" + } + }, + "node_modules/should-format": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", + "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-type-adaptors": "^1.0.1" + } + }, + "node_modules/should-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "license": "MIT" + }, + "node_modules/should-type-adaptors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", + "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "license": "MIT", + "dependencies": { + "should-type": "^1.3.0", + "should-util": "^1.0.0" + } + }, + "node_modules/should-util": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "license": "MIT" + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -15605,29 +14799,6 @@ ], "license": "MIT" }, - "node_modules/sucrase": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", - "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.2", - "commander": "^4.0.0", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "tinyglobby": "^0.2.11", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -15689,6 +14860,42 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/swagger2openapi": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", + "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "license": "BSD-3-Clause", + "dependencies": { + "call-me-maybe": "^1.0.1", + "node-fetch": "^2.6.1", + "node-fetch-h2": "^2.3.0", + "node-readfiles": "^0.2.0", + "oas-kit-common": "^1.0.8", + "oas-resolver": "^2.5.6", + "oas-schema-walker": "^1.1.5", + "oas-validator": "^5.0.8", + "reftools": "^1.1.9", + "yaml": "^1.10.0", + "yargs": "^17.0.1" + }, + "bin": { + "boast": "boast.js", + "oas-validate": "oas-validate.js", + "swagger2openapi": "swagger2openapi.js" + }, + "funding": { + "url": "https://github.com/Mermade/oas-kit?sponsor=1" + } + }, + "node_modules/swagger2openapi/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -15838,29 +15045,6 @@ "b4a": "^1.6.4" } }, - "node_modules/thenify": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", - "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", - "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -15868,13 +15052,6 @@ "license": "MIT", "peer": true }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true, - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -15945,15 +15122,11 @@ "node": ">=0.6" } }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "license": "MIT", - "bin": { - "tree-kill": "cli.js" - } + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" }, "node_modules/ts-api-utils": { "version": "2.1.0", @@ -15968,13 +15141,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/ts-jest": { "version": "29.4.6", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", @@ -16127,59 +15293,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsup": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", - "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^5.1.0", - "cac": "^6.7.14", - "chokidar": "^4.0.3", - "consola": "^3.4.0", - "debug": "^4.4.0", - "esbuild": "^0.27.0", - "fix-dts-default-cjs-exports": "^1.0.0", - "joycon": "^3.1.1", - "picocolors": "^1.1.1", - "postcss-load-config": "^6.0.1", - "resolve-from": "^5.0.0", - "rollup": "^4.34.8", - "source-map": "^0.7.6", - "sucrase": "^3.35.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.11", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@microsoft/api-extractor": "^7.36.0", - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": ">=4.5.0" - }, - "peerDependenciesMeta": { - "@microsoft/api-extractor": { - "optional": true - }, - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", @@ -16817,13 +15930,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ufo": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", - "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", - "dev": true, - "license": "MIT" - }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -17054,6 +16160,22 @@ "makeerror": "1.0.12" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 8125ec3..05d5337 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,22 @@ { "name": "zibri", - "version": "2.1.3", + "version": "2.1.4", "main": "./dist/index.js", - "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", "repository": { "url": "https://github.com/Service-Soft/zibri" }, "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", "require": "./dist/index.js" } }, "files": [ + "./src", "./dist", "./README.md", + "./CONTRIBUTING.md", "./LICENSE" ], "engines": { @@ -24,13 +25,14 @@ "scripts": { "test": "jest", "test:ci": "jest --testPathPatterns='^(?!.*multithreading\\.service\\.test\\.ts$).*'", - "start": "tsup --watch", + "start": "tsc --watch", "sandbox": "cd sandbox && npm run start", - "build": "tsup", + "sandbox:build": "cd sandbox && npm run build", + "build": "tsc && typedoc", "lint": "eslint . --max-warnings=0", "lint:fix": "eslint . --max-warnings=0 --fix" }, - "keywords": [], + "keywords": ["Zibri", "Backend", "Framework"], "author": "Tim Fabian", "license": "MIT", "description": "", @@ -60,6 +62,7 @@ "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", "swagger-ui-express": "^5.0.1", + "swagger2openapi": "^7.0.8", "systeminformation": "^5.27.10", "typeorm": "^0.3.27" }, @@ -75,13 +78,13 @@ "@types/nodemailer": "^7.0.1", "@types/pdfmake": "^0.2.11", "@types/swagger-ui-express": "^4.1.8", + "@types/swagger2openapi": "^7.0.4", "eslint": "^9.36.0", "eslint-config-service-soft": "^2.1.2", "jest": "^30.1.3", "openapi3-ts": "^4.5.0", "testcontainers": "^11.6.0", "ts-jest": "^29.4.4", - "tsup": "^8.5.0", "typedoc": "^0.27.9", "typescript": "^5.8.3" } diff --git a/sandbox/src/data-sources/db/db.data-source.ts b/sandbox/src/data-sources/db/db.data-source.ts index aea6e37..b9f7361 100644 --- a/sandbox/src/data-sources/db/db.data-source.ts +++ b/sandbox/src/data-sources/db/db.data-source.ts @@ -1,4 +1,4 @@ -import { PostgresDataSource, PostgresOptions, BaseEntity, DataSource, Newable, MigrationEntity, JwtRefreshToken, JwtCredentials, PasswordResetToken, MailingList, MailingListSubscriber, MailingListSubscriptionConfirmationToken, Log, Change, ChangeSet, Entity, OmitClass, OtpCredentials, BackupResourceEntity, BackupEntity, Invoice, NumberInvoices } from 'zibri'; +import { PostgresDataSource, PostgresOptions, BaseEntity, DataSource, Newable, MigrationEntity, JwtRefreshToken, JwtCredentials, PasswordResetToken, MailingList, MailingListSubscriber, MailingListSubscriptionConfirmationToken, Log, Change, ChangeSet, Entity, OmitClass, OtpCredentials, BackupResourceEntity, BackupEntity, Invoice, NumberInvoices, Email, CronJobEntity, ThreadJobEntity, WebsocketChannel, WebsocketMessage } from 'zibri'; import { Company, Test, User } from '../../models'; @@ -37,6 +37,11 @@ export class DbDataSource extends PostgresDataSource { NumberInvoices, OtpCredentials, BackupEntity, - BackupResourceEntity + BackupResourceEntity, + Email, + CronJobEntity, + ThreadJobEntity, + WebsocketChannel, + WebsocketMessage ]; } \ No newline at end of file diff --git a/sandbox/src/index.ts b/sandbox/src/index.ts index 9444ada..eb18c38 100644 --- a/sandbox/src/index.ts +++ b/sandbox/src/index.ts @@ -18,7 +18,7 @@ async function start(): Promise { const app: ZibriApplication = new ZibriApplication({ name: 'Api', baseUrl: 'http://localhost:3000', - plugins: [ZibriInvoicingPlugin], + plugins: [new ZibriInvoicingPlugin()], controllers: [ TestController, FileController, diff --git a/sandbox/src/models/generated/index.ts b/sandbox/src/models/generated/index.ts new file mode 100644 index 0000000..56d28c9 --- /dev/null +++ b/sandbox/src/models/generated/index.ts @@ -0,0 +1 @@ +export * from './petstore'; \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/index.ts b/sandbox/src/models/generated/petstore/index.ts new file mode 100644 index 0000000..21f09fa --- /dev/null +++ b/sandbox/src/models/generated/petstore/index.ts @@ -0,0 +1,8 @@ +export * from './petstore.api-response.model'; +export * from './petstore.category.model'; +export * from './petstore.pet.model'; +export * from './petstore.tag.model'; +export * from './petstore.order.model'; +export * from './petstore.user.model'; +export * from './petstore.upload-file.model'; +export * from './petstore.update-pet-with-form.model'; \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/petstore.api-response.model.ts b/sandbox/src/models/generated/petstore/petstore.api-response.model.ts new file mode 100644 index 0000000..6d3c6c5 --- /dev/null +++ b/sandbox/src/models/generated/petstore/petstore.api-response.model.ts @@ -0,0 +1,13 @@ +import { Property } from 'zibri'; + +export class PetstoreApiResponse { + + @Property.number({ required: false }) + code?: number; + + @Property.string({ required: false }) + type?: string; + + @Property.string({ required: false }) + message?: string; +} \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/petstore.category.model.ts b/sandbox/src/models/generated/petstore/petstore.category.model.ts new file mode 100644 index 0000000..4b5b802 --- /dev/null +++ b/sandbox/src/models/generated/petstore/petstore.category.model.ts @@ -0,0 +1,10 @@ +import { Property } from 'zibri'; + +export class PetstoreCategory { + + @Property.number({ required: false }) + id?: number; + + @Property.string({ required: false }) + name?: string; +} \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/petstore.order.model.ts b/sandbox/src/models/generated/petstore/petstore.order.model.ts new file mode 100644 index 0000000..e84a225 --- /dev/null +++ b/sandbox/src/models/generated/petstore/petstore.order.model.ts @@ -0,0 +1,22 @@ +import { Property } from 'zibri'; + +export class PetstoreOrder { + + @Property.number({ required: false }) + id?: number; + + @Property.number({ required: false }) + petId?: number; + + @Property.number({ required: false }) + quantity?: number; + + @Property.date({ required: false }) + shipDate?: Date; + + @Property.string({ required: false }) + status?: 'placed' | 'approved' | 'delivered'; + + @Property.boolean({ required: false }) + complete?: boolean; +} \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/petstore.pet.model.ts b/sandbox/src/models/generated/petstore/petstore.pet.model.ts new file mode 100644 index 0000000..ac447d1 --- /dev/null +++ b/sandbox/src/models/generated/petstore/petstore.pet.model.ts @@ -0,0 +1,24 @@ +import { PetstoreTag } from './petstore.tag.model'; +import { PetstoreCategory } from './petstore.category.model'; +import { Property } from 'zibri'; + +export class PetstorePet { + + @Property.number({ required: false }) + id?: number; + + @Property.object({ required: false, cls: () => PetstoreCategory }) + category?: PetstoreCategory; + + @Property.string() + name!: string; + + @Property.array({ items: { type: 'string' } }) + photoUrls!: string[]; + + @Property.array({ required: false, items: { type: 'object', cls: () => PetstoreTag } }) + tags?: PetstoreTag[]; + + @Property.string({ required: false }) + status?: 'available' | 'pending' | 'sold'; +} \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/petstore.tag.model.ts b/sandbox/src/models/generated/petstore/petstore.tag.model.ts new file mode 100644 index 0000000..17ad3b5 --- /dev/null +++ b/sandbox/src/models/generated/petstore/petstore.tag.model.ts @@ -0,0 +1,10 @@ +import { Property } from 'zibri'; + +export class PetstoreTag { + + @Property.number({ required: false }) + id?: number; + + @Property.string({ required: false }) + name?: string; +} \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/petstore.update-pet-with-form.model.ts b/sandbox/src/models/generated/petstore/petstore.update-pet-with-form.model.ts new file mode 100644 index 0000000..f49a3a5 --- /dev/null +++ b/sandbox/src/models/generated/petstore/petstore.update-pet-with-form.model.ts @@ -0,0 +1,10 @@ +import { Property } from 'zibri'; + +export class PetstoreUpdatePetWithForm { + + @Property.string({ required: false }) + name?: string; + + @Property.string({ required: false }) + status?: string; +} \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/petstore.upload-file.model.ts b/sandbox/src/models/generated/petstore/petstore.upload-file.model.ts new file mode 100644 index 0000000..6176b9d --- /dev/null +++ b/sandbox/src/models/generated/petstore/petstore.upload-file.model.ts @@ -0,0 +1,10 @@ +import { Property } from 'zibri'; + +export class PetstoreUploadFile { + + @Property.string({ required: false }) + additionalMetadata?: string; + + @Property.string({ required: false }) + file?: string; +} \ No newline at end of file diff --git a/sandbox/src/models/generated/petstore/petstore.user.model.ts b/sandbox/src/models/generated/petstore/petstore.user.model.ts new file mode 100644 index 0000000..75b08e3 --- /dev/null +++ b/sandbox/src/models/generated/petstore/petstore.user.model.ts @@ -0,0 +1,28 @@ +import { Property } from 'zibri'; + +export class PetstoreUser { + + @Property.number({ required: false }) + id?: number; + + @Property.string({ required: false }) + username?: string; + + @Property.string({ required: false }) + firstName?: string; + + @Property.string({ required: false }) + lastName?: string; + + @Property.string({ required: false }) + email?: string; + + @Property.string({ required: false }) + password?: string; + + @Property.string({ required: false }) + phone?: string; + + @Property.number({ required: false }) + userStatus?: number; +} \ No newline at end of file diff --git a/sandbox/src/models/generated/providers.ts b/sandbox/src/models/generated/providers.ts new file mode 100644 index 0000000..c8b3617 --- /dev/null +++ b/sandbox/src/models/generated/providers.ts @@ -0,0 +1,3 @@ +import { EntityGenerationProvider, OpenApiUrlProvider } from 'zibri'; + +export const providers: EntityGenerationProvider[] = [new OpenApiUrlProvider('Petstore', 'https://petstore.swagger.io/v2/swagger.json', false)]; \ No newline at end of file diff --git a/sandbox/src/models/index.ts b/sandbox/src/models/index.ts index 2221bc9..3e86aae 100644 --- a/sandbox/src/models/index.ts +++ b/sandbox/src/models/index.ts @@ -1,4 +1,5 @@ export * from './test.model'; export * from './user.model'; export * from './roles.enum'; -export * from './company.model'; \ No newline at end of file +export * from './company.model'; +export * from './generated'; \ No newline at end of file diff --git a/sandbox/webpack.config.js b/sandbox/webpack.config.js index 7f3136e..f1d7d18 100644 --- a/sandbox/webpack.config.js +++ b/sandbox/webpack.config.js @@ -2,7 +2,7 @@ const path = require('path'); const { spawn } = require('child_process'); const CopyPlugin = require('copy-webpack-plugin'); -const { generateHandlebarTypeFiles } = require('zibri'); +const { generateHandlebarTypeFiles, generateEntityFiles } = require('zibri'); class OnBuildSuccessPlugin { /** @type {import('webpack').WebpackPluginFunction } */ @@ -34,6 +34,17 @@ class HandlebarsTypegenPlugin { } } +class EntityGenerationPlugin { + /** @type {import('webpack').WebpackPluginFunction } */ + apply(compiler) { + // on every rebuild (and initial build), run our stub generator first + compiler.hooks.beforeCompile.tapPromise( + 'EntityGenerationPlugin', + () => generateEntityFiles() + ); + } +} + /** @type {import('webpack').Configuration} */ module.exports = { target: 'node', @@ -81,11 +92,13 @@ module.exports = { mysql: 'commonjs2 mysql', 'hdb-pool': 'commonjs2 hdb-pool', 'better-sqlite3': 'commonjs2 better-sqlite3', + sqlite3: 'commonjs2 sqlite3', ioredis: 'commonjs2 ioredis', mysql2: 'commonjs2 mysql2', mongodb: 'commonjs2 mongodb', '@sap\/hana-client': 'commonjs2 @sap\/hana-client', '@sap\/hana-client\/extension\/Stream': 'commonjs2 @sap\/hana-client\/extension\/Stream', + 'ts-node': 'commonjs2 ts-node', 'utf-8-validate': 'utf-8-validate', bufferutil: 'bufferutil' }, @@ -105,7 +118,8 @@ module.exports = { exclude: [ /node_modules[\/\\]node-cron/, /node_modules\/xmlbuilder2/, - /node_modules\/@oozcitak/ + /node_modules\/@oozcitak/, + /node_modules\/@jridgewell/ ] }, { @@ -125,6 +139,7 @@ module.exports = { }, plugins: [ new HandlebarsTypegenPlugin(), + new EntityGenerationPlugin(), new OnBuildSuccessPlugin(), new CopyPlugin({ patterns: [ diff --git a/src/__testing__/mocks/entities/child.entity.ts b/src/__testing__/mocks/entities/child.entity.ts index 2dceee2..d941aa5 100644 --- a/src/__testing__/mocks/entities/child.entity.ts +++ b/src/__testing__/mocks/entities/child.entity.ts @@ -1,7 +1,7 @@ import { Parent } from './parent.entity'; import { Entity, Property } from '../../../entity'; -@Entity('child') +@Entity() export class Child { @Property.string({ primary: true }) id!: string; diff --git a/src/__testing__/mocks/entities/parent.entity.ts b/src/__testing__/mocks/entities/parent.entity.ts index 8264e97..e055289 100644 --- a/src/__testing__/mocks/entities/parent.entity.ts +++ b/src/__testing__/mocks/entities/parent.entity.ts @@ -1,7 +1,7 @@ import { Child } from './child.entity'; import { Entity, Property } from '../../../entity'; -@Entity('parent') +@Entity() export class Parent { @Property.string({ primary: true }) id!: string; diff --git a/src/__testing__/mocks/entities/profile.entity.ts b/src/__testing__/mocks/entities/profile.entity.ts index e87466b..24b0368 100644 --- a/src/__testing__/mocks/entities/profile.entity.ts +++ b/src/__testing__/mocks/entities/profile.entity.ts @@ -1,7 +1,7 @@ import { User } from './user.entity'; import { Entity, Property } from '../../../entity'; -@Entity('profile') +@Entity() export class Profile { @Property.string({ primary: true }) id!: string; diff --git a/src/__testing__/mocks/entities/role.entity.ts b/src/__testing__/mocks/entities/role.entity.ts index a6fb1cb..168d210 100644 --- a/src/__testing__/mocks/entities/role.entity.ts +++ b/src/__testing__/mocks/entities/role.entity.ts @@ -1,7 +1,7 @@ import { User } from './user.entity'; import { Entity, Property } from '../../../entity'; -@Entity('role') +@Entity() export class Role { @Property.string({ primary: true }) id!: string; diff --git a/src/__testing__/mocks/entities/user.entity.ts b/src/__testing__/mocks/entities/user.entity.ts index f246eb6..c8ffad0 100644 --- a/src/__testing__/mocks/entities/user.entity.ts +++ b/src/__testing__/mocks/entities/user.entity.ts @@ -7,7 +7,7 @@ import { Role } from './role.entity'; import { Entity, Property } from '../../../entity'; import { OmitStrict } from '../../../types'; -@Entity('user') +@Entity() export class User { @Property.string({ primary: true }) id!: string; diff --git a/src/application-options.model.ts b/src/application-options.model.ts index f9544a7..0d8a0f8 100644 --- a/src/application-options.model.ts +++ b/src/application-options.model.ts @@ -66,5 +66,5 @@ export type ZibriApplicationOptions = { /** * The plugins to be used. */ - plugins?: Newable[] + plugins?: ZibriPlugin[] }; \ No newline at end of file diff --git a/src/application.ts b/src/application.ts index f321b3f..59186ac 100644 --- a/src/application.ts +++ b/src/application.ts @@ -20,7 +20,6 @@ import { MetricsServiceInterface } from './metrics'; import { MultithreadingServiceInterface } from './multithreading'; import { OpenApiServiceInterface } from './open-api'; import { FormDataBodyParser, JsonBodyParser, ParserInterface } from './parsing'; -import { ZibriPlugin } from './plugin'; import { Route, RouterInterface } from './routing'; import { OmitStrict } from './types'; import { BaseWebsocketConnection, WebsocketServiceInterface } from './websocket'; @@ -175,8 +174,7 @@ export class ZibriApplication { await this.backupService.init(); for (const plugin of this.providedOptions.plugins ?? []) { - const p: ZibriPlugin = inject(plugin); - await p.validate(this); + await plugin.validate(this); } GlobalRegistry.markAppAsInitialized(); @@ -213,13 +211,12 @@ export class ZibriApplication { ...this.providedOptions }; for (const plugin of this.providedOptions.plugins ?? []) { - const p: ZibriPlugin = inject(plugin); // TODO: handle order of plugin initialization so that everything is available for DI inside the plugin constructor. - res.authStrategies = [...p.authStrategies, ...res.authStrategies]; - res.bodyParsers = [...p.bodyParsers, ...res.bodyParsers]; - res.controllers = [...p.controllers, ...res.controllers]; - res.cronJobs = [...p.cronJobs, ...res.cronJobs]; - res.providers = [...p.providers, ...res.providers]; + res.authStrategies = [...plugin.authStrategies, ...res.authStrategies]; + res.bodyParsers = [...plugin.bodyParsers, ...res.bodyParsers]; + res.controllers = [...plugin.controllers, ...res.controllers]; + res.cronJobs = [...plugin.cronJobs, ...res.cronJobs]; + res.providers = [...plugin.providers, ...res.providers]; } if (!res.authStrategies.length) { diff --git a/src/auth/strategies/jwt/jwt.auth-strategy.ts b/src/auth/strategies/jwt/jwt.auth-strategy.ts index 421be93..ce25ffb 100644 --- a/src/auth/strategies/jwt/jwt.auth-strategy.ts +++ b/src/auth/strategies/jwt/jwt.auth-strategy.ts @@ -1,7 +1,5 @@ import { randomBytes } from 'crypto'; -import { SecuritySchemeObject } from 'openapi3-ts/oas31'; - import { EncodedJwtAccessToken } from './encoded-jwt-access-token.model'; import { JwtAccessTokenPayload } from './jwt-access-token-payload.model'; import { JwtAuthData } from './jwt-auth-data.model'; @@ -20,6 +18,7 @@ import { TooManyRequestsError, UnauthorizedError } from '../../../error-handling import { GlobalRegistry } from '../../../global'; import { renderEmailTemplate } from '../../../handlebars'; import { HttpRequest } from '../../../http'; +import { OpenApiSecuritySchemeObject } from '../../../open-api'; import { Newable } from '../../../types'; import { Ms, UUIDUtilities, validateEntitiesRegistered } from '../../../utilities'; import { WebsocketRequest } from '../../../websocket'; @@ -49,7 +48,7 @@ implements AuthStrategyInterface< readonly name: string = 'jwt'; // eslint-disable-next-line jsdoc/require-jsdoc - readonly securityScheme: SecuritySchemeObject = { + readonly securityScheme: OpenApiSecuritySchemeObject = { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', diff --git a/src/backup/backup-service.test.ts b/src/backup/backup-service.test.ts index 9c4675e..6d361e8 100644 --- a/src/backup/backup-service.test.ts +++ b/src/backup/backup-service.test.ts @@ -21,7 +21,7 @@ import { PostgresDataSource, PostgresOptions } from '../data-source'; const backupFsFolder: string = path.join(testFileFolder, 'backups'); -@Entity('item') +@Entity() class Item { @Property.string({ primary: true }) id!: string; diff --git a/src/data-source/data-source.service.ts b/src/data-source/data-source.service.ts index 0a503ef..87aa011 100644 --- a/src/data-source/data-source.service.ts +++ b/src/data-source/data-source.service.ts @@ -2,16 +2,13 @@ import { GlobalRegistry } from '../global'; import { DataSourceServiceInterface } from './data-source-service.interface'; import { JwtCredentials, PasswordResetToken, JwtRefreshToken, OtpCredentials } from '../auth'; import { BackupEntity, BackupResourceEntity } from '../backup'; -import { CronJobEntity } from '../cron'; import { inject, ZIBRI_DI_TOKENS } from '../di'; -import { Email, MailingList, MailingListSubscriber } from '../email'; +import { MailingList, MailingListSubscriber } from '../email'; import { BaseEntity } from '../entity/base-entity.model'; import { Log, LoggerInterface } from '../logging'; -import { ThreadJobEntity } from '../multithreading'; import { Invoice, NumberInvoices } from '../plugin'; import { Newable } from '../types'; import { validateEntitiesRegistered } from '../utilities'; -import { WebsocketChannel, WebsocketMessage } from '../websocket'; import { DataSourceInterface } from './data-sources/data-source.interface'; /** @@ -20,13 +17,6 @@ import { DataSourceInterface } from './data-sources/data-source.interface'; export class DataSourceService implements DataSourceServiceInterface { private readonly logger: LoggerInterface; - private readonly defaultEntities: Newable[] = [ - CronJobEntity, - Email, - ThreadJobEntity, - WebsocketChannel, - WebsocketMessage - ]; private readonly allowedOrphans: Newable[] = [ JwtRefreshToken, JwtCredentials, @@ -54,11 +44,6 @@ export class DataSourceService implements DataSourceServiceInterface { for (const dataSourceClass of GlobalRegistry.dataSourceClasses) { const dataSource: DataSourceInterface = inject(dataSourceClass); - for (const entity of this.defaultEntities) { - if (!dataSource.entities.includes(entity)) { - dataSource.entities.push(entity); - } - } await this.logger.info(` - ${dataSourceClass.name} (${dataSource.entities.length} entities)`); await dataSource.init(); } diff --git a/src/data-source/models/options/count-options.model.ts b/src/data-source/models/options/count-options.model.ts new file mode 100644 index 0000000..2b80ad7 --- /dev/null +++ b/src/data-source/models/options/count-options.model.ts @@ -0,0 +1,14 @@ +import { BaseRepositoryOptions } from './base-repository-options.model'; +import { BaseEntity } from '../../../entity/base-entity.model'; +import { Where } from '../where'; + +/** + * Options for counting entities. + */ +export type CountOptions = BaseRepositoryOptions + & { + /** + * The where filter to count the options by. + */ + where?: Where + }; \ No newline at end of file diff --git a/src/data-source/models/options/find-all-options.model.ts b/src/data-source/models/options/find-all-options.model.ts index 1af4a93..2765bf7 100644 --- a/src/data-source/models/options/find-all-options.model.ts +++ b/src/data-source/models/options/find-all-options.model.ts @@ -2,21 +2,20 @@ import { FindManyOptions } from 'typeorm'; import { BaseRepositoryOptions } from './base-repository-options.model'; import { BaseEntity } from '../../../entity/base-entity.model'; -import { OmitStrict } from '../../../types'; import { Where } from '../where'; /** * Options for finding multiple entities. */ -export type FindAllOptions = BaseRepositoryOptions & OmitStrict< - FindManyOptions, 'transaction' | 'where' | 'relations' | 'withDeleted' -> & { - /** - * The where filter to find the options by. - */ - where?: Where, - /** - * The relations to include in the found entities. - */ - relations?: (keyof T)[] -}; \ No newline at end of file +export type FindAllOptions = BaseRepositoryOptions + & Pick, 'order' | 'skip' | 'take'> + & { + /** + * The where filter to find the options by. + */ + where?: Where, + /** + * The relations to include in the found entities. + */ + relations?: (keyof T)[] + }; \ No newline at end of file diff --git a/src/data-source/models/options/find-one-options.model.ts b/src/data-source/models/options/find-one-options.model.ts index 058a6e5..613452b 100644 --- a/src/data-source/models/options/find-one-options.model.ts +++ b/src/data-source/models/options/find-one-options.model.ts @@ -1,15 +1,12 @@ -import { FindOneOptions as TOFindOneOptions } from 'typeorm'; - import { BaseRepositoryOptions } from './base-repository-options.model'; import { BaseEntity } from '../../../entity/base-entity.model'; -import { OmitStrict } from '../../../types'; import { Where } from '../where'; /** * Options for finding a single entity. */ export type FindOneOptions = BaseRepositoryOptions - & OmitStrict, 'where' | 'transaction' | 'relations' | 'withDeleted'> & { + & { /** * The where filter to find the entity by. */ diff --git a/src/data-source/models/options/index.ts b/src/data-source/models/options/index.ts index 120ec13..e43f154 100644 --- a/src/data-source/models/options/index.ts +++ b/src/data-source/models/options/index.ts @@ -8,4 +8,5 @@ export * from './find-all-paginated-options.model'; export * from './find-by-id-options.model'; export * from './find-one-options.model'; export * from './update-all-options.model'; -export * from './update-by-id-options.model'; \ No newline at end of file +export * from './update-by-id-options.model'; +export * from './count-options.model'; \ No newline at end of file diff --git a/src/entity/generation/generate-entity-file.function.ts b/src/entity/generation/generate-entity-file.function.ts new file mode 100644 index 0000000..543b0c5 --- /dev/null +++ b/src/entity/generation/generate-entity-file.function.ts @@ -0,0 +1,230 @@ + +import { getEntityFileName } from './get-entity-file-name.function'; +import { EntityGenerationProvider } from './providers/entity-generation-provider.interface'; +import { OpenApiReferenceObject, OpenApiSchemaObject, OpenApiSchemas } from '../../open-api'; +import { toCamelCase, toPascalCase } from '../../utilities'; +import { addImportStatement } from '../../utilities/add-import-statement.function'; + +/** + * The result for generating a single entity. + */ +export type GenerateEntityFileResult = { + /** + * Any additional found open api schemas. + */ + foundSchemas: OpenApiSchemas, + /** + * The lines needed to generate the entity file. + */ + lines: string[] +}; + +/** + * The result of resolving a typescript type. + */ +type ResolveTsTypeResult = { + /** + * The actual type eg. 'string' | 'number'. + */ + type: string, + /** + * Whether or not the type in open api is a reference. + */ + isRef: boolean, + /** + * The open api schema of the type. + */ + schema?: OpenApiSchemaObject | OpenApiReferenceObject +}; + +/** + * Generates a single entity file. + * @param provider - The provider from which the schema was resolved. + * @param name - The name of the entity. + * @param schema - The open api schema of the entity. + * @returns Any additional schemas found and the lines needed to generate the entity file. + */ +// eslint-disable-next-line sonar/cognitive-complexity +export function generateEntityFile( + provider: EntityGenerationProvider, + name: string, + schema: OpenApiSchemaObject +): GenerateEntityFileResult { + const properties: OpenApiSchemas = schema.properties ?? {}; + const required: Set = new Set(schema.required ?? []); + + const lines: string[] = []; + + lines.push(`import { ${provider.markAsEntities ? 'Entity, ' : ''}Property } from \'zibri\';`); + lines.push(''); + if (provider.markAsEntities) { + lines.push('@Entity()'); + } + lines.push(`export class ${getEntityName(provider.prefix, name)} {`); + + const foundSchemas: OpenApiSchemas = {}; + + for (const [propName, propSchema] of Object.entries(properties)) { + const isRequired: boolean = required.has(propName); + const optional: string = isRequired ? '!' : '?'; + const { type, isRef, schema } = mapSchemaToTsType(propSchema, propName); + if (isRef && schema && !('$ref' in schema)) { + // type could be "X" or "X[]"; normalize to base name + const baseType: string = type.endsWith('[]') ? type.slice(0, -2) : type; + // only add if not already present (will get processed by outer loop) + if (!(baseType in foundSchemas)) { + foundSchemas[baseType] = schema; + } + } + const decoratorLines: string[] = mapSchemaToDecoratorLines(propSchema, propName, provider.prefix, isRequired, type); + + if (!isRef) { + lines.push('', ...decoratorLines, ` ${toCamelCase(propName)}${optional}: ${type};`); + continue; + } + + lines.push('', ...decoratorLines, ` ${toCamelCase(propName)}${optional}: ${getEntityName(provider.prefix, type)};`); + addImportStatement( + lines, + { + defaultImport: false, + element: getEntityName(provider.prefix, type.split('[]')[0]), + path: `./${getEntityFileName(provider.prefix, type.split('[]')[0]).split('.ts')[0]}` + } + ); + } + + lines.push('}'); + + return { foundSchemas, lines }; +} + +// eslint-disable-next-line jsdoc/require-jsdoc +function mapSchemaToTsType( + schema: OpenApiSchemaObject | OpenApiReferenceObject, + propName: string +): ResolveTsTypeResult { + if ('$ref' in schema) { + return { type: schema.$ref?.split('/').pop() ?? '', isRef: true }; + } + + switch (schema.type) { + case 'string': { + if (schema.enum) { + return { type: schema.enum.map(v => JSON.stringify(v).replaceAll('"', '\'')).join(' | '), isRef: false }; + } + if (schema.format === 'date-time') { + return { type: 'Date', isRef: false }; + } + return { type: 'string', isRef: false }; + } + case 'integer': + case 'number': { + return { type: 'number', isRef: false }; + } + case 'boolean': { + return { type: 'boolean', isRef: false }; + } + case 'array': { + if (!schema.items) { + return { + type: 'unknown[]', + isRef: false + }; + } + const { type, isRef, schema: s } = mapSchemaToTsType(schema.items, `${propName}Item`); + return { type: `${type}[]`, isRef, schema: s }; + } + case 'object': { + if (!schema.properties) { + return { type: 'Record', isRef: false }; + } + return { type: schema.title ?? propName, isRef: true, schema: schema }; + } + case undefined: { + return { type: 'undefined', isRef: false }; + } + case 'null': { + return { type: 'null', isRef: false }; + } + default: { + return { type: 'unknown', isRef: false }; + } + } +} + +// eslint-disable-next-line jsdoc/require-jsdoc, sonar/cognitive-complexity +function mapSchemaToDecoratorLines( + schema: OpenApiSchemaObject | OpenApiReferenceObject, + propName: string, + prefix: string, + isRequired: boolean, + type: string +): string[] { + if ('$ref' in schema) { + // eslint-disable-next-line sonar/no-duplicate-string + return [` @Property.object({ ${!isRequired ? 'required: false, ' : ''}cls: () => ${getEntityName(prefix, type)} })`]; + } + + switch (schema.type) { + case 'string': { + if (schema.format === 'date-time') { + if (isRequired) { + return [' @Property.date()']; + } + return [` @Property.date({ ${!isRequired ? 'required: false' : ''} })`]; + } + // TODO + // if (schema.enum) { + // return { type: schema.enum.map(v => JSON.stringify(v)).join(' | '), isRef: false }; + // } + if (isRequired) { + return [' @Property.string()']; + } + return [' @Property.string({ required: false })']; + } + case 'integer': + case 'number': { + if (isRequired) { + return [' @Property.number()']; + } + return [' @Property.number({ required: false })']; + } + case 'boolean': { + if (isRequired) { + return [' @Property.boolean()']; + } + return [' @Property.boolean({ required: false })']; + } + case 'array': { + if (!schema.items) { + return [` @Property.array({ ${!isRequired ? 'required: false, ' : ''}items: { type: 'unknown' } })`]; + } + const { type: t, isRef } = mapSchemaToTsType(schema.items, propName); + const itemsType: string = isRef ? `'object', cls: () => ${getEntityName(prefix, type.split('[]')[0])}` : `'${t}'`; + if (itemsType.startsWith('\'\'')) { + return [` @Property.array({ ${!isRequired ? 'required: false, ' : ''}items: { type: 'string' } })`]; + } + if (itemsType === '\'Record\'') { + return [` @Property.array({ ${!isRequired ? 'required: false, ' : ''}items: { type: 'unknown' } })`]; + } + return [` @Property.array({ ${!isRequired ? 'required: false, ' : ''}items: { type: ${itemsType} } })`]; + } + case undefined: + case 'null': { + return []; + } + case 'object': + default: { + if (isRequired) { + return [' @Property.unknown()']; + } + return [' @Property.unknown({ required: false })']; + } + } +} + +// eslint-disable-next-line jsdoc/require-jsdoc +function getEntityName(prefix: string, name: string): string { + return `${toPascalCase(prefix)}${toPascalCase(name)}`.replaceAll('{', '').replaceAll('}', ''); +} \ No newline at end of file diff --git a/src/entity/generation/generate-entity-files-for-provider.function.ts b/src/entity/generation/generate-entity-files-for-provider.function.ts new file mode 100644 index 0000000..ce11114 --- /dev/null +++ b/src/entity/generation/generate-entity-files-for-provider.function.ts @@ -0,0 +1,194 @@ +import { mkdir } from 'fs/promises'; +import path from 'path'; + +import { generateEntityFile } from './generate-entity-file.function'; +import { getEntityFileName } from './get-entity-file-name.function'; +import { EntityGenerationProvider } from './providers'; +import { warn } from '../../logging/logger.helpers'; +import { OpenApiDefinition, OpenApiOperation, OpenApiReferenceObject, OpenApiResponseObject, OpenApiSchemaObject, OpenApiSchemas } from '../../open-api'; +import { pathExists, toKebabCase, toPascalCase } from '../../utilities'; + +/** + * All data needed to generate a file. + */ +export type FileToGenerate = { + /** + * The path where the file should be generated. + */ + path: string, + /** + * The actual content of the file in lines. + */ + lines: string[] +}; + +/** + * All data needed to generate files for a provider and updating the index.ts. + */ +export type GenerateEntityFilesForProviderResult = { + /** + * The files to generate. + */ + filesToGenerate: FileToGenerate[], + /** + * The lines to add to the index.ts. + */ + indexLines: string[] +}; + +// eslint-disable-next-line typescript/typedef +const OP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const; + +/** + * Generates entity files for the given provider. + * @param provider - The provider to generate the files for. + * @param cwd - The current working directory. + * @returns The files to generate and the lines to add to the index.ts. + */ +// eslint-disable-next-line sonar/cognitive-complexity +export async function generateEntityFilesForProvider( + provider: EntityGenerationProvider, + cwd: string +): Promise { + const definition: OpenApiDefinition = await provider.resolveSpec(); + const schemas: OpenApiSchemas = definition.components?.schemas ?? {}; + // TODO + for (const [key, path] of Object.entries(definition.paths ?? {})) { + for (const method of OP_METHODS) { + const operation: OpenApiOperation | undefined = path[method]; + if (!operation) { + continue; + } + + const baseFromOp: string = operation.operationId ?? toPascalCase(key); + + if (operation.requestBody && !('$ref' in operation.requestBody)) { + for (const media of Object.values(operation.requestBody.content ?? {})) { + if (!media.schema) { + continue; + } + collectSchemaCandidate(media.schema, baseFromOp, schemas); + } + } + + for (const value of Object.values(operation.responses ?? {})) { + // eslint-disable-next-line typescript/no-unsafe-assignment + const response: OpenApiResponseObject | OpenApiReferenceObject | undefined = value; + if (response == undefined) { + continue; + } + if ('$ref' in response) { + // ignore response $ref (could point to components.responses); responses often wrap schemas inside content + continue; + } + for (const media of Object.values(response.content ?? {})) { + if (!media.schema) { + continue; + } + // build name hint using status code if available in parent loop? we only have the schema and baseFromOp + collectSchemaCandidate(media.schema, baseFromOp, schemas); + } + } + } + } + + if (Object.keys(schemas).length === 0) { + warn(`Could not find any schemas on spec for provider with prefix "${provider.prefix}"`); + return { + filesToGenerate: [], + indexLines: [] + }; + } + + const filesToGenerate: FileToGenerate[] = []; + const indexLines: string[] = []; + + const processedSchemas: Set = new Set(); + let foundSchemas: OpenApiSchemas = schemas; + + while (Object.keys(foundSchemas).length) { + const entries: [string, OpenApiSchemaObject | OpenApiReferenceObject][] = Object.entries(foundSchemas); + const nextFoundSchemas: OpenApiSchemas = {}; + for (const [key, value] of entries) { + // skip if we already generated this schema earlier + if (processedSchemas.has(key)) { + continue; + } + if (!('type' in value) || value.type !== 'object') { + processedSchemas.add(key); + continue; + } + const fileName: string = getEntityFileName(provider.prefix, key); + + // eslint-disable-next-line sonar/no-duplicate-string + const filePath: string = path.join(cwd, 'src/models/generated', toKebabCase(provider.prefix), fileName); + if (await pathExists(filePath)) { + processedSchemas.add(key); + continue; + } + + // eslint-disable-next-line typescript/typedef + const generatedData = generateEntityFile(provider, key, value); + + // accumulate any inline schemas discovered while generating this file + for (const inlineKey of Object.keys(generatedData.foundSchemas)) { + if (!processedSchemas.has(inlineKey) && !(inlineKey in nextFoundSchemas)) { + nextFoundSchemas[inlineKey] = generatedData.foundSchemas[inlineKey]; + } + } + + // mark this one as processed and queue the file write + processedSchemas.add(key); + filesToGenerate.push({ path: filePath, lines: generatedData.lines }); + indexLines.push(`export * from './${fileName.split('.ts')[0]}';`); + } + + foundSchemas = nextFoundSchemas; + } + + if (!filesToGenerate.length) { + return { + filesToGenerate, + indexLines: [] + }; + } + + await mkdir(path.join(cwd, 'src/models/generated', toKebabCase(provider.prefix)), { recursive: true }); + filesToGenerate.push({ path: path.join(cwd, 'src/models/generated', toKebabCase(provider.prefix), 'index.ts'), lines: indexLines }); + + return { + filesToGenerate, + indexLines: [`export * from './${toKebabCase(provider.prefix)}';`] + }; +} + +// eslint-disable-next-line jsdoc/require-jsdoc +function collectSchemaCandidate( + schema: OpenApiSchemaObject | OpenApiReferenceObject, + nameHint: string, + globalSchemas: OpenApiSchemas +): void { + if ('$ref' in schema) { + return; + } + + // if primitive / non-object, ignore + if (schema.type !== 'object' && schema.type !== 'array') { + return; + } + + // arrays: examine items + if (schema.type === 'array' && schema.items) { + collectSchemaCandidate(schema.items, schema.title ?? `${toPascalCase(nameHint)}Item`, globalSchemas); + return; + } + + // inline object with properties -> register under a deterministic name + if ((!schema.type || schema.type === 'object') && schema.properties) { + const name: string = schema.title ?? toPascalCase(nameHint); + // don't override existing component definitions (prefer original) + if (!(name in globalSchemas)) { + globalSchemas[name] = schema; + } + } +} \ No newline at end of file diff --git a/src/entity/generation/generate-entity-files-for-provider.test.ts b/src/entity/generation/generate-entity-files-for-provider.test.ts new file mode 100644 index 0000000..b062464 --- /dev/null +++ b/src/entity/generation/generate-entity-files-for-provider.test.ts @@ -0,0 +1,474 @@ +import assert from 'assert'; + +import { describe, expect, it } from '@jest/globals'; + +import { FileToGenerate, generateEntityFilesForProvider } from './generate-entity-files-for-provider.function'; +import { EntityGenerationProvider, OpenApiUrlProvider } from './providers'; +import { OpenApiDefinition } from '../../open-api'; +import { toKebabCase, toPascalCase } from '../../utilities'; + +// small InlineProvider so tests are offline and deterministic +class InlineProvider implements EntityGenerationProvider { + constructor(public prefix: string, private readonly spec: OpenApiDefinition, public markAsEntities: boolean = true) {} + // eslint-disable-next-line typescript/require-await + async resolveSpec(): Promise { + return this.spec; + } +} + +function findFile(files: FileToGenerate[], className: string): FileToGenerate | undefined { + return files.find(f => f.lines.join('\n').includes(`export class ${className}`)); +} + +function expectDecoratorAboveProperty( + fileContent: string, + decoratorRegex: RegExp, + propName: string, + propNameRegex = new RegExp(`\\b${propName}[!?]:`) +): void { + // Match a decorator block immediately followed by the property line. + // Allows optional single blank line between decorator and property (be tolerant), + // but not other property lines in between. + const re: RegExp = new RegExp( + `${decoratorRegex.source}\\s*\\r?\\n\\s*${propNameRegex.source}`, + 'm' + ); + expect(re.test(fileContent)).toBe(true); +} + +describe('generateEntityFiles', () => { + it('PetStore', async () => { + const provider: OpenApiUrlProvider = new OpenApiUrlProvider('PetStore', 'https://petstore.swagger.io/v2/swagger.json'); + const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + + const pascalPrefix: string = toPascalCase(provider.prefix); + const kebabPrefix: string = toKebabCase(provider.prefix); + + const expectedSchemas: string[] = ['ApiResponse', 'Category', 'Pet', 'Tag', 'Order', 'User']; + for (const schemaName of expectedSchemas) { + const expectedClassName: string = `${pascalPrefix}${toPascalCase(schemaName)}`; + const found: boolean = filesToGenerate.some(f => f.lines.join('\n').includes(`export class ${expectedClassName}`)); + expect(found).toBe(true); + + const expectedExportFragment: string = `${kebabPrefix}.${toKebabCase(schemaName)}.model`; + const indexFound: boolean = indexLines.some(l => l.includes(expectedExportFragment)); + expect(indexFound).toBe(true); + } + + // In-depth checks for Pet entity + const petClassName: string = `${pascalPrefix}Pet`; + const petFile: FileToGenerate | undefined = filesToGenerate.find(f => f.lines.join('\n').includes(`export class ${petClassName}`)); + expect(petFile).toBeDefined(); + + assert(petFile); + + const petContent: string = petFile.lines.join('\n'); + + // required fields should be marked (e.g. name! and photoUrls!) + expect(petContent).toMatch(/\bname[!?]:/); + expect(petContent).toMatch(/\bphotoUrls[!?]:/); + + // status enum should be present (contains one of the enum values) + expect(petContent).toMatch(/available|pending|sold/); + + // imports for related inline entities should be present (Category, Tag) + const catClass: string = `${pascalPrefix}Category`; + const tagClass: string = `${pascalPrefix}Tag`; + expect(petContent).toContain(catClass); + expect(petContent).toContain(tagClass); + + // indexLines should contain at least the pet export + const petExport: string = `${kebabPrefix}.pet.model`; + expect(indexLines.some(l => l.includes(petExport))).toBe(true); + }); + + it('string (required/optional) and date-time produce correct decorators', async () => { + const spec: OpenApiDefinition = { + openapi: '3.1.0', + info: { title: 'test', version: '1.0' }, + components: { + schemas: { + Thing: { + type: 'object', + properties: { + name: { type: 'string' }, + note: { type: 'string' }, + created: { type: 'string', format: 'date-time' } + }, + required: ['name', 'created'] + } + } + }, + paths: {} + }; + + const provider: InlineProvider = new InlineProvider('TestSvc', spec); + const { filesToGenerate } = await generateEntityFilesForProvider(provider, 'test'); + + const className: string = `${toPascalCase(provider.prefix)}${toPascalCase('Thing')}`; + const file: FileToGenerate | undefined = findFile(filesToGenerate, className); + expect(file).toBeDefined(); + assert(file); + const content: string = file.lines.join('\n'); + + // name is required -> should have '!' and @Property.string() immediately above + expect(content).toMatch(/\bname!:/); + expectDecoratorAboveProperty(content, /@Property\.string\(\)/, 'name'); + + // note is optional -> @Property.string({ required: false }) must be right above `note?:` + expect(content).toMatch(/\bnote\?:/); + expectDecoratorAboveProperty(content, /@Property\.string\({\s*required:\s*false\s*}\)/, 'note'); + + // created -> date decorator right above created property + expectDecoratorAboveProperty(content, /@Property\.date\(\)/, 'created'); + }); + + it('number/integer and boolean map to @Property.number/@Property.boolean', async () => { + const spec: OpenApiDefinition = { + openapi: '3.1.0', + info: { title: 'NumBool', version: '1.0' }, + components: { + schemas: { + Metrics: { + type: 'object', + properties: { + count: { type: 'integer', format: 'int32' }, + ratio: { type: 'number' }, + flag: { type: 'boolean' } + }, + required: ['count', 'flag'] + } + } + }, + paths: {} + }; + + const provider: InlineProvider = new InlineProvider('NumBool', spec); + const { filesToGenerate } = await generateEntityFilesForProvider(provider, 'test'); + + const className: string = `${toPascalCase(provider.prefix)}${toPascalCase('Metrics')}`; + const file: FileToGenerate | undefined = findFile(filesToGenerate, className); + expect(file).toBeDefined(); + assert(file); + const content: string = file.lines.join('\n'); + + // count required -> number decorator + '!' + expect(content).toMatch(/\bcount!:/); + expectDecoratorAboveProperty(content, /@Property\.number\(\)/, 'count'); + + // ratio optional -> number decorator with required:false immediately above ratio property + expect(content).toMatch(/\bratio\?:/); + expectDecoratorAboveProperty(content, /@Property\.number\({\s*required:\s*false\s*}\)/, 'ratio'); + + // flag required -> boolean decorator + '!' + expect(content).toMatch(/\bflag!:/); + expectDecoratorAboveProperty(content, /@Property\.boolean\(\)/, 'flag'); + }); + + it('array of primitives uses @Property.array with item type', async () => { + const spec: OpenApiDefinition = { + openapi: '3.1.0', + info: { title: 'arrays', version: '1.0' }, + components: { + schemas: { + Bag: { + type: 'object', + properties: { + tags: { type: 'array', items: { type: 'string' } }, + counts: { type: 'array', items: { type: 'integer' } } + }, + required: ['tags'] + } + } + }, + paths: {} + }; + + const provider: InlineProvider = new InlineProvider('BagSvc', spec); + const { filesToGenerate } = await generateEntityFilesForProvider(provider, 'test'); + + const className: string = `${toPascalCase(provider.prefix)}${toPascalCase('Bag')}`; + const file: FileToGenerate | undefined = findFile(filesToGenerate, className); + expect(file).toBeDefined(); + assert(file); + const content: string = file.lines.join('\n'); + + // tags required => property signature with ! and decorator items: { type: 'string' } + expect(content).toMatch(/\btags!:/); + expectDecoratorAboveProperty(content, /@Property\.array\([^)]*items:\s*{\s*type:\s*'string'\s*}[^)]*\)/, 'tags'); + + // counts optional => items type integer -> mapped to 'number' in decorator and required:false + expect(content).toMatch(/\bcounts\?:/); + expectDecoratorAboveProperty(content, /@Property\.array\([^)]*required:\s*false[^)]*items:\s*{\s*type:\s*'number'\s*}[^)]*\)/, 'counts'); + }); + + it('array of inline objects uses schema.title for generated name and preserves property metadata', async () => { + const spec: OpenApiDefinition = { + openapi: '3.1.0', + info: { title: 'inline-arr-rich', version: '1.0' }, + components: { + schemas: { + Container: { + type: 'object', + properties: { + parts: { + type: 'array', + items: { + type: 'object', + title: 'CustomPart', // important: generator should use this title + description: 'A detailed part object', + properties: { + id: { type: 'string', description: 'identifier' }, + amount: { type: 'integer', format: 'int32' }, + status: { type: 'string', enum: ['ok', 'bad'] }, + createdAt: { type: 'string', format: 'date-time' } + }, + required: ['id', 'createdAt'] + } + } + }, + required: ['parts'] + } + } + }, + paths: {} + }; + + const provider: InlineProvider = new InlineProvider('InlineSvc', spec); + const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + + // expected names: prefix + title + const parentClass: string = `${toPascalCase(provider.prefix)}Container`; + const childClass: string = `${toPascalCase(provider.prefix)}CustomPart`; // must use schema.title, not prop name + + const parentFile: FileToGenerate | undefined = findFile(filesToGenerate, parentClass); + const childFile: FileToGenerate | undefined = findFile(filesToGenerate, childClass); + + expect(parentFile).toBeDefined(); + expect(childFile).toBeDefined(); + + assert(parentFile); + assert(childFile); + + const parentContent: string = parentFile.lines.join('\n'); + const childContent: string = childFile.lines.join('\n'); + + // parent should reference child type in property signature + expect(parentContent).toMatch(new RegExp(`\\bparts!:\\s*${childClass}\\[\\]`)); + + // parent decorator must reference cls: () => ChildClass directly above parts + expectDecoratorAboveProperty( + parentContent, + new RegExp(`@Property\\.array\\([^)]*type:\\s*'object'[^)]*cls:\\s*\\(\\)\\s*=>\\s*${childClass}[^)]*\\)`), + 'parts' + ); + + // parent file should import the child (simple presence of the child class name in imports) + const importLinePresent: boolean = parentFile.lines.some(l => l.includes('import') && l.includes(childClass)); + expect(importLinePresent).toBe(true); + + // child file: check class name and decorator placement for required fields + expect(childContent).toContain(`export class ${childClass}`); + expect(childContent).toMatch(/\bid!:/); // required id + expectDecoratorAboveProperty(childContent, /@Property\.string\(\)/, 'id'); + + // createdAt uses date-time -> date decorator and required + expect(childContent).toMatch(/\bcreatedAt!:/); + expectDecoratorAboveProperty(childContent, /@Property\.date\(\)/, 'createdAt'); + + // amount mapped to number decorator + expectDecoratorAboveProperty(childContent, /@Property\.number\({\s*required:\s*false\s*}\)/, 'amount'); + + // enum values should appear in the generated type (or decorator metadata) + expect(childContent).toMatch(/'ok'\s*|\s*'bad'/); + + // index should export both files (kebab-case fragment) + const kebabPrefix: string = toKebabCase(provider.prefix); + expect(indexLines.some(l => l.includes(`${kebabPrefix}.container.model`))).toBe(true); + expect(indexLines.some(l => l.includes(`${kebabPrefix}.custom-part.model`))).toBe(true); + }); + + it('property referencing a components schema ($ref) produces object decorator and import', async () => { + const spec: OpenApiDefinition = { + openapi: '3.1.0', + info: { title: 'ref-test', version: '1.0' }, + components: { + schemas: { + Refed: { + type: 'object', + properties: { a: { type: 'string' } }, + required: ['a'] + }, + Host: { + type: 'object', + properties: { + other: { $ref: '#/components/schemas/Refed' } + }, + required: ['other'] + } + } + }, + paths: {} + }; + + const provider: InlineProvider = new InlineProvider('RefSvc', spec); + const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + + const hostClass: string = `${toPascalCase(provider.prefix)}${toPascalCase('Host')}`; + const refedClass: string = `${toPascalCase(provider.prefix)}${toPascalCase('Refed')}`; + + const hostFile: FileToGenerate | undefined = findFile(filesToGenerate, hostClass); + const refedFile: FileToGenerate | undefined = findFile(filesToGenerate, refedClass); + + expect(hostFile).toBeDefined(); + expect(refedFile).toBeDefined(); + assert(hostFile); + assert(refedFile); + + const hostContent: string = hostFile.lines.join('\n'); + + // decorator should reference the refed class directly above the property + expectDecoratorAboveProperty( + hostContent, + new RegExp(`@Property\\.object\\([^)]*cls:\\s*\\(\\)\\s*=>\\s*${refedClass}[^)]*\\)`), + 'other' + ); + + // host property should be typed to the referenced class + expect(hostContent).toMatch(new RegExp(`\\bother!:\\s*${refedClass}\\b`)); + + // import line for the refed class should be present + const importLinePresent: boolean = hostFile.lines.some(l => l.includes('import') && l.includes(refedClass)); + expect(importLinePresent).toBe(true); + + // index exports should include both files + const kebabPrefix: string = toKebabCase(provider.prefix); + expect(indexLines.some(l => l.includes(`${kebabPrefix}.host.model`))).toBe(true); + expect(indexLines.some(l => l.includes(`${kebabPrefix}.refed.model`))).toBe(true); + }); + + it('generates entities from requestBody schemas (inline)', async () => { + const spec: OpenApiDefinition = { + openapi: '3.1.0', + info: { title: 'request-body-entities', version: '1.0' }, + paths: { + '/users': { + post: { + operationId: 'createUser', + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + title: 'CreateUserPayload', // generator should prefer this title + properties: { + id: { type: 'integer', format: 'int64' }, + email: { type: 'string', format: 'date-time' }, + role: { type: 'string', enum: ['admin', 'user'] } + }, + required: ['id', 'email'] + } + } + } + }, + responses: { + 200: { description: 'ok' } + } + } + } + }, + components: { schemas: {} } + }; + + const provider: InlineProvider = new InlineProvider('Api', spec); + const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + + const childClass: string = `${toPascalCase(provider.prefix)}CreateUserPayload`; + const childFile: FileToGenerate | undefined = findFile(filesToGenerate, childClass); + expect(childFile).toBeDefined(); + assert(childFile); + + const content: string = childFile.lines.join('\n'); + + // class name and required markers + expect(content).toContain(`export class ${childClass}`); + expect(content).toMatch(/\bid!:/); // id required + expectDecoratorAboveProperty(content, /@Property\.number\(\)/, 'id'); + + // email required -> date decorator + expect(content).toMatch(/\bemail!:/); + expectDecoratorAboveProperty(content, /@Property\.date\(\)/, 'email'); + + // role optional -> union literal present and decorator above property (optional) + expect(content).toMatch(/'admin'\s*\|\s*'user'/); + expect(content).toMatch(/\brole\?:/); + expectDecoratorAboveProperty(content, /@Property\.string\({\s*required:\s*false\s*}\)/, 'role'); + + // index export present (kebab-case) + const kebabPrefix: string = toKebabCase(provider.prefix); + expect(indexLines.some(l => l.includes(`${kebabPrefix}.create-user-payload.model`))).toBe(true); + }); + + it('generates entities from response schemas (inline)', async () => { + const spec: OpenApiDefinition = { + openapi: '3.1.0', + info: { title: 'responses-entities', version: '1.0' }, + paths: { + '/pets/{id}': { + get: { + operationId: 'getPet', + responses: { + 200: { + description: 'successful response', + content: { + 'application/json': { + schema: { + type: 'object', + title: 'PetResponse', // generator should prefer this title + properties: { + id: { type: 'integer', format: 'int64' }, + name: { type: 'string' }, + status: { type: 'string', enum: ['available', 'pending', 'sold'] } + }, + required: ['id', 'name'] + } + } + } + } + } + } + } + }, + components: { schemas: {} } + }; + + const provider: InlineProvider = new InlineProvider('Api', spec); + const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + + const childClass: string = `${toPascalCase(provider.prefix)}PetResponse`; + const childFile: FileToGenerate | undefined = findFile(filesToGenerate, childClass); + expect(childFile).toBeDefined(); + assert(childFile); + + const content: string = childFile.lines.join('\n'); + + // class name and required markers + expect(content).toContain(`export class ${childClass}`); + expect(content).toMatch(/\bid!:/); // id required + expectDecoratorAboveProperty(content, /@Property\.number\(\)/, 'id'); + + // name required -> string decorator + expect(content).toMatch(/\bname!:/); + expectDecoratorAboveProperty(content, /@Property\.string\(\)/, 'name'); + + // status enum optional -> union literal present and decorator above property (optional) + expect(content).toMatch(/'available'\s*\|\s*'pending'\s*\|\s*'sold'/); + expect(content).toMatch(/\bstatus\?:/); + expectDecoratorAboveProperty(content, /@Property\.string\({\s*required:\s*false\s*}\)/, 'status'); + + // index export present (kebab-case) + const kebabPrefix: string = toKebabCase(provider.prefix); + expect(indexLines.some(l => l.includes(`${kebabPrefix}.pet-response.model`))).toBe(true); + }); + +}); \ No newline at end of file diff --git a/src/entity/generation/generate-entity-files.function.ts b/src/entity/generation/generate-entity-files.function.ts new file mode 100644 index 0000000..8c64a91 --- /dev/null +++ b/src/entity/generation/generate-entity-files.function.ts @@ -0,0 +1,63 @@ +import { writeFile } from 'fs/promises'; +import path from 'path'; + +import { register } from 'ts-node'; + +import { FileToGenerate, generateEntityFilesForProvider, GenerateEntityFilesForProviderResult } from './generate-entity-files-for-provider.function'; +import { EntityGenerationProvider } from './providers/entity-generation-provider.interface'; +import { pathExists } from '../../utilities'; + +/** + * Resolves providers from the src/models/generated/providers.ts file and generates entities from them. + */ +export async function generateEntityFiles(): Promise { + const cwd: string = process.cwd(); + const providersPath: string = await resolveProvidersPath(cwd); + const ext: string = path.extname(providersPath).toLowerCase(); + if (ext !== '.ts') { + return; + } + register(); + // eslint-disable-next-line typescript/no-explicit-any, typescript/no-unsafe-assignment, typescript/no-require-imports, typescript/no-var-requires + const imported: any = require(providersPath); + // eslint-disable-next-line typescript/no-unsafe-member-access + const providersRaw: unknown = (imported.providers ?? imported.default) as unknown; + + if (!Array.isArray(providersRaw)) { + throw new Error(`Expected 'providers' (or default export) to be an array in ${providersPath}`); + } + + const indexLines: string[] = []; + const filesToGenerate: FileToGenerate[] = []; + + const providers: EntityGenerationProvider[] = providersRaw as EntityGenerationProvider[]; + for (const provider of providers) { + const data: GenerateEntityFilesForProviderResult = await generateEntityFilesForProvider(provider, cwd); + filesToGenerate.push(...data.filesToGenerate); + indexLines.push(...data.indexLines); + } + + if (indexLines.length) { + filesToGenerate.push({ path: path.join(cwd, 'src/models/generated/index.ts'), lines: indexLines }); + } + for (const f of filesToGenerate) { + await writeFile(f.path, f.lines.join('\n'), 'utf8'); + } +} + +// eslint-disable-next-line jsdoc/require-jsdoc +async function resolveProvidersPath(cwd: string): Promise { + const candidates: string[] = [ + path.join(cwd, 'src/models/generated/providers.js'), + path.join(cwd, 'src/models/generated/providers.cjs'), + path.join(cwd, 'src/models/generated/providers.mjs'), + path.join(cwd, 'src/models/generated/providers.ts') + ]; + const providersPath: string = await Promise.any(candidates.map(async p => { + if (await pathExists(p)) { + return p; + } + throw new Error(`Could not locate ${p}`); + })); + return providersPath; +} \ No newline at end of file diff --git a/src/entity/generation/get-entity-file-name.function.ts b/src/entity/generation/get-entity-file-name.function.ts new file mode 100644 index 0000000..623fcae --- /dev/null +++ b/src/entity/generation/get-entity-file-name.function.ts @@ -0,0 +1,11 @@ +import { toKebabCase } from '../../utilities'; + +/** + * Gets the file name for the given entity name. + * @param prefix - The prefix that should be added to the name. + * @param name - The actual name of the entity. + * @returns The full file name of the entity. + */ +export function getEntityFileName(prefix: string, name: string): string { + return `${toKebabCase(prefix)}.${toKebabCase(name)}.model.ts`; +} \ No newline at end of file diff --git a/src/entity/generation/index.ts b/src/entity/generation/index.ts new file mode 100644 index 0000000..03d5a7d --- /dev/null +++ b/src/entity/generation/index.ts @@ -0,0 +1,2 @@ +export * from './providers'; +export * from './generate-entity-files.function'; \ No newline at end of file diff --git a/src/entity/generation/providers/entity-generation-provider.interface.ts b/src/entity/generation/providers/entity-generation-provider.interface.ts new file mode 100644 index 0000000..5b54a01 --- /dev/null +++ b/src/entity/generation/providers/entity-generation-provider.interface.ts @@ -0,0 +1,19 @@ +import { OpenApiDefinition } from '../../../open-api'; + +/** + * A provider for automatic entity generation. + */ +export interface EntityGenerationProvider { + /** + * The prefix to add to all generated entities, so that they don't overlap with any existing ones. + */ + readonly prefix: string, + /** + * Whether or not the generated entities should be marked with @Entity. + */ + readonly markAsEntities: boolean, + /** + * The method that actually resolves the open api spec that is then later on used to generate the entities. + */ + resolveSpec: () => Promise +} \ No newline at end of file diff --git a/src/entity/generation/providers/index.ts b/src/entity/generation/providers/index.ts new file mode 100644 index 0000000..0268c90 --- /dev/null +++ b/src/entity/generation/providers/index.ts @@ -0,0 +1,3 @@ +export * from './entity-generation-provider.interface'; +export * from './open-api-url.provider'; +export * from './open-api-file.provider'; \ No newline at end of file diff --git a/src/entity/generation/providers/open-api-file.provider.ts b/src/entity/generation/providers/open-api-file.provider.ts new file mode 100644 index 0000000..e7dafcf --- /dev/null +++ b/src/entity/generation/providers/open-api-file.provider.ts @@ -0,0 +1,19 @@ + +import { readFile } from 'fs/promises'; + +import { EntityGenerationProvider } from './entity-generation-provider.interface'; +import { openApiToV3 } from './open-api-to-v3.function'; +import { OpenApiDefinition } from '../../../open-api'; + +/** + * An entity generation provider using a local open api file. + */ +export class OpenApiFileProvider implements EntityGenerationProvider { + constructor(readonly prefix: string, protected readonly filePath: string, readonly markAsEntities: boolean = true) {} + + // eslint-disable-next-line jsdoc/require-jsdoc + async resolveSpec(): Promise { + const spec: unknown = JSON.parse(await readFile(this.filePath, 'utf8')); + return await openApiToV3(spec); + } +} \ No newline at end of file diff --git a/src/entity/generation/providers/open-api-to-v3.function.ts b/src/entity/generation/providers/open-api-to-v3.function.ts new file mode 100644 index 0000000..30c287c --- /dev/null +++ b/src/entity/generation/providers/open-api-to-v3.function.ts @@ -0,0 +1,37 @@ +import { convertObj, ConvertOutputOptions } from 'swagger2openapi'; + +import { OpenApiDefinition } from '../../../open-api'; + +/** + * Converts the given spec to open api v3.1. + * @param spec - The spec to transform. + * @returns An open api v3 spec. + */ +export async function openApiToV3(spec: unknown): Promise { + if (spec == undefined || typeof spec !== 'object' || Array.isArray(spec)) { + throw new Error('Invalid OpenAPI document'); + } + + if ('swagger' in spec && spec.swagger === '2.0') { + // eslint-disable-next-line typescript/no-explicit-any, typescript/no-unsafe-argument + const converted: ConvertOutputOptions = await convertObj(spec as any, { patch: true, warnOnly: false }); + return { + ...converted.openapi, + openapi: '3.1.0' + } as OpenApiDefinition; + } + + if ('openapi' in spec && typeof spec.openapi === 'string') { + if (spec.openapi.startsWith('3.1')) { + return spec as OpenApiDefinition; + } + if (spec.openapi.startsWith('3.0')) { + return { + ...(spec as OpenApiDefinition), + openapi: '3.1.0' + }; + } + } + + throw new Error('Unsupported OpenAPI version'); +} \ No newline at end of file diff --git a/src/entity/generation/providers/open-api-url.provider.ts b/src/entity/generation/providers/open-api-url.provider.ts new file mode 100644 index 0000000..bee429a --- /dev/null +++ b/src/entity/generation/providers/open-api-url.provider.ts @@ -0,0 +1,25 @@ + +import { EntityGenerationProvider } from './entity-generation-provider.interface'; +import { openApiToV3 } from './open-api-to-v3.function'; +import { inject, ZIBRI_DI_TOKENS } from '../../../di'; +import { HttpClientInterface } from '../../../http-client'; +import { OpenApiDefinition } from '../../../open-api'; + +/** + * An entity generation provider using an open api url. + */ +export class OpenApiUrlProvider implements EntityGenerationProvider { + + // eslint-disable-next-line jsdoc/require-jsdoc + protected get http(): HttpClientInterface { + return inject(ZIBRI_DI_TOKENS.HTTP_CLIENT); + } + + constructor(readonly prefix: string, protected readonly baseUrl: string, readonly markAsEntities: boolean = true) {} + + // eslint-disable-next-line jsdoc/require-jsdoc + async resolveSpec(): Promise { + const spec: unknown = (await this.http.get(this.baseUrl)).rawBody; + return await openApiToV3(spec); + } +} \ No newline at end of file diff --git a/src/entity/index.ts b/src/entity/index.ts index 6a080fa..64b9c72 100644 --- a/src/entity/index.ts +++ b/src/entity/index.ts @@ -3,4 +3,5 @@ export * from './omit-class.model'; export * from './intersection-class.model'; export * from './partial-class.model'; export * from './pick-class.model'; -export * from './models'; \ No newline at end of file +export * from './models'; +export * from './generation'; \ No newline at end of file diff --git a/src/error-handling/errors/missing-entities.error.ts b/src/error-handling/errors/missing-entities.error.ts index ed30f05..6492b6b 100644 --- a/src/error-handling/errors/missing-entities.error.ts +++ b/src/error-handling/errors/missing-entities.error.ts @@ -14,7 +14,7 @@ export class MissingEntitiesError extends Error { messages.push(` - ${entity.name}`); } messages.push( - 'Did you forget to add it to your data source entities array?\n', + 'Did you forget to add them to your data source entities array?\n', `If you don\'t want to use "${context}" you can also provide an undefined value for the injection token of "${context}".` ); diff --git a/src/http-client/http-client-response.model.ts b/src/http-client/http-client-response.model.ts index 4b6e840..9923204 100644 --- a/src/http-client/http-client-response.model.ts +++ b/src/http-client/http-client-response.model.ts @@ -39,10 +39,11 @@ export type HttpClientResponse< export type HttpClientResponseForBodyType< T extends object, HeaderParamsObject extends Record, - BodyType extends BodyMetadata['type'] + BodyType extends BodyMetadata['type'], + IsArray extends boolean > = BodyType extends MimeType.FORM_DATA ? HttpClientResponse, HeaderParamsObject> - : HttpClientResponse; + : HttpClientResponse; /** * Checks if the given value is a HttpClientResponse. diff --git a/src/http-client/http-client.interface.ts b/src/http-client/http-client.interface.ts index dfcbcc8..dccea94 100644 --- a/src/http-client/http-client.interface.ts +++ b/src/http-client/http-client.interface.ts @@ -1,5 +1,5 @@ import { HttpClientResponseForBodyType } from './http-client-response.model'; -import { KnownHeader, MimeType } from '../http'; +import { HttpMethod, KnownHeader, MimeType } from '../http'; import { BodyMetadata, BodyMetadataInput, HeaderMetaInputObjectToMetaObject, HeaderMetaObjectToParamsObject, HeaderParamMetadataInput } from '../routing'; import { Newable, OmitStrict } from '../types'; @@ -16,7 +16,8 @@ type HttpOptions< QueryParamsObject extends Record, HeaderParamsObject extends Record, ResponseHeaderMetaInputObject extends Record, - BodyType extends BodyMetadata['type'] + BodyType extends BodyMetadata['type'], + IsArray extends boolean > = { /** * The query parameters of the request, as an object. @@ -39,7 +40,7 @@ type HttpOptions< * * THIS ALSO ACTUALLY VALIDATES THE RESPONSE. */ - responseBody: Newable | OmitStrict & { + responseBody: Newable | OmitStrict & { /** * The type of the response. */ @@ -47,7 +48,11 @@ type HttpOptions< /** * The class that defines the structure of the body. */ - modelClass: Newable + modelClass: Newable, + /** + * Whether or not the response is an array. + */ + isArray?: IsArray }, /** * Definition of the expected response headers. Also handles validating them. @@ -63,13 +68,37 @@ export type HttpOptionsInput< QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON -> = Partial>; + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false +> = Partial>; /** * Interface for a http client. */ export interface HttpClientInterface { + /** + * Sends a request with the given data. + */ + request: < + T extends object, + QueryParamsObject extends Record, + HeaderParamsObject extends Record, + ResponseHeaderMetaInputObject extends Record, + BodyType extends BodyMetadata['type'], + IsArray extends boolean + >( + method: HttpMethod, + url: string, + requestBody: unknown, + options: HttpOptionsInput | undefined + ) => Promise< + HttpClientResponseForBodyType< + T, + HeaderMetaObjectToParamsObject>, + BodyType, + IsArray + > + >, /** * Sends a http post request. */ @@ -78,16 +107,18 @@ export interface HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, body: unknown, - options?: HttpOptionsInput + options?: HttpOptionsInput ) => Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > >, /** @@ -98,15 +129,17 @@ export interface HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, - options?: HttpOptionsInput + options?: HttpOptionsInput ) => Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > >, /** @@ -117,16 +150,18 @@ export interface HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, body: unknown, - options?: HttpOptionsInput + options?: HttpOptionsInput ) => Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > >, /** @@ -137,16 +172,18 @@ export interface HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, body: unknown, - options?: HttpOptionsInput + options?: HttpOptionsInput ) => Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > >, /** @@ -157,15 +194,17 @@ export interface HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, - options?: HttpOptionsInput + options?: HttpOptionsInput ) => Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > > } \ No newline at end of file diff --git a/src/http-client/http-client.ts b/src/http-client/http-client.ts index ca7dc66..b9b0d7f 100644 --- a/src/http-client/http-client.ts +++ b/src/http-client/http-client.ts @@ -53,16 +53,18 @@ export class HttpClient implements HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, body: unknown, - options: HttpOptionsInput = {} + options: HttpOptionsInput = {} ): Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > > { return await this.request(HttpMethod.POST, url, body, options); @@ -74,15 +76,17 @@ export class HttpClient implements HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, - options: HttpOptionsInput = {} + options: HttpOptionsInput = {} ): Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > > { return await this.request(HttpMethod.GET, url, undefined, options); @@ -94,16 +98,18 @@ export class HttpClient implements HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, body: unknown, - options: HttpOptionsInput = {} + options: HttpOptionsInput = {} ): Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > > { return await this.request(HttpMethod.PUT, url, body, options); @@ -115,16 +121,18 @@ export class HttpClient implements HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, body: unknown, - options: HttpOptionsInput = {} + options: HttpOptionsInput = {} ): Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > > { return await this.request(HttpMethod.PATCH, url, body, options); @@ -136,37 +144,41 @@ export class HttpClient implements HttpClientInterface { QueryParamsObject extends Record = Record, HeaderParamsObject extends Record = Partial>, ResponseHeaderMetaInputObject extends Record = Record, - BodyType extends BodyMetadata['type'] = MimeType.JSON + BodyType extends BodyMetadata['type'] = MimeType.JSON, + IsArray extends boolean = false >( url: string, - options: HttpOptionsInput = {} + options: HttpOptionsInput = {} ): Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > > { return await this.request(HttpMethod.DELETE, url, undefined, options); } - // eslint-disable-next-line sonar/cognitive-complexity - private async request< + // eslint-disable-next-line jsdoc/require-jsdoc, sonar/cognitive-complexity + async request< T extends object, QueryParamsObject extends Record, HeaderParamsObject extends Record, ResponseHeaderMetaInputObject extends Record, - BodyType extends BodyMetadata['type'] + BodyType extends BodyMetadata['type'], + IsArray extends boolean >( method: HttpMethod, url: string, requestBody: unknown, - options: HttpOptionsInput | undefined + options: HttpOptionsInput | undefined ): Promise< HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > > { const config: RawAxiosRequestConfig = { @@ -226,7 +238,8 @@ export class HttpClient implements HttpClientInterface { const res: HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray > = { rawBody: axiosResponse.data, body: undefined as unknown as T, @@ -236,7 +249,8 @@ export class HttpClient implements HttpClientInterface { } as HttpClientResponseForBodyType< T, HeaderMetaObjectToParamsObject>, - BodyType + BodyType, + IsArray >; if (!options?.responseBody) { return res; @@ -249,6 +263,7 @@ export class HttpClient implements HttpClientInterface { type: MimeType.JSON, cleanupAfterMs: Ms.DAY, modelClass, + isArray: false, maxSize: resolveMaxBodySize(modelClass, ('modelClass' in options.responseBody ? options.responseBody : {}).baseMaxSize), ...'modelClass' in options.responseBody ? options.responseBody : {} } as BodyMetadata; diff --git a/src/open-api/decorators/response.decorator.ts b/src/open-api/decorators/response.decorator.ts index 8b1d812..8e83fb7 100644 --- a/src/open-api/decorators/response.decorator.ts +++ b/src/open-api/decorators/response.decorator.ts @@ -1,10 +1,9 @@ /* eslint-disable jsdoc/require-returns */ -import { Property } from '../../entity'; import { HttpStatus } from '../../http'; import { ExcludeStrict, Newable, OmitStrict } from '../../types'; import { MetadataUtilities } from '../../utilities'; import { JsonOpenApiResponse, ErrorOpenApiResponse, FileOpenApiResponse, OpenApiResponse, HtmlOpenApiResponse } from '../open-api.model'; -import { PaginationResult } from '../pagination-result.model'; +import { PaginationResultClass } from '../pagination-result.model'; /** * Bundles decorators for http responses. @@ -81,21 +80,12 @@ export namespace Response { MetadataUtilities.setFilePath(ctor, stack); const responses: OpenApiResponse[] = MetadataUtilities.getRouteResponses(ctor, propertyKey.toString()); - // eslint-disable-next-line jsdoc/require-jsdoc - class Temp implements PaginationResult { - // eslint-disable-next-line jsdoc/require-jsdoc - @Property.array({ items: { type: 'object', cls: () => entityClass }, description: 'the paginated items' }) - items!: (typeof entityClass)[]; - // eslint-disable-next-line jsdoc/require-jsdoc - @Property.number({ description: 'the total amount of items' }) - totalAmount!: number; - } responses.push({ description: `response of paginated ${entityClass.name} entities`, status: HttpStatus.OK, ...data, isArray: false, - cls: Temp, + cls: PaginationResultClass(entityClass), type: 'json' }); MetadataUtilities.setRouteResponses(ctor, responses, propertyKey.toString()); diff --git a/src/open-api/open-api.model.ts b/src/open-api/open-api.model.ts index 7bf5196..937421e 100644 --- a/src/open-api/open-api.model.ts +++ b/src/open-api/open-api.model.ts @@ -33,6 +33,18 @@ export type OpenApiRequestBodyObject = oas31.RequestBodyObject; */ export type OpenApiSchemaObject = oas31.SchemaObject; +/** + * Reference definition object. + */ +export type OpenApiReferenceObject = oas31.ReferenceObject; + +/** + * Schema definitions. + */ +export type OpenApiSchemas = { + [schema: string]: OpenApiSchemaObject | OpenApiReferenceObject +}; + /** * Security scheme definition object. */ @@ -137,6 +149,31 @@ export type HtmlOpenApiResponse = { description?: string }; +/** + * Definition for a single response. + */ +export type OpenApiResponseObject = oas31.ResponseObject; + +/** + * Definition for responses. + */ +export type OpenApiResponsesObject = oas31.ResponsesObject; + +/** + * Definition for a tag. + */ +export type OpenApiTagObject = oas31.TagObject; + +/** + * The location of a parameter, like 'query', 'header' etc. + */ +export type OpenApiParameterLocation = oas31.ParameterLocation; + +/** + * Definition for a content object. + */ +export type OpenApiContentObject = oas31.ContentObject; + /** * Union of all supported OpenAPI response descriptors. */ diff --git a/src/open-api/open-api.service.ts b/src/open-api/open-api.service.ts index 6cfd8d7..a4514d1 100644 --- a/src/open-api/open-api.service.ts +++ b/src/open-api/open-api.service.ts @@ -1,6 +1,5 @@ import path from 'path'; -import { ContentObject, ParameterLocation, ResponseObject, ResponsesObject, TagObject } from 'openapi3-ts/oas31'; import swaggerUi from 'swagger-ui-express'; import { ZibriApplication } from '../application'; @@ -14,7 +13,7 @@ import { HttpMethod, HttpStatus, KnownHeader, MimeType } from '../http'; import { LoggerInterface } from '../logging'; import { BodyMetadata, ControllerRouteConfiguration, HeaderParamMetadata, PathParamMetadata, QueryParamMetadata, Route, RouteHandler } from '../routing'; import { OpenApiServiceInterface } from './open-api-service.interface'; -import { OpenApiDefinition, OpenApiOperation, OpenApiParameter, OpenApiPaths, OpenApiRequestBodyObject, OpenApiResponse, OpenApiSchemaObject, OpenApiSecurityRequirementObject, OpenApiSecuritySchemeObject } from './open-api.model'; +import { OpenApiContentObject, OpenApiDefinition, OpenApiOperation, OpenApiParameter, OpenApiParameterLocation, OpenApiPaths, OpenApiRequestBodyObject, OpenApiResponse, OpenApiResponseObject, OpenApiResponsesObject, OpenApiSchemaObject, OpenApiSecurityRequirementObject, OpenApiSecuritySchemeObject, OpenApiTagObject } from './open-api.model'; import { FileResponse } from '../parsing'; import { MissingBaseRouteError } from '../routing/missing-base-route.error'; import { Newable } from '../types'; @@ -257,7 +256,7 @@ export class OpenApiService implements OpenApiServiceInterface { // eslint-disable-next-line jsdoc/require-jsdoc async createOpenApiDefinition(app: ZibriApplication): Promise { - const tags: TagObject[] = app.options.controllers.map(cls => ({ name: cls.name })); + const tags: OpenApiTagObject[] = app.options.controllers.map(cls => ({ name: cls.name })); const res: OpenApiDefinition = { openapi: '3.1.0', info: { @@ -362,8 +361,8 @@ export class OpenApiService implements OpenApiServiceInterface { return res; } - private buildResponses(responses: OpenApiResponse[]): ResponsesObject | undefined { - const res: ResponsesObject = {}; + private buildResponses(responses: OpenApiResponse[]): OpenApiResponsesObject | undefined { + const res: OpenApiResponsesObject = {}; const groupedResponses: Record = {}; for (const response of responses) { @@ -378,7 +377,7 @@ export class OpenApiService implements OpenApiServiceInterface { for (const status in groupedResponses) { const r: OpenApiResponse[] = groupedResponses[status]; if (r.length > 1) { - const data: ResponseObject = { + const data: OpenApiResponseObject = { description: '', content: this.buildResponsesContent(r) }; @@ -386,7 +385,7 @@ export class OpenApiService implements OpenApiServiceInterface { } else { const response: OpenApiResponse = r[0]; - const data: ResponseObject = { + const data: OpenApiResponseObject = { description: response.description ?? defaultDescriptionForHttpStatus[response.status ?? 'default'], content: this.buildResponseContent(response) }; @@ -398,7 +397,7 @@ export class OpenApiService implements OpenApiServiceInterface { } // eslint-disable-next-line sonar/cognitive-complexity - private buildResponseContent(response: OpenApiResponse): ContentObject | undefined { + private buildResponseContent(response: OpenApiResponse): OpenApiContentObject | undefined { switch (response.type) { case 'file': { const schema: OpenApiSchemaObject = { type: 'string', format: 'binary' }; @@ -409,7 +408,7 @@ export class OpenApiService implements OpenApiServiceInterface { ? [response.mimeType === 'all' ? MimeType.OCTET_STREAM : response.mimeType] : [MimeType.OCTET_STREAM]; - const content: ContentObject = {}; + const content: OpenApiContentObject = {}; for (const mt of mimeTypes) { content[mt] = { schema }; } @@ -445,7 +444,7 @@ export class OpenApiService implements OpenApiServiceInterface { } - private buildResponsesContent(responses: OpenApiResponse[]): ContentObject | undefined { + private buildResponsesContent(responses: OpenApiResponse[]): OpenApiContentObject | undefined { const schemas: OpenApiSchemaObject[] = []; for (const response of responses) { switch (response.type) { @@ -677,7 +676,7 @@ export class OpenApiService implements OpenApiServiceInterface { private buildParameters( params: Record, - location: ParameterLocation + location: OpenApiParameterLocation ): OpenApiParameter[] { return Object.values(params).map(meta => ({ name: meta.name, diff --git a/src/open-api/pagination-result.model.ts b/src/open-api/pagination-result.model.ts index dcc4fac..9ef40d7 100644 --- a/src/open-api/pagination-result.model.ts +++ b/src/open-api/pagination-result.model.ts @@ -1,3 +1,6 @@ +import { Property } from '../entity'; +import { Newable } from '../types'; + /** * Type of a pagination result, consisting of the items from the current page and the total amount of items. */ @@ -10,4 +13,23 @@ export type PaginationResult = { * The total amount of items. */ totalAmount: number -}; \ No newline at end of file +}; + +// eslint-disable-next-line jsdoc/require-returns +/** + * A pagination result class, consisting of the items from the current page and the total amount of items. + * @param itemClass - The pagination item class. + */ +export function PaginationResultClass(itemClass: Newable): Newable> { + // eslint-disable-next-line jsdoc/require-jsdoc + class PaginationResultClass implements PaginationResult { + // eslint-disable-next-line jsdoc/require-jsdoc + @Property.array({ items: { type: 'object', cls: () => itemClass }, description: 'the paginated items' }) + items!: T[]; + // eslint-disable-next-line jsdoc/require-jsdoc + @Property.number({ description: 'the total amount of items' }) + totalAmount!: number; + } + + return PaginationResultClass; +} \ No newline at end of file diff --git a/src/plugin/invoicing/invoicing.plugin.ts b/src/plugin/invoicing/invoicing.plugin.ts index f18cdff..419c278 100644 --- a/src/plugin/invoicing/invoicing.plugin.ts +++ b/src/plugin/invoicing/invoicing.plugin.ts @@ -1,16 +1,15 @@ /* eslint-disable jsdoc/require-jsdoc */ -import { DiProvider, inject, Injectable } from '../../di'; -import { NoProviderError } from '../../di/errors'; -import { validateEntitiesRegistered } from '../../utilities'; -import { ZibriPlugin } from '../plugin.model'; import { ZIBRI_INVOICING_DI_TOKENS, ZibriInvoicingPluginDiProvider, ZibriInvoicingPluginDiProviders } from './invoicing.tokens'; import { Invoice, InvoicingOptions, InvoicingOptionsInput, NumberInvoices } from './models'; import { InvoiceCalcService, InvoiceNumberService, InvoicePdfService, PeppolConformanceService, XRechnungConformanceService } from './services'; +import { DiProvider, inject } from '../../di'; +import { NoProviderError } from '../../di/errors'; +import { validateEntitiesRegistered } from '../../utilities'; +import { ZibriPlugin } from '../plugin.model'; /** * Plugin that includes everything for handling invoices. */ -@Injectable() export class ZibriInvoicingPlugin extends ZibriPlugin { private readonly defaultDiProviders: Record< typeof ZIBRI_INVOICING_DI_TOKENS[keyof typeof ZIBRI_INVOICING_DI_TOKENS], diff --git a/src/plugin/invoicing/services/invoice-number.service.ts b/src/plugin/invoicing/services/invoice-number.service.ts index ce436d2..c2b8483 100644 --- a/src/plugin/invoicing/services/invoice-number.service.ts +++ b/src/plugin/invoicing/services/invoice-number.service.ts @@ -12,11 +12,10 @@ import { Invoice, InvoiceAddress, type InvoicingOptions, NumberInvoices } from ' export class InvoiceNumberService implements InvoiceNumberServiceInterface { constructor( @InjectRepository(Invoice) - private readonly invoiceRepository: Repository, + private readonly invoiceRepository: Repository>, @InjectRepository(NumberInvoices) private readonly numberInvoicesRepository: Repository< NumberInvoices, - OmitStrict, OmitStrict >, @Inject(ZIBRI_INVOICING_DI_TOKENS.OPTIONS) diff --git a/src/routing/decorators/body.decorator.ts b/src/routing/decorators/body.decorator.ts index 1f029b0..98dac69 100644 --- a/src/routing/decorators/body.decorator.ts +++ b/src/routing/decorators/body.decorator.ts @@ -10,9 +10,17 @@ import { BigNumber, MetadataUtilities, Ms } from '../../utilities'; */ type BaseBodyMetadata = OmitStrict & { /** - * The class that defines the structure of the body metadata. + * The class that defines the structure of the body. */ modelClass: Newable, + /** + * Whether or not the body is a single modelClass or an array of them. + */ + isArray: boolean, + /** + * Whether or not additional properties are allowed on the body. + */ + allowAdditionalProperties: boolean, /** * The index at which the body parameter is provided in the controller method. */ @@ -76,11 +84,13 @@ export function Body(modelClass: Newable, options: BodyMetadataInput = const fullMetadata: BodyMetadata = { index, modelClass, + isArray: false, required: true, description: undefined, type: MimeType.JSON, cleanupAfterMs: Ms.DAY, maxSize: resolveMaxBodySize(modelClass, options.baseMaxSize), + allowAdditionalProperties: false, ...options }; if ('baseMaxSize' in fullMetadata) { diff --git a/src/routing/models/crud-controller.model.ts b/src/routing/models/crud-controller.model.ts index a88dd90..0bea47e 100644 --- a/src/routing/models/crud-controller.model.ts +++ b/src/routing/models/crud-controller.model.ts @@ -86,7 +86,7 @@ export function CrudController< @Get('/:id') async findById( @Param.path('id', { type: 'string', format: 'uuid' }) - id: string + id: T['id'] ): Promise { return await this.repo.findById(id); } @@ -97,7 +97,7 @@ export function CrudController< @Patch('/:id') async updateById( @Param.path('id', { type: 'string', format: 'uuid' }) - id: string, + id: T['id'], @Body(updateDataClass) data: UpdateData ): Promise { diff --git a/src/routing/router.ts b/src/routing/router.ts index 1aae7d8..9e1c2ba 100644 --- a/src/routing/router.ts +++ b/src/routing/router.ts @@ -119,6 +119,8 @@ export class Router implements RouterInterface { description: undefined, type: MimeType.JSON, cleanupAfterMs: Ms.DAY, + isArray: false, + allowAdditionalProperties: false, maxSize: resolveMaxBodySize(input.bodyMetadata.modelClass, input.bodyMetadata.baseMaxSize), ...input.bodyMetadata } diff --git a/src/utilities/add-import-statement.function.ts b/src/utilities/add-import-statement.function.ts new file mode 100644 index 0000000..9ec8e29 --- /dev/null +++ b/src/utilities/add-import-statement.function.ts @@ -0,0 +1,55 @@ +/** + * Definition of a typescript import. + */ +export type TsImportDefinition = { + /** + * The element that should be imported. + */ + element: string, + /** + * The path of where to import from. + */ + path: string, + /** + * Wether or not the element should be a default import. + */ + defaultImport: boolean +}; + +/** + * Adds the given import to the given typescript file lines. + * @param lines - The lines of the typescript file to add the import to. + * @param imp - The import that should be added. + */ +export function addImportStatement(lines: string[], imp: TsImportDefinition): void { + if (lines.find(l => l.includes('import ') && l.includes(imp.path) && l.includes(imp.element))) { + return; + } + const existingImport: string | undefined = lines.find(l => l.includes('import ') && l.includes(imp.path)); + if (!existingImport) { + lines.unshift(getNewImportStatement(imp)); + return; + } + lines[lines.indexOf(existingImport)] = getUpdatedImportStatement(existingImport, imp); +} + +// eslint-disable-next-line jsdoc/require-jsdoc +function getNewImportStatement(imp: TsImportDefinition): string { + return imp.defaultImport + ? `import ${imp.element} from \'${imp.path}\';` + : `import { ${imp.element} } from \'${imp.path}\';`; +} + +// eslint-disable-next-line jsdoc/require-jsdoc +function getUpdatedImportStatement(existingImport: string, imp: TsImportDefinition): string { + if (imp.defaultImport && !existingImport.includes('{')) { + throw new Error(`There is already a default import from ${imp.path}`); + } + if (imp.defaultImport) { + return existingImport.replace('{', `${imp.element}, {`); + } + if (existingImport.includes('{')) { + return existingImport.replace('import {', `import { ${imp.element},`); + } + return existingImport.replace('import ', `import { ${imp.element} }, `); +} \ No newline at end of file diff --git a/src/utilities/index.ts b/src/utilities/index.ts index a7f94be..d092f0c 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -10,4 +10,7 @@ export * from './metadata.utilities'; export * from './ms'; export * from './path-exists.function'; export * from './validate-entities-registered.function'; -export * from './uuid.utilities'; \ No newline at end of file +export * from './uuid.utilities'; +export * from './to-kebab-case.function'; +export * from './to-pascal-case.function'; +export * from './to-camel-case.function'; \ No newline at end of file diff --git a/src/utilities/to-camel-case.function.ts b/src/utilities/to-camel-case.function.ts new file mode 100644 index 0000000..2d85d6a --- /dev/null +++ b/src/utilities/to-camel-case.function.ts @@ -0,0 +1,24 @@ +/** + * Transforms the given input string into camel case. + * @param input - The string to transform. + * @returns The given input in camel case. + */ +export function toCamelCase(input: string): string { + if (!input) { + return ''; + } + const normalized: string = input + .replaceAll(/([\da-z])([A-Z])/g, '$1 $2') // split camel/Pascal transitions + .replaceAll(/[._\-]+/g, ' ') + .trim(); + const parts: string[] = normalized.split(/\s+/); + if (parts.length === 0) { + return ''; + } + const first: string = parts[0].toLowerCase(); + const rest: string = parts + .slice(1) + .map(p => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()) + .join(''); + return first + rest; +} \ No newline at end of file diff --git a/src/utilities/to-kebab-case.function.ts b/src/utilities/to-kebab-case.function.ts new file mode 100644 index 0000000..e7511dc --- /dev/null +++ b/src/utilities/to-kebab-case.function.ts @@ -0,0 +1,19 @@ +/** + * Transforms the given input string into kebab case. + * @param input - The string to transform. + * @returns The given input in kebab case. + */ +export function toKebabCase(input: string): string { + return input.trim() + .replaceAll(/([\da-z])([A-Z])/g, '$1-$2') + .replaceAll(/([A-Z])([A-Z][\da-z])/g, '$1-$2') + .replaceAll(/[\s.\-]+/g, '-') + .replaceAll(/__+/g, '-') + .replaceAll('_', '-') + .replaceAll('/', '-') + .replaceAll('\\', '-') + .replaceAll('{', '-') + .replaceAll('}', '-') + .replaceAll(/^-+|-+$/g, '') + .toLowerCase(); +} \ No newline at end of file diff --git a/src/utilities/to-pascal-case.function.ts b/src/utilities/to-pascal-case.function.ts new file mode 100644 index 0000000..b40c738 --- /dev/null +++ b/src/utilities/to-pascal-case.function.ts @@ -0,0 +1,12 @@ +/** + * Transforms the given input string into pascal case. + * @param input - The string to transform. + * @returns The given input in pascal case. + */ +export function toPascalCase(input: string): string { + return input + .replaceAll(/[_\-]+/g, ' ') + .replaceAll('/', ' ') + .replaceAll(/^\w|[A-Z]|\b\w/g, (w) => w.toUpperCase()) + .replaceAll(/\s+/g, ''); +} \ No newline at end of file diff --git a/src/validation/validation.service.ts b/src/validation/validation.service.ts index 4eef435..b615b43 100644 --- a/src/validation/validation.service.ts +++ b/src/validation/validation.service.ts @@ -144,7 +144,19 @@ export class ValidationService implements ValidationServiceInterface { } const cls: Newable = meta.type === MimeType.FORM_DATA ? Temp : meta.modelClass; - const res: ValidationProblem[] = this.validateModel(body, cls, undefined); + let res: ValidationProblem[]; + if (meta.isArray) { + if (!Array.isArray(body)) { + throw new ValidationError('body', [new TypeMismatchValidationProblem('body', 'array')]); + } + res = body.reduce((prev, curr, i) => [ + ...prev, + ...this.validateModel(curr, cls, `[${i}]`, meta.allowAdditionalProperties) + ], []); + } + else { + res = this.validateModel(body, cls, undefined, meta.allowAdditionalProperties); + } if (res.length) { throw new ValidationError('body', res); } @@ -152,7 +164,7 @@ export class ValidationService implements ValidationServiceInterface { // eslint-disable-next-line jsdoc/require-jsdoc validateWebsocketRequest(req: unknown): void { - const res: ValidationProblem[] = this.validateModel(req, WebsocketRequest, undefined); + const res: ValidationProblem[] = this.validateModel(req, WebsocketRequest, undefined, false); if (res.length) { throw new ValidationError('websocketRequest', res); } @@ -183,7 +195,12 @@ export class ValidationService implements ValidationServiceInterface { // } } - private validateModel(body: unknown, cls: Newable, parentKey: string | undefined): ValidationProblem[] { + private validateModel( + body: unknown, + cls: Newable, + parentKey: string | undefined, + allowAdditionalProperties: boolean + ): ValidationProblem[] { const modelProperties: Record = MetadataUtilities.getModelProperties(cls); const keysOfBody: string[] = Object.keys(body as Record); @@ -191,6 +208,9 @@ export class ValidationService implements ValidationServiceInterface { const unknownKeys: string[] = keysOfBody.filter(k => !keysOfModel.includes(k)); const res: ValidationProblem[] = []; for (const key of unknownKeys) { + if (allowAdditionalProperties) { + continue; + } const fullKey: string = parentKey ? `${parentKey}.${key}` : key; res.push({ key: fullKey, message: 'this key is unknown' }); } diff --git a/src/websocket/decorators/websocket-body.decorator.ts b/src/websocket/decorators/websocket-body.decorator.ts index 4fa38e6..e18f7ae 100644 --- a/src/websocket/decorators/websocket-body.decorator.ts +++ b/src/websocket/decorators/websocket-body.decorator.ts @@ -30,6 +30,8 @@ export function WebsocketBody( const fullMetadata: JsonBodyMetadata = { index, modelClass, + isArray: false, + allowAdditionalProperties: false, required: true, description: undefined, type: MimeType.JSON, diff --git a/src/websocket/models/websocket-message.model.ts b/src/websocket/models/websocket-message.model.ts index 1d6fdbd..c6d03cf 100644 --- a/src/websocket/models/websocket-message.model.ts +++ b/src/websocket/models/websocket-message.model.ts @@ -75,7 +75,7 @@ export class WebsocketMessage extends BaseEntity { @Property.string({ enum: WebsocketRecipientType }) recipientType!: WebsocketRecipientType; /** - * The id of th either the channel or the user that this message was sent to. + * The id of either the channel or the user that this message was sent to. * Can be empty when the message was sent to all or to a non user connection. */ @Property.string({ required: (m: WebsocketMessage) => m.recipientType != WebsocketRecipientType.ALL, format: 'uuid' }) diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 0000000..7d48033 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index ab4a226..819b5ef 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,8 +1,9 @@ { "compilerOptions": { "target": "ES2022", - "module": "ESNext", - "moduleResolution": "Node", + "module": "nodenext", + "moduleResolution": "nodenext", + "isolatedModules": true, "outDir": "dist", "rootDir": "src", "strict": true, @@ -10,8 +11,12 @@ "skipLibCheck": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, + "forceConsistentCasingInFileNames": true, "sourceMap": true, - "forceConsistentCasingInFileNames": true + "inlineSourceMap": false, + "inlineSources": false, + "declaration": true, + "declarationMap": true, }, - "include": ["src"] + "files": ["src/index.ts"] } \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts deleted file mode 100644 index 238506b..0000000 --- a/tsup.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['src/index.ts'], - outDir: 'dist', - target: 'node20', - format: ['esm', 'cjs'], - ignoreWatch: 'sandbox', - splitting: false, - sourcemap: true, - clean: true, - dts: true, - onSuccess: 'typedoc' -}); \ No newline at end of file From b2525e00ae5f2b83f48359ddf8acd8e1fd89b0dd Mon Sep 17 00:00:00 2001 From: Tim Fabian Date: Sun, 4 Jan 2026 03:30:05 +0100 Subject: [PATCH 2/2] fixed tests --- .../generate-entity-files-for-provider.test.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/entity/generation/generate-entity-files-for-provider.test.ts b/src/entity/generation/generate-entity-files-for-provider.test.ts index b062464..1a6e0db 100644 --- a/src/entity/generation/generate-entity-files-for-provider.test.ts +++ b/src/entity/generation/generate-entity-files-for-provider.test.ts @@ -39,7 +39,8 @@ function expectDecoratorAboveProperty( describe('generateEntityFiles', () => { it('PetStore', async () => { const provider: OpenApiUrlProvider = new OpenApiUrlProvider('PetStore', 'https://petstore.swagger.io/v2/swagger.json'); - const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + const { filesToGenerate } = await generateEntityFilesForProvider(provider, 'test'); + const indexLines: string[] = filesToGenerate.find(f => f.path.endsWith('index.ts'))?.lines ?? []; const pascalPrefix: string = toPascalCase(provider.prefix); const kebabPrefix: string = toKebabCase(provider.prefix); @@ -235,7 +236,8 @@ describe('generateEntityFiles', () => { }; const provider: InlineProvider = new InlineProvider('InlineSvc', spec); - const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + const { filesToGenerate } = await generateEntityFilesForProvider(provider, 'test'); + const indexLines: string[] = filesToGenerate.find(f => f.path.endsWith('index.ts'))?.lines ?? []; // expected names: prefix + title const parentClass: string = `${toPascalCase(provider.prefix)}Container`; @@ -312,7 +314,8 @@ describe('generateEntityFiles', () => { }; const provider: InlineProvider = new InlineProvider('RefSvc', spec); - const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + const { filesToGenerate } = await generateEntityFilesForProvider(provider, 'test'); + const indexLines: string[] = filesToGenerate.find(f => f.path.endsWith('index.ts'))?.lines ?? []; const hostClass: string = `${toPascalCase(provider.prefix)}${toPascalCase('Host')}`; const refedClass: string = `${toPascalCase(provider.prefix)}${toPascalCase('Refed')}`; @@ -381,7 +384,8 @@ describe('generateEntityFiles', () => { }; const provider: InlineProvider = new InlineProvider('Api', spec); - const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + const { filesToGenerate } = await generateEntityFilesForProvider(provider, 'test'); + const indexLines: string[] = filesToGenerate.find(f => f.path.endsWith('index.ts'))?.lines ?? []; const childClass: string = `${toPascalCase(provider.prefix)}CreateUserPayload`; const childFile: FileToGenerate | undefined = findFile(filesToGenerate, childClass); @@ -443,7 +447,8 @@ describe('generateEntityFiles', () => { }; const provider: InlineProvider = new InlineProvider('Api', spec); - const { filesToGenerate, indexLines } = await generateEntityFilesForProvider(provider, 'test'); + const { filesToGenerate } = await generateEntityFilesForProvider(provider, 'test'); + const indexLines: string[] = filesToGenerate.find(f => f.path.endsWith('index.ts'))?.lines ?? []; const childClass: string = `${toPascalCase(provider.prefix)}PetResponse`; const childFile: FileToGenerate | undefined = findFile(filesToGenerate, childClass);