adorn-api 1.0.22 → 1.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +3 -0
- package/.eslintrc.cjs +30 -0
- package/README.md +375 -531
- package/dist/core/express-adapter.d.ts +27 -0
- package/dist/core/express-adapter.d.ts.map +1 -0
- package/dist/core/express-adapter.js +146 -0
- package/dist/core/express-adapter.js.map +1 -0
- package/dist/core/http-error.d.ts +7 -0
- package/dist/core/http-error.d.ts.map +1 -0
- package/dist/core/http-error.js +14 -0
- package/dist/core/http-error.js.map +1 -0
- package/dist/decorators/controller.decorator.d.ts +2 -0
- package/dist/decorators/controller.decorator.d.ts.map +1 -0
- package/dist/decorators/controller.decorator.js +26 -0
- package/dist/decorators/controller.decorator.js.map +1 -0
- package/dist/decorators/create.decorator.d.ts +8 -0
- package/dist/decorators/create.decorator.d.ts.map +1 -0
- package/dist/decorators/create.decorator.js +67 -0
- package/dist/decorators/create.decorator.js.map +1 -0
- package/dist/decorators/http-method.decorator.d.ts +16 -0
- package/dist/decorators/http-method.decorator.d.ts.map +1 -0
- package/dist/decorators/http-method.decorator.js +117 -0
- package/dist/decorators/http-method.decorator.js.map +1 -0
- package/dist/decorators/http-params.d.ts +17 -0
- package/dist/decorators/http-params.d.ts.map +1 -0
- package/dist/decorators/http-params.js +26 -0
- package/dist/decorators/http-params.js.map +1 -0
- package/dist/decorators/index.d.ts +10 -5
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +14 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/decorators/list.decorator.d.ts +18 -0
- package/dist/decorators/list.decorator.d.ts.map +1 -0
- package/dist/decorators/list.decorator.js +99 -0
- package/dist/decorators/list.decorator.js.map +1 -0
- package/dist/decorators/middleware.decorator.d.ts +4 -0
- package/dist/decorators/middleware.decorator.d.ts.map +1 -0
- package/dist/decorators/middleware.decorator.js +34 -0
- package/dist/decorators/middleware.decorator.js.map +1 -0
- package/dist/decorators/response.decorator.d.ts +8 -0
- package/dist/decorators/response.decorator.d.ts.map +1 -0
- package/dist/decorators/response.decorator.js +44 -0
- package/dist/decorators/response.decorator.js.map +1 -0
- package/dist/decorators/route-options.d.ts +14 -0
- package/dist/decorators/route-options.d.ts.map +1 -0
- package/dist/decorators/route-options.js +22 -0
- package/dist/decorators/route-options.js.map +1 -0
- package/dist/decorators/schema.decorator.d.ts +82 -0
- package/dist/decorators/schema.decorator.d.ts.map +1 -0
- package/dist/decorators/schema.decorator.js +123 -0
- package/dist/decorators/schema.decorator.js.map +1 -0
- package/dist/decorators/update.decorator.d.ts +8 -0
- package/dist/decorators/update.decorator.d.ts.map +1 -0
- package/dist/decorators/update.decorator.js +63 -0
- package/dist/decorators/update.decorator.js.map +1 -0
- package/dist/index.d.ts +11 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -637
- package/dist/index.js.map +1 -1
- package/dist/metadata/metadata-storage.d.ts +38 -0
- package/dist/metadata/metadata-storage.d.ts.map +1 -0
- package/dist/metadata/metadata-storage.js +102 -0
- package/dist/metadata/metadata-storage.js.map +1 -0
- package/dist/metal-orm-integration/dto-helper.d.ts +5 -0
- package/dist/metal-orm-integration/dto-helper.d.ts.map +1 -0
- package/dist/metal-orm-integration/dto-helper.js +48 -0
- package/dist/metal-orm-integration/dto-helper.js.map +1 -0
- package/dist/metal-orm-integration/dto-response.decorator.d.ts +4 -0
- package/dist/metal-orm-integration/dto-response.decorator.d.ts.map +1 -0
- package/dist/metal-orm-integration/dto-response.decorator.js +69 -0
- package/dist/metal-orm-integration/dto-response.decorator.js.map +1 -0
- package/dist/metal-orm-integration/entity-schema-builder.d.ts +20 -0
- package/dist/metal-orm-integration/entity-schema-builder.d.ts.map +1 -0
- package/dist/metal-orm-integration/entity-schema-builder.js +356 -0
- package/dist/metal-orm-integration/entity-schema-builder.js.map +1 -0
- package/dist/metal-orm-integration/index.d.ts +5 -0
- package/dist/metal-orm-integration/index.d.ts.map +1 -0
- package/dist/metal-orm-integration/index.js +5 -0
- package/dist/metal-orm-integration/index.js.map +1 -0
- package/dist/metal-orm-integration/schema-modifier.d.ts +11 -0
- package/dist/metal-orm-integration/schema-modifier.d.ts.map +1 -0
- package/dist/metal-orm-integration/schema-modifier.js +62 -0
- package/dist/metal-orm-integration/schema-modifier.js.map +1 -0
- package/dist/openapi/index.d.ts +4 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +4 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/openapi-generator.d.ts +22 -0
- package/dist/openapi/openapi-generator.d.ts.map +1 -0
- package/dist/openapi/openapi-generator.js +428 -0
- package/dist/openapi/openapi-generator.js.map +1 -0
- package/dist/openapi/swagger-ui.d.ts +11 -0
- package/dist/openapi/swagger-ui.d.ts.map +1 -0
- package/dist/openapi/swagger-ui.js +20 -0
- package/dist/openapi/swagger-ui.js.map +1 -0
- package/dist/openapi/zod-to-openapi.d.ts +4 -0
- package/dist/openapi/zod-to-openapi.d.ts.map +1 -0
- package/dist/openapi/zod-to-openapi.js +184 -0
- package/dist/openapi/zod-to-openapi.js.map +1 -0
- package/dist/types/common.d.ts +4 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +2 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/controller.d.ts +14 -0
- package/dist/types/controller.d.ts.map +1 -0
- package/dist/types/controller.js +2 -0
- package/dist/types/controller.js.map +1 -0
- package/dist/types/metadata.d.ts +48 -0
- package/dist/types/metadata.d.ts.map +1 -0
- package/dist/types/metadata.js +2 -0
- package/dist/types/metadata.js.map +1 -0
- package/dist/types/openapi.d.ts +30 -0
- package/dist/types/openapi.d.ts.map +1 -0
- package/dist/types/openapi.js +2 -0
- package/dist/types/openapi.js.map +1 -0
- package/dist/validation/zod-adapter.d.ts +15 -0
- package/dist/validation/zod-adapter.d.ts.map +1 -0
- package/dist/validation/zod-adapter.js +61 -0
- package/dist/validation/zod-adapter.js.map +1 -0
- package/examples/basic/app.ts +15 -0
- package/examples/basic/index.ts +6 -0
- package/examples/basic/user.controller.ts +35 -0
- package/examples/basic/user.dtos.ts +23 -0
- package/examples/metal-orm-sqlite/app.ts +18 -0
- package/examples/metal-orm-sqlite/db.ts +90 -0
- package/examples/metal-orm-sqlite/index.ts +6 -0
- package/examples/metal-orm-sqlite/post.controller.ts +209 -0
- package/examples/metal-orm-sqlite/post.dtos.ts +78 -0
- package/examples/metal-orm-sqlite/post.entity.ts +24 -0
- package/examples/metal-orm-sqlite/user.controller.helpers.ts +305 -0
- package/examples/metal-orm-sqlite/user.controller.ts +231 -0
- package/examples/metal-orm-sqlite/user.dtos.ts +88 -0
- package/examples/metal-orm-sqlite/user.entity.ts +21 -0
- package/examples/metal-orm-sqlite-music/album.controller.ts +278 -0
- package/examples/metal-orm-sqlite-music/album.dtos.ts +85 -0
- package/examples/metal-orm-sqlite-music/album.entity.ts +28 -0
- package/examples/metal-orm-sqlite-music/app.ts +19 -0
- package/examples/metal-orm-sqlite-music/artist.controller.ts +272 -0
- package/examples/metal-orm-sqlite-music/artist.dtos.ts +68 -0
- package/examples/metal-orm-sqlite-music/artist.entity.ts +27 -0
- package/examples/metal-orm-sqlite-music/db.ts +148 -0
- package/examples/metal-orm-sqlite-music/index.ts +6 -0
- package/examples/metal-orm-sqlite-music/track.controller.ts +221 -0
- package/examples/metal-orm-sqlite-music/track.dtos.ts +82 -0
- package/examples/metal-orm-sqlite-music/track.entity.ts +27 -0
- package/examples/openapi/health.controller.ts +11 -0
- package/examples/openapi/health.dto.ts +7 -0
- package/examples/openapi/index.ts +12 -0
- package/examples/restful/app.ts +15 -0
- package/examples/restful/index.ts +9 -0
- package/examples/restful/task.controller.ts +118 -0
- package/examples/restful/task.dtos.ts +66 -0
- package/examples/restful/task.store.ts +95 -0
- package/examples/tsconfig.json +8 -0
- package/examples/utils/start-server.ts +56 -0
- package/package.json +33 -97
- package/scripts/run-example.js +29 -0
- package/src/adapter/express.ts +589 -0
- package/src/adapter/metal-orm/convention-overrides.ts +115 -0
- package/src/adapter/metal-orm/crud-dtos.ts +141 -0
- package/src/adapter/metal-orm/dto.ts +20 -0
- package/src/adapter/metal-orm/error-dtos.ts +51 -0
- package/src/adapter/metal-orm/field-builder.ts +185 -0
- package/src/adapter/metal-orm/filters.ts +52 -0
- package/src/adapter/metal-orm/index.ts +66 -0
- package/src/adapter/metal-orm/paged-dtos.ts +94 -0
- package/src/adapter/metal-orm/pagination.ts +28 -0
- package/src/adapter/metal-orm/types.ts +250 -0
- package/src/adapter/metal-orm/utils.ts +36 -0
- package/src/adapter/metal-orm.test.ts +439 -0
- package/src/core/__tests__/coerce.test.ts +39 -0
- package/src/core/__tests__/dto-compose.test.ts +68 -0
- package/src/core/__tests__/schema-builder.test.ts +82 -0
- package/src/core/coerce.ts +190 -0
- package/src/core/decorators.ts +645 -0
- package/src/core/errors.ts +55 -0
- package/src/core/metadata.ts +110 -0
- package/src/core/openapi.ts +282 -0
- package/src/core/schema-builder.ts +287 -0
- package/src/core/schema.ts +400 -0
- package/src/core/types.ts +14 -0
- package/src/e2e/http-error.e2e.test.ts +52 -0
- package/src/e2e/sqlite-metal-orm.e2e.test.ts +174 -0
- package/src/e2e/sqlite.e2e.test.ts +126 -0
- package/src/index.ts +8 -0
- package/tsconfig.eslint.json +7 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +8 -0
- package/dist/adapter/express/auth.d.ts +0 -13
- package/dist/adapter/express/auth.d.ts.map +0 -1
- package/dist/adapter/express/bootstrap.d.ts +0 -40
- package/dist/adapter/express/bootstrap.d.ts.map +0 -1
- package/dist/adapter/express/coercion.d.ts +0 -102
- package/dist/adapter/express/coercion.d.ts.map +0 -1
- package/dist/adapter/express/index.d.ts +0 -6
- package/dist/adapter/express/index.d.ts.map +0 -1
- package/dist/adapter/express/merge.d.ts +0 -45
- package/dist/adapter/express/merge.d.ts.map +0 -1
- package/dist/adapter/express/openapi.d.ts +0 -66
- package/dist/adapter/express/openapi.d.ts.map +0 -1
- package/dist/adapter/express/router.d.ts +0 -10
- package/dist/adapter/express/router.d.ts.map +0 -1
- package/dist/adapter/express/swagger.d.ts +0 -18
- package/dist/adapter/express/swagger.d.ts.map +0 -1
- package/dist/adapter/express/types.d.ts +0 -110
- package/dist/adapter/express/types.d.ts.map +0 -1
- package/dist/adapter/express/validation.d.ts +0 -27
- package/dist/adapter/express/validation.d.ts.map +0 -1
- package/dist/cli/progress.d.ts +0 -122
- package/dist/cli/progress.d.ts.map +0 -1
- package/dist/cli.cjs +0 -4390
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -4371
- package/dist/cli.js.map +0 -1
- package/dist/compiler/analyze/index.d.ts +0 -5
- package/dist/compiler/analyze/index.d.ts.map +0 -1
- package/dist/compiler/analyze/scanControllers.d.ts +0 -88
- package/dist/compiler/analyze/scanControllers.d.ts.map +0 -1
- package/dist/compiler/cache/isStale.d.ts +0 -46
- package/dist/compiler/cache/isStale.d.ts.map +0 -1
- package/dist/compiler/cache/loadArtifacts.d.ts +0 -149
- package/dist/compiler/cache/loadArtifacts.d.ts.map +0 -1
- package/dist/compiler/cache/schema.d.ts +0 -32
- package/dist/compiler/cache/schema.d.ts.map +0 -1
- package/dist/compiler/cache/writeCache.d.ts +0 -14
- package/dist/compiler/cache/writeCache.d.ts.map +0 -1
- package/dist/compiler/gems.d.ts +0 -75
- package/dist/compiler/gems.d.ts.map +0 -1
- package/dist/compiler/generator/index.d.ts +0 -7
- package/dist/compiler/generator/index.d.ts.map +0 -1
- package/dist/compiler/generator/manifest.d.ts +0 -23
- package/dist/compiler/generator/manifest.d.ts.map +0 -1
- package/dist/compiler/generator/openapi.d.ts +0 -118
- package/dist/compiler/generator/openapi.d.ts.map +0 -1
- package/dist/compiler/graph/builder.d.ts +0 -24
- package/dist/compiler/graph/builder.d.ts.map +0 -1
- package/dist/compiler/graph/index.d.ts +0 -7
- package/dist/compiler/graph/index.d.ts.map +0 -1
- package/dist/compiler/graph/schemaGraph.d.ts +0 -67
- package/dist/compiler/graph/schemaGraph.d.ts.map +0 -1
- package/dist/compiler/graph/types.d.ts +0 -203
- package/dist/compiler/graph/types.d.ts.map +0 -1
- package/dist/compiler/index.d.ts +0 -12
- package/dist/compiler/index.d.ts.map +0 -1
- package/dist/compiler/ir/index.d.ts +0 -7
- package/dist/compiler/ir/index.d.ts.map +0 -1
- package/dist/compiler/ir/pipeline.d.ts +0 -82
- package/dist/compiler/ir/pipeline.d.ts.map +0 -1
- package/dist/compiler/ir/stages.d.ts +0 -40
- package/dist/compiler/ir/stages.d.ts.map +0 -1
- package/dist/compiler/ir/visitor.d.ts +0 -98
- package/dist/compiler/ir/visitor.d.ts.map +0 -1
- package/dist/compiler/manifest/emit.d.ts +0 -21
- package/dist/compiler/manifest/emit.d.ts.map +0 -1
- package/dist/compiler/manifest/format.d.ts +0 -119
- package/dist/compiler/manifest/format.d.ts.map +0 -1
- package/dist/compiler/manifest/index.d.ts +0 -6
- package/dist/compiler/manifest/index.d.ts.map +0 -1
- package/dist/compiler/runner/createProgram.d.ts +0 -24
- package/dist/compiler/runner/createProgram.d.ts.map +0 -1
- package/dist/compiler/runner/index.d.ts +0 -5
- package/dist/compiler/runner/index.d.ts.map +0 -1
- package/dist/compiler/schema/extractAnnotations.d.ts +0 -57
- package/dist/compiler/schema/extractAnnotations.d.ts.map +0 -1
- package/dist/compiler/schema/index.d.ts +0 -8
- package/dist/compiler/schema/index.d.ts.map +0 -1
- package/dist/compiler/schema/intersectionHandler.d.ts +0 -44
- package/dist/compiler/schema/intersectionHandler.d.ts.map +0 -1
- package/dist/compiler/schema/objectHandler.d.ts +0 -146
- package/dist/compiler/schema/objectHandler.d.ts.map +0 -1
- package/dist/compiler/schema/openapi.d.ts +0 -71
- package/dist/compiler/schema/openapi.d.ts.map +0 -1
- package/dist/compiler/schema/parameters.d.ts +0 -90
- package/dist/compiler/schema/parameters.d.ts.map +0 -1
- package/dist/compiler/schema/partitioner.d.ts +0 -85
- package/dist/compiler/schema/partitioner.d.ts.map +0 -1
- package/dist/compiler/schema/primitives.d.ts +0 -68
- package/dist/compiler/schema/primitives.d.ts.map +0 -1
- package/dist/compiler/schema/queryBuilderAnalyzer.d.ts +0 -76
- package/dist/compiler/schema/queryBuilderAnalyzer.d.ts.map +0 -1
- package/dist/compiler/schema/queryBuilderSchemaBuilder.d.ts +0 -13
- package/dist/compiler/schema/queryBuilderSchemaBuilder.d.ts.map +0 -1
- package/dist/compiler/schema/splitOpenapi.d.ts +0 -46
- package/dist/compiler/schema/splitOpenapi.d.ts.map +0 -1
- package/dist/compiler/schema/typeToJsonSchema.d.ts +0 -26
- package/dist/compiler/schema/typeToJsonSchema.d.ts.map +0 -1
- package/dist/compiler/schema/types.d.ts +0 -70
- package/dist/compiler/schema/types.d.ts.map +0 -1
- package/dist/compiler/schema/unionHandler.d.ts +0 -70
- package/dist/compiler/schema/unionHandler.d.ts.map +0 -1
- package/dist/compiler/transform/dedup.d.ts +0 -35
- package/dist/compiler/transform/dedup.d.ts.map +0 -1
- package/dist/compiler/transform/flatten.d.ts +0 -50
- package/dist/compiler/transform/flatten.d.ts.map +0 -1
- package/dist/compiler/transform/index.d.ts +0 -7
- package/dist/compiler/transform/index.d.ts.map +0 -1
- package/dist/compiler/transform/inline.d.ts +0 -46
- package/dist/compiler/transform/inline.d.ts.map +0 -1
- package/dist/compiler/validation/emitPrecompiledValidators.d.ts +0 -62
- package/dist/compiler/validation/emitPrecompiledValidators.d.ts.map +0 -1
- package/dist/compiler/validation/index.d.ts +0 -5
- package/dist/compiler/validation/index.d.ts.map +0 -1
- package/dist/decorators/Auth.d.ts +0 -22
- package/dist/decorators/Auth.d.ts.map +0 -1
- package/dist/decorators/Controller.d.ts +0 -17
- package/dist/decorators/Controller.d.ts.map +0 -1
- package/dist/decorators/Public.d.ts +0 -15
- package/dist/decorators/Public.d.ts.map +0 -1
- package/dist/decorators/Use.d.ts +0 -23
- package/dist/decorators/Use.d.ts.map +0 -1
- package/dist/decorators/methods.d.ts +0 -26
- package/dist/decorators/methods.d.ts.map +0 -1
- package/dist/express.cjs +0 -1186
- package/dist/express.cjs.map +0 -1
- package/dist/express.d.ts +0 -8
- package/dist/express.d.ts.map +0 -1
- package/dist/express.js +0 -1150
- package/dist/express.js.map +0 -1
- package/dist/http.d.ts +0 -33
- package/dist/http.d.ts.map +0 -1
- package/dist/index.cjs +0 -724
- package/dist/index.cjs.map +0 -1
- package/dist/metal/applyListQuery.d.ts +0 -100
- package/dist/metal/applyListQuery.d.ts.map +0 -1
- package/dist/metal/index.cjs +0 -278
- package/dist/metal/index.cjs.map +0 -1
- package/dist/metal/index.d.ts +0 -15
- package/dist/metal/index.d.ts.map +0 -1
- package/dist/metal/index.js +0 -243
- package/dist/metal/index.js.map +0 -1
- package/dist/metal/listQuery.d.ts +0 -26
- package/dist/metal/listQuery.d.ts.map +0 -1
- package/dist/metal/queryOptions.d.ts +0 -16
- package/dist/metal/queryOptions.d.ts.map +0 -1
- package/dist/metal/readMetalBag.d.ts +0 -69
- package/dist/metal/readMetalBag.d.ts.map +0 -1
- package/dist/metal/registerMetalEntities.d.ts +0 -26
- package/dist/metal/registerMetalEntities.d.ts.map +0 -1
- package/dist/metal/schemaFromEntity.d.ts +0 -41
- package/dist/metal/schemaFromEntity.d.ts.map +0 -1
- package/dist/metal/searchWhere.d.ts +0 -97
- package/dist/metal/searchWhere.d.ts.map +0 -1
- package/dist/metal/symbolMetadata.d.ts +0 -8
- package/dist/metal/symbolMetadata.d.ts.map +0 -1
- package/dist/runtime/auth/runtime.d.ts +0 -183
- package/dist/runtime/auth/runtime.d.ts.map +0 -1
- package/dist/runtime/metadata/bucket.d.ts +0 -2
- package/dist/runtime/metadata/bucket.d.ts.map +0 -1
- package/dist/runtime/metadata/key.d.ts +0 -2
- package/dist/runtime/metadata/key.d.ts.map +0 -1
- package/dist/runtime/metadata/read.d.ts +0 -2
- package/dist/runtime/metadata/read.d.ts.map +0 -1
- package/dist/runtime/metadata/types.d.ts +0 -95
- package/dist/runtime/metadata/types.d.ts.map +0 -1
- package/dist/runtime/polyfill.d.ts +0 -2
- package/dist/runtime/polyfill.d.ts.map +0 -1
- package/dist/runtime/upload.d.ts +0 -44
- package/dist/runtime/upload.d.ts.map +0 -1
- package/dist/runtime/validation/ajv.d.ts +0 -120
- package/dist/runtime/validation/ajv.d.ts.map +0 -1
- package/dist/runtime/validation/index.d.ts +0 -11
- package/dist/runtime/validation/index.d.ts.map +0 -1
- package/dist/schema/decorators.d.ts +0 -37
- package/dist/schema/decorators.d.ts.map +0 -1
- package/dist/schema/index.cjs +0 -214
- package/dist/schema/index.cjs.map +0 -1
- package/dist/schema/index.d.ts +0 -2
- package/dist/schema/index.d.ts.map +0 -1
- package/dist/schema/index.js +0 -163
- package/dist/schema/index.js.map +0 -1
- package/dist/scripts/adorn-example.cjs +0 -404
- package/dist/scripts/adorn-example.cjs.map +0 -1
- package/dist/utils/operationId.d.ts +0 -2
- package/dist/utils/operationId.d.ts.map +0 -1
- package/dist/utils/path.d.ts +0 -2
- package/dist/utils/path.d.ts.map +0 -1
- package/dist/utils/port.d.ts +0 -9
- package/dist/utils/port.d.ts.map +0 -1
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import express, { type Request, type Response, type NextFunction } from "express";
|
|
2
|
+
import { buildOpenApi, type OpenApiInfo, type OpenApiServer } from "../core/openapi";
|
|
3
|
+
import type { Constructor, DtoConstructor } from "../core/types";
|
|
4
|
+
import { getControllerMeta, getDtoMeta, type InputMeta } from "../core/metadata";
|
|
5
|
+
import type {
|
|
6
|
+
SchemaNode,
|
|
7
|
+
SchemaSource,
|
|
8
|
+
ArraySchema,
|
|
9
|
+
NumberSchema,
|
|
10
|
+
ObjectSchema,
|
|
11
|
+
RecordSchema,
|
|
12
|
+
RefSchema,
|
|
13
|
+
UnionSchema
|
|
14
|
+
} from "../core/schema";
|
|
15
|
+
import { coerce } from "../core/coerce";
|
|
16
|
+
import { HttpError, isHttpError } from "../core/errors";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Request context provided to route handlers.
|
|
20
|
+
*/
|
|
21
|
+
export interface RequestContext<
|
|
22
|
+
TBody = unknown,
|
|
23
|
+
TQuery extends object | undefined = Record<string, unknown>,
|
|
24
|
+
TParams extends object | undefined = Record<string, string | number | boolean | undefined>,
|
|
25
|
+
THeaders extends object | undefined = Record<string, string | string[] | undefined>
|
|
26
|
+
> {
|
|
27
|
+
/** Express request object */
|
|
28
|
+
req: Request;
|
|
29
|
+
/** Express response object */
|
|
30
|
+
res: Response;
|
|
31
|
+
/** Parsed request body */
|
|
32
|
+
body: TBody;
|
|
33
|
+
/** Parsed query parameters */
|
|
34
|
+
query: TQuery;
|
|
35
|
+
/** Parsed path parameters */
|
|
36
|
+
params: TParams;
|
|
37
|
+
/** Request headers */
|
|
38
|
+
headers: THeaders;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Input coercion modes.
|
|
43
|
+
*/
|
|
44
|
+
export type InputCoercionMode = "safe" | "strict";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Input coercion setting - can be a mode or disabled.
|
|
48
|
+
*/
|
|
49
|
+
export type InputCoercionSetting = InputCoercionMode | false;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Options for creating an Express application adapter.
|
|
53
|
+
*/
|
|
54
|
+
export interface ExpressAdapterOptions {
|
|
55
|
+
/** Array of controller classes */
|
|
56
|
+
controllers: Constructor[];
|
|
57
|
+
/** Whether to enable JSON body parsing */
|
|
58
|
+
jsonBody?: boolean;
|
|
59
|
+
/** OpenAPI configuration */
|
|
60
|
+
openApi?: OpenApiExpressOptions;
|
|
61
|
+
/** Input coercion setting */
|
|
62
|
+
inputCoercion?: InputCoercionSetting;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates an Express application with Adorn controllers.
|
|
67
|
+
* @param options - Express adapter options
|
|
68
|
+
* @returns Configured Express application
|
|
69
|
+
*/
|
|
70
|
+
export function createExpressApp(options: ExpressAdapterOptions): express.Express {
|
|
71
|
+
const app = express();
|
|
72
|
+
if (options.jsonBody ?? true) {
|
|
73
|
+
app.use(express.json());
|
|
74
|
+
}
|
|
75
|
+
const inputCoercion = options.inputCoercion ?? "safe";
|
|
76
|
+
attachControllers(app, options.controllers, inputCoercion);
|
|
77
|
+
if (options.openApi) {
|
|
78
|
+
attachOpenApi(app, options.controllers, options.openApi);
|
|
79
|
+
}
|
|
80
|
+
return app;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Options for OpenAPI documentation UI.
|
|
85
|
+
*/
|
|
86
|
+
export interface OpenApiDocsOptions {
|
|
87
|
+
/** Path for documentation UI */
|
|
88
|
+
path?: string;
|
|
89
|
+
/** Title for documentation page */
|
|
90
|
+
title?: string;
|
|
91
|
+
/** URL for Swagger UI assets */
|
|
92
|
+
swaggerUiUrl?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* OpenAPI configuration for Express adapter.
|
|
97
|
+
*/
|
|
98
|
+
export interface OpenApiExpressOptions {
|
|
99
|
+
/** OpenAPI document info */
|
|
100
|
+
info: OpenApiInfo;
|
|
101
|
+
/** Array of servers */
|
|
102
|
+
servers?: OpenApiServer[];
|
|
103
|
+
/** Path for OpenAPI JSON endpoint */
|
|
104
|
+
path?: string;
|
|
105
|
+
/** Documentation UI configuration */
|
|
106
|
+
docs?: boolean | OpenApiDocsOptions;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Attaches controllers to an Express application.
|
|
111
|
+
* @param app - Express application instance
|
|
112
|
+
* @param controllers - Array of controller classes
|
|
113
|
+
* @param inputCoercion - Input coercion setting
|
|
114
|
+
*/
|
|
115
|
+
export function attachControllers(
|
|
116
|
+
app: express.Express,
|
|
117
|
+
controllers: Constructor[],
|
|
118
|
+
inputCoercion: InputCoercionSetting = "safe"
|
|
119
|
+
): void {
|
|
120
|
+
for (const controller of controllers) {
|
|
121
|
+
const meta = getControllerMeta(controller);
|
|
122
|
+
if (!meta) {
|
|
123
|
+
throw new Error(`Controller "${controller.name}" is missing @Controller decorator.`);
|
|
124
|
+
}
|
|
125
|
+
const instance = new controller();
|
|
126
|
+
for (const route of meta.routes) {
|
|
127
|
+
const path = joinPaths(meta.basePath, route.path);
|
|
128
|
+
const handler = instance[route.handlerName as keyof typeof instance];
|
|
129
|
+
if (typeof handler !== "function") {
|
|
130
|
+
throw new Error(`Handler "${String(route.handlerName)}" is not a function on ${controller.name}.`);
|
|
131
|
+
}
|
|
132
|
+
const coerceParams = inputCoercion === false
|
|
133
|
+
? undefined
|
|
134
|
+
: createInputCoercer<Record<string, string | number | boolean | undefined>>(
|
|
135
|
+
route.params,
|
|
136
|
+
{ mode: inputCoercion, location: "params" }
|
|
137
|
+
);
|
|
138
|
+
const coerceQuery = inputCoercion === false
|
|
139
|
+
? undefined
|
|
140
|
+
: createInputCoercer(route.query, { mode: inputCoercion, location: "query" });
|
|
141
|
+
app[route.httpMethod](path, async (req: Request, res: Response, next: NextFunction) => {
|
|
142
|
+
try {
|
|
143
|
+
const ctx: RequestContext = {
|
|
144
|
+
req,
|
|
145
|
+
res,
|
|
146
|
+
body: req.body,
|
|
147
|
+
query: coerceQuery ? coerceQuery(req.query) : req.query,
|
|
148
|
+
params: coerceParams ? coerceParams(req.params) : req.params,
|
|
149
|
+
headers: req.headers
|
|
150
|
+
};
|
|
151
|
+
const result = await handler.call(instance, ctx);
|
|
152
|
+
if (res.headersSent) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (result === undefined) {
|
|
156
|
+
res.status(defaultStatus(route)).end();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
res.status(defaultStatus(route)).json(result);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (isHttpError(error)) {
|
|
162
|
+
sendHttpError(res, error);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
next(error);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function attachOpenApi(
|
|
173
|
+
app: express.Express,
|
|
174
|
+
controllers: Constructor[],
|
|
175
|
+
options: OpenApiExpressOptions
|
|
176
|
+
): void {
|
|
177
|
+
const openApiPath = normalizePath(options.path, "/openapi.json");
|
|
178
|
+
const document = buildOpenApi({
|
|
179
|
+
info: options.info,
|
|
180
|
+
servers: options.servers,
|
|
181
|
+
controllers
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
app.get(openApiPath, (_req, res) => {
|
|
185
|
+
res.json(document);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!options.docs) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const docsOptions = typeof options.docs === "object" ? options.docs : {};
|
|
193
|
+
const docsPath = normalizePath(docsOptions.path, "/docs");
|
|
194
|
+
const title = docsOptions.title ?? `${options.info.title} Docs`;
|
|
195
|
+
const swaggerUiUrl = (docsOptions.swaggerUiUrl ?? "https://unpkg.com/swagger-ui-dist@5").replace(
|
|
196
|
+
/\/+$/,
|
|
197
|
+
""
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
const html = buildSwaggerUiHtml({ title, swaggerUiUrl, openApiPath });
|
|
201
|
+
app.get(docsPath, (_req, res) => {
|
|
202
|
+
res.type("html").send(html);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildSwaggerUiHtml(options: {
|
|
207
|
+
title: string;
|
|
208
|
+
swaggerUiUrl: string;
|
|
209
|
+
openApiPath: string;
|
|
210
|
+
}): string {
|
|
211
|
+
return `<!doctype html>
|
|
212
|
+
<html lang="en">
|
|
213
|
+
<head>
|
|
214
|
+
<meta charset="utf-8" />
|
|
215
|
+
<title>${options.title}</title>
|
|
216
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
217
|
+
<link rel="stylesheet" href="${options.swaggerUiUrl}/swagger-ui.css" />
|
|
218
|
+
<style>
|
|
219
|
+
body {
|
|
220
|
+
margin: 0;
|
|
221
|
+
background: #f6f6f6;
|
|
222
|
+
}
|
|
223
|
+
</style>
|
|
224
|
+
</head>
|
|
225
|
+
<body>
|
|
226
|
+
<div id="swagger-ui"></div>
|
|
227
|
+
<script src="${options.swaggerUiUrl}/swagger-ui-bundle.js"></script>
|
|
228
|
+
<script>
|
|
229
|
+
window.onload = () => {
|
|
230
|
+
window.ui = SwaggerUIBundle({
|
|
231
|
+
url: "${options.openApiPath}",
|
|
232
|
+
dom_id: "#swagger-ui",
|
|
233
|
+
deepLinking: true,
|
|
234
|
+
presets: [SwaggerUIBundle.presets.apis],
|
|
235
|
+
layout: "BaseLayout"
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
</script>
|
|
239
|
+
</body>
|
|
240
|
+
</html>`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function defaultStatus(route: {
|
|
244
|
+
responses?: Array<{ status: number; error?: boolean }>;
|
|
245
|
+
}): number {
|
|
246
|
+
const responses = route.responses ?? [];
|
|
247
|
+
const success = responses.find(
|
|
248
|
+
(response) => !response.error && response.status < 400
|
|
249
|
+
);
|
|
250
|
+
return success?.status ?? 200;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function joinPaths(basePath: string, routePath: string): string {
|
|
254
|
+
const base = basePath ? basePath.replace(/\/+$/, "") : "";
|
|
255
|
+
const route = routePath ? routePath.replace(/^\/+/, "") : "";
|
|
256
|
+
if (!base && !route) {
|
|
257
|
+
return "/";
|
|
258
|
+
}
|
|
259
|
+
if (!base) {
|
|
260
|
+
return `/${route}`;
|
|
261
|
+
}
|
|
262
|
+
if (!route) {
|
|
263
|
+
return base.startsWith("/") ? base : `/${base}`;
|
|
264
|
+
}
|
|
265
|
+
return `${base.startsWith("/") ? base : `/${base}`}/${route}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function normalizePath(path: string | undefined, fallback: string): string {
|
|
269
|
+
const value = path && path.trim().length ? path.trim() : fallback;
|
|
270
|
+
return value.startsWith("/") ? value : `/${value}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function sendHttpError(res: Response, error: HttpError): void {
|
|
274
|
+
if (res.headersSent) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
if (error.headers) {
|
|
278
|
+
for (const [key, value] of Object.entries(error.headers)) {
|
|
279
|
+
res.setHeader(key, value);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const body = error.body ?? { message: error.message };
|
|
283
|
+
if (body === undefined) {
|
|
284
|
+
res.status(error.status).end();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
res.status(error.status).json(body);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
type CoerceField = { name: string; schema: SchemaNode };
|
|
291
|
+
type InputLocation = "params" | "query";
|
|
292
|
+
|
|
293
|
+
interface InputCoercionOptions {
|
|
294
|
+
mode: InputCoercionMode;
|
|
295
|
+
location: InputLocation;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
interface CoerceOutcome {
|
|
299
|
+
value: unknown;
|
|
300
|
+
ok: boolean;
|
|
301
|
+
changed: boolean;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function createInputCoercer<T extends Record<string, unknown> = Record<string, unknown>>(
|
|
305
|
+
input: InputMeta | undefined,
|
|
306
|
+
options: InputCoercionOptions
|
|
307
|
+
): ((value: T) => T) | undefined {
|
|
308
|
+
if (!input) {
|
|
309
|
+
return undefined;
|
|
310
|
+
}
|
|
311
|
+
const fields = extractFields(input.schema);
|
|
312
|
+
if (!fields.length) {
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
return (value: T) => {
|
|
316
|
+
const result = coerceRecord(value, fields, options.mode);
|
|
317
|
+
if (options.mode === "strict" && result.invalidFields.length) {
|
|
318
|
+
throw new HttpError(400, buildInvalidMessage(options.location, result.invalidFields));
|
|
319
|
+
}
|
|
320
|
+
return result.value as T;
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function coerceRecord(
|
|
325
|
+
value: unknown,
|
|
326
|
+
fields: CoerceField[],
|
|
327
|
+
mode: InputCoercionMode
|
|
328
|
+
): { value: unknown; invalidFields: string[] } {
|
|
329
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
330
|
+
return { value, invalidFields: [] };
|
|
331
|
+
}
|
|
332
|
+
const input = value as Record<string, unknown>;
|
|
333
|
+
let changed = false;
|
|
334
|
+
const output: Record<string, unknown> = { ...input };
|
|
335
|
+
const invalidFields: string[] = [];
|
|
336
|
+
for (const field of fields) {
|
|
337
|
+
if (!(field.name in input)) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const original = input[field.name];
|
|
341
|
+
const result = coerceValue(original, field.schema, mode);
|
|
342
|
+
if (!result.ok && mode === "strict") {
|
|
343
|
+
invalidFields.push(field.name);
|
|
344
|
+
}
|
|
345
|
+
if (result.changed) {
|
|
346
|
+
output[field.name] = result.value;
|
|
347
|
+
changed = true;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return { value: changed ? output : value, invalidFields };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function coerceValue(
|
|
354
|
+
value: unknown,
|
|
355
|
+
schema: SchemaNode,
|
|
356
|
+
mode: InputCoercionMode
|
|
357
|
+
): CoerceOutcome {
|
|
358
|
+
switch (schema.kind) {
|
|
359
|
+
case "integer":
|
|
360
|
+
return coerceNumber(value, schema, true);
|
|
361
|
+
case "number":
|
|
362
|
+
return coerceNumber(value, schema, false);
|
|
363
|
+
case "boolean": {
|
|
364
|
+
return coerceBoolean(value);
|
|
365
|
+
}
|
|
366
|
+
case "string": {
|
|
367
|
+
return coerceString(value);
|
|
368
|
+
}
|
|
369
|
+
case "array":
|
|
370
|
+
return coerceArrayValue(value, schema, mode);
|
|
371
|
+
case "object":
|
|
372
|
+
return coerceObjectValue(value, schema, mode);
|
|
373
|
+
case "record":
|
|
374
|
+
return coerceRecordValue(value, schema, mode);
|
|
375
|
+
case "ref":
|
|
376
|
+
return coerceRefValue(value, schema, mode);
|
|
377
|
+
case "union":
|
|
378
|
+
return coerceUnionValue(value, schema, mode);
|
|
379
|
+
default:
|
|
380
|
+
return { value, ok: true, changed: false };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function coerceNumber(value: unknown, schema: NumberSchema, integer: boolean): CoerceOutcome {
|
|
385
|
+
if (!isPresent(value)) {
|
|
386
|
+
return { value, ok: true, changed: false };
|
|
387
|
+
}
|
|
388
|
+
const parsed = integer
|
|
389
|
+
? coerce.integer(value, { min: schema.minimum, max: schema.maximum })
|
|
390
|
+
: coerce.number(value, { min: schema.minimum, max: schema.maximum });
|
|
391
|
+
if (parsed === undefined) {
|
|
392
|
+
return { value, ok: false, changed: false };
|
|
393
|
+
}
|
|
394
|
+
if (schema.exclusiveMinimum !== undefined && parsed <= schema.exclusiveMinimum) {
|
|
395
|
+
return { value, ok: false, changed: false };
|
|
396
|
+
}
|
|
397
|
+
if (schema.exclusiveMaximum !== undefined && parsed >= schema.exclusiveMaximum) {
|
|
398
|
+
return { value, ok: false, changed: false };
|
|
399
|
+
}
|
|
400
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function coerceBoolean(value: unknown): CoerceOutcome {
|
|
404
|
+
if (!isPresent(value)) {
|
|
405
|
+
return { value, ok: true, changed: false };
|
|
406
|
+
}
|
|
407
|
+
const parsed = coerce.boolean(value);
|
|
408
|
+
if (parsed === undefined) {
|
|
409
|
+
return { value, ok: false, changed: false };
|
|
410
|
+
}
|
|
411
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function coerceString(value: unknown): CoerceOutcome {
|
|
415
|
+
const parsed = coerce.string(value);
|
|
416
|
+
if (parsed === undefined) {
|
|
417
|
+
return { value, ok: true, changed: false };
|
|
418
|
+
}
|
|
419
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function coerceArrayValue(
|
|
423
|
+
value: unknown,
|
|
424
|
+
schema: ArraySchema,
|
|
425
|
+
mode: InputCoercionMode
|
|
426
|
+
): CoerceOutcome {
|
|
427
|
+
if (value === undefined || value === null) {
|
|
428
|
+
return { value, ok: true, changed: false };
|
|
429
|
+
}
|
|
430
|
+
const input = Array.isArray(value) ? value : [value];
|
|
431
|
+
let changed = !Array.isArray(value);
|
|
432
|
+
let ok = true;
|
|
433
|
+
const output = input.map((entry) => {
|
|
434
|
+
const result = coerceValue(entry, schema.items, mode);
|
|
435
|
+
if (!result.ok) {
|
|
436
|
+
ok = false;
|
|
437
|
+
}
|
|
438
|
+
if (result.changed) {
|
|
439
|
+
changed = true;
|
|
440
|
+
}
|
|
441
|
+
return result.value;
|
|
442
|
+
});
|
|
443
|
+
return { value: changed ? output : value, ok, changed };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function coerceObjectValue(
|
|
447
|
+
value: unknown,
|
|
448
|
+
schema: ObjectSchema,
|
|
449
|
+
mode: InputCoercionMode
|
|
450
|
+
): CoerceOutcome {
|
|
451
|
+
if (value === undefined || value === null) {
|
|
452
|
+
return { value, ok: true, changed: false };
|
|
453
|
+
}
|
|
454
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
455
|
+
return { value, ok: mode === "safe", changed: false };
|
|
456
|
+
}
|
|
457
|
+
const properties = schema.properties ?? {};
|
|
458
|
+
const fields = Object.entries(properties).map(([name, fieldSchema]) => ({
|
|
459
|
+
name,
|
|
460
|
+
schema: fieldSchema
|
|
461
|
+
}));
|
|
462
|
+
if (!fields.length) {
|
|
463
|
+
return { value, ok: true, changed: false };
|
|
464
|
+
}
|
|
465
|
+
const result = coerceRecord(value, fields, mode);
|
|
466
|
+
return {
|
|
467
|
+
value: result.value,
|
|
468
|
+
ok: result.invalidFields.length === 0,
|
|
469
|
+
changed: result.value !== value
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function coerceRecordValue(
|
|
474
|
+
value: unknown,
|
|
475
|
+
schema: RecordSchema,
|
|
476
|
+
mode: InputCoercionMode
|
|
477
|
+
): CoerceOutcome {
|
|
478
|
+
if (value === undefined || value === null) {
|
|
479
|
+
return { value, ok: true, changed: false };
|
|
480
|
+
}
|
|
481
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
482
|
+
return { value, ok: mode === "safe", changed: false };
|
|
483
|
+
}
|
|
484
|
+
const input = value as Record<string, unknown>;
|
|
485
|
+
let changed = false;
|
|
486
|
+
let ok = true;
|
|
487
|
+
const output: Record<string, unknown> = { ...input };
|
|
488
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
489
|
+
const result = coerceValue(entry, schema.values, mode);
|
|
490
|
+
if (!result.ok) {
|
|
491
|
+
ok = false;
|
|
492
|
+
}
|
|
493
|
+
if (result.changed) {
|
|
494
|
+
output[key] = result.value;
|
|
495
|
+
changed = true;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return { value: changed ? output : value, ok, changed };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function coerceRefValue(
|
|
502
|
+
value: unknown,
|
|
503
|
+
schema: RefSchema,
|
|
504
|
+
mode: InputCoercionMode
|
|
505
|
+
): CoerceOutcome {
|
|
506
|
+
if (value === undefined || value === null) {
|
|
507
|
+
return { value, ok: true, changed: false };
|
|
508
|
+
}
|
|
509
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
510
|
+
return { value, ok: mode === "safe", changed: false };
|
|
511
|
+
}
|
|
512
|
+
const meta = getDtoMetaSafe(schema.dto);
|
|
513
|
+
const fields = Object.entries(meta.fields).map(([name, field]) => ({
|
|
514
|
+
name,
|
|
515
|
+
schema: field.schema
|
|
516
|
+
}));
|
|
517
|
+
if (!fields.length) {
|
|
518
|
+
return { value, ok: true, changed: false };
|
|
519
|
+
}
|
|
520
|
+
const result = coerceRecord(value, fields, mode);
|
|
521
|
+
return {
|
|
522
|
+
value: result.value,
|
|
523
|
+
ok: result.invalidFields.length === 0,
|
|
524
|
+
changed: result.value !== value
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function coerceUnionValue(
|
|
529
|
+
value: unknown,
|
|
530
|
+
schema: UnionSchema,
|
|
531
|
+
mode: InputCoercionMode
|
|
532
|
+
): CoerceOutcome {
|
|
533
|
+
let fallback: CoerceOutcome | undefined;
|
|
534
|
+
for (const option of schema.anyOf) {
|
|
535
|
+
const result = coerceValue(value, option, mode);
|
|
536
|
+
if (!result.ok) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (result.changed) {
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
542
|
+
fallback ??= result;
|
|
543
|
+
}
|
|
544
|
+
if (fallback) {
|
|
545
|
+
return fallback;
|
|
546
|
+
}
|
|
547
|
+
return { value, ok: mode === "safe", changed: false };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function extractFields(schema: SchemaSource): CoerceField[] {
|
|
551
|
+
if (isSchemaNode(schema)) {
|
|
552
|
+
if (schema.kind === "object" && schema.properties) {
|
|
553
|
+
return Object.entries(schema.properties).map(([name, fieldSchema]) => ({
|
|
554
|
+
name,
|
|
555
|
+
schema: fieldSchema
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
return [];
|
|
559
|
+
}
|
|
560
|
+
const meta = getDtoMetaSafe(schema);
|
|
561
|
+
return Object.entries(meta.fields).map(([name, field]) => ({
|
|
562
|
+
name,
|
|
563
|
+
schema: field.schema
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function getDtoMetaSafe(dto: DtoConstructor): {
|
|
568
|
+
fields: Record<string, { schema: SchemaNode }>;
|
|
569
|
+
} {
|
|
570
|
+
const meta = getDtoMeta(dto);
|
|
571
|
+
if (!meta) {
|
|
572
|
+
throw new Error(`DTO "${dto.name}" is missing @Dto decorator.`);
|
|
573
|
+
}
|
|
574
|
+
return meta;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function isSchemaNode(value: unknown): value is SchemaNode {
|
|
578
|
+
return !!value && typeof value === "object" && "kind" in (value as SchemaNode);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function isPresent(value: unknown): boolean {
|
|
582
|
+
return coerce.string(value) !== undefined;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function buildInvalidMessage(location: InputLocation, fields: string[]): string {
|
|
586
|
+
const label = location === "params" ? "path parameter" : "query parameter";
|
|
587
|
+
const suffix = fields.length > 1 ? "s" : "";
|
|
588
|
+
return `Invalid ${label}${suffix}: ${fields.join(", ")}.`;
|
|
589
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { getColumnMap, type ColumnDef } from "metal-orm";
|
|
2
|
+
import { t, type SchemaNode } from "../../core/schema";
|
|
3
|
+
|
|
4
|
+
export interface CreateMetalDtoOverridesOptions {
|
|
5
|
+
overrides?: Record<string, SchemaNode>;
|
|
6
|
+
exclude?: string[];
|
|
7
|
+
entityName?: string;
|
|
8
|
+
timestampDescription?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createMetalDtoOverrides(
|
|
12
|
+
target: any,
|
|
13
|
+
options: CreateMetalDtoOverridesOptions = {}
|
|
14
|
+
): Record<string, SchemaNode> {
|
|
15
|
+
const columns = getColumnMap(target);
|
|
16
|
+
const {
|
|
17
|
+
overrides = {},
|
|
18
|
+
exclude = [],
|
|
19
|
+
entityName = target.name,
|
|
20
|
+
timestampDescription = "Creation timestamp."
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
const result: Record<string, SchemaNode> = {};
|
|
24
|
+
|
|
25
|
+
for (const [name, col] of Object.entries(columns)) {
|
|
26
|
+
if (exclude.includes(name)) continue;
|
|
27
|
+
|
|
28
|
+
if (overrides[name]) {
|
|
29
|
+
result[name] = overrides[name];
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const convention = inferFromMetadata(name, col, entityName, timestampDescription);
|
|
34
|
+
if (convention) {
|
|
35
|
+
result[name] = convention;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function inferFromMetadata(
|
|
43
|
+
name: string,
|
|
44
|
+
col: ColumnDef,
|
|
45
|
+
entityName: string,
|
|
46
|
+
timestampDescription: string
|
|
47
|
+
): SchemaNode | null {
|
|
48
|
+
const normalizedType = col.type.toUpperCase();
|
|
49
|
+
|
|
50
|
+
if (col.primary || col.autoIncrement) {
|
|
51
|
+
return t.integer({
|
|
52
|
+
minimum: 1,
|
|
53
|
+
description: `${entityName} id.`
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (col.references) {
|
|
58
|
+
const targetEntity = extractEntityName(col.references.table);
|
|
59
|
+
return t.integer({
|
|
60
|
+
minimum: 1,
|
|
61
|
+
description: `${targetEntity} id.`
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (normalizedType === "DATETIME" || normalizedType === "TIMESTAMP") {
|
|
66
|
+
return t.dateTime({ description: timestampDescription });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (col.notNull && isTextColumn(col)) {
|
|
70
|
+
return t.string({ minLength: 1 });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!col.notNull && isTextColumn(col)) {
|
|
74
|
+
return t.nullable(t.string());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (col.notNull && isIntegerColumn(col)) {
|
|
78
|
+
return t.integer({ minimum: 1 });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!col.notNull && isIntegerColumn(col)) {
|
|
82
|
+
return t.nullable(t.integer());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function extractEntityName(tableName: string): string {
|
|
89
|
+
const pascalCase = tableName
|
|
90
|
+
.split("_")
|
|
91
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
92
|
+
.join("");
|
|
93
|
+
|
|
94
|
+
return singularize(pascalCase);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function singularize(word: string): string {
|
|
98
|
+
if (word.endsWith("ies")) {
|
|
99
|
+
return word.slice(0, -3) + "y";
|
|
100
|
+
}
|
|
101
|
+
if (word.endsWith("s") && !word.endsWith("ss")) {
|
|
102
|
+
return word.slice(0, -1);
|
|
103
|
+
}
|
|
104
|
+
return word;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isTextColumn(col: ColumnDef): boolean {
|
|
108
|
+
const type = col.type.toUpperCase();
|
|
109
|
+
return type === "TEXT" || type === "VARCHAR" || type === "CHAR";
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isIntegerColumn(col: ColumnDef): boolean {
|
|
113
|
+
const type = col.type.toUpperCase();
|
|
114
|
+
return type === "INT" || type === "INTEGER" || type === "BIGINT";
|
|
115
|
+
}
|