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,231 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Delete,
|
|
5
|
+
Get,
|
|
6
|
+
HttpError,
|
|
7
|
+
Params,
|
|
8
|
+
Patch,
|
|
9
|
+
Post,
|
|
10
|
+
Put,
|
|
11
|
+
Query,
|
|
12
|
+
Returns,
|
|
13
|
+
parsePagination,
|
|
14
|
+
t,
|
|
15
|
+
type RequestContext
|
|
16
|
+
} from "../../src";
|
|
17
|
+
import { applyFilter, toPagedResponse } from "metal-orm";
|
|
18
|
+
import type { SimpleWhereInput } from "metal-orm";
|
|
19
|
+
import { entityRef, selectFromEntity } from "metal-orm";
|
|
20
|
+
import { createSession } from "./db";
|
|
21
|
+
import {
|
|
22
|
+
CreateUserDto,
|
|
23
|
+
ReplaceUserDto,
|
|
24
|
+
UpdateUserDto,
|
|
25
|
+
UserDto,
|
|
26
|
+
UserErrors,
|
|
27
|
+
UserParamsDto,
|
|
28
|
+
UserWithPostsPagedResponseDto,
|
|
29
|
+
UserQueryDto,
|
|
30
|
+
UserQueryDtoClass
|
|
31
|
+
} from "./user.dtos";
|
|
32
|
+
import { CreateUserPostDto, CreateUserPostDtoClass, PostDto } from "./post.dtos";
|
|
33
|
+
import { User } from "./user.entity";
|
|
34
|
+
|
|
35
|
+
type IntegerOptions = {
|
|
36
|
+
min?: number;
|
|
37
|
+
max?: number;
|
|
38
|
+
clamp?: boolean;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
function parseInteger(value: unknown, options: IntegerOptions = {}): number | undefined {
|
|
42
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const result = value;
|
|
46
|
+
if (options.min !== undefined && result < options.min) {
|
|
47
|
+
return options.clamp ? options.min : undefined;
|
|
48
|
+
}
|
|
49
|
+
if (options.max !== undefined && result > options.max) {
|
|
50
|
+
return options.clamp ? options.max : undefined;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requireUserId(value: unknown): number {
|
|
56
|
+
const id = parseInteger(value, { min: 1 });
|
|
57
|
+
if (id === undefined) {
|
|
58
|
+
throw new HttpError(400, "Invalid user id.");
|
|
59
|
+
}
|
|
60
|
+
return id;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const userRef = entityRef(User);
|
|
64
|
+
|
|
65
|
+
type UserFilter = SimpleWhereInput<typeof User, "name" | "email">;
|
|
66
|
+
type OrmSession = ReturnType<typeof createSession>;
|
|
67
|
+
|
|
68
|
+
async function withSession<T>(handler: (session: OrmSession) => Promise<T>) {
|
|
69
|
+
const session = createSession();
|
|
70
|
+
try {
|
|
71
|
+
return await handler(session);
|
|
72
|
+
} finally {
|
|
73
|
+
await session.dispose();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function getUserOrThrow(session: OrmSession, id: number): Promise<User> {
|
|
78
|
+
const user = await session.find(User, id);
|
|
79
|
+
if (!user) {
|
|
80
|
+
throw new HttpError(404, "User not found.");
|
|
81
|
+
}
|
|
82
|
+
return user;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildUserFilter(query?: UserQueryDto): UserFilter | undefined {
|
|
86
|
+
if (!query) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const filter: UserFilter = {};
|
|
90
|
+
if (query.nameContains) {
|
|
91
|
+
filter.name = { contains: query.nameContains };
|
|
92
|
+
}
|
|
93
|
+
if (query.emailContains) {
|
|
94
|
+
filter.email = { contains: query.emailContains };
|
|
95
|
+
}
|
|
96
|
+
return Object.keys(filter).length ? filter : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@Controller("/users")
|
|
100
|
+
export class UserController {
|
|
101
|
+
@Get("/")
|
|
102
|
+
@Query(UserQueryDtoClass)
|
|
103
|
+
@Returns(UserWithPostsPagedResponseDto)
|
|
104
|
+
async list(ctx: RequestContext<unknown, UserQueryDto>) {
|
|
105
|
+
const paginationQuery = (ctx.query ?? {}) as Record<string, unknown>;
|
|
106
|
+
const { page, pageSize } = parsePagination(paginationQuery);
|
|
107
|
+
return withSession(async (session) => {
|
|
108
|
+
const filters = buildUserFilter(ctx.query);
|
|
109
|
+
const query = applyFilter(
|
|
110
|
+
selectFromEntity(User)
|
|
111
|
+
.orderBy(userRef.id, "ASC")
|
|
112
|
+
.include("posts", {
|
|
113
|
+
columns: ["id", "title", "body", "userId", "createdAt"]
|
|
114
|
+
}),
|
|
115
|
+
User,
|
|
116
|
+
filters
|
|
117
|
+
);
|
|
118
|
+
const paged = await query.executePaged(session, { page, pageSize });
|
|
119
|
+
return toPagedResponse(paged);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@Get("/:id")
|
|
124
|
+
@Params(UserParamsDto)
|
|
125
|
+
@Returns(UserDto)
|
|
126
|
+
@UserErrors
|
|
127
|
+
async getOne(ctx: RequestContext<unknown, undefined, UserParamsDto>) {
|
|
128
|
+
const id = requireUserId(ctx.params.id);
|
|
129
|
+
return withSession(async (session) => {
|
|
130
|
+
const user = await getUserOrThrow(session, id);
|
|
131
|
+
return user as UserDto;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Post("/")
|
|
136
|
+
@Body(CreateUserDto)
|
|
137
|
+
@Returns({ status: 201, schema: UserDto })
|
|
138
|
+
async create(ctx: RequestContext<CreateUserDto>) {
|
|
139
|
+
return withSession(async (session) => {
|
|
140
|
+
const user = new User();
|
|
141
|
+
user.name = ctx.body.name;
|
|
142
|
+
user.email = ctx.body.email ?? null;
|
|
143
|
+
user.createdAt = new Date().toISOString();
|
|
144
|
+
await session.persist(user);
|
|
145
|
+
await session.commit();
|
|
146
|
+
return user as UserDto;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@Put("/:id")
|
|
151
|
+
@Params(UserParamsDto)
|
|
152
|
+
@Body(ReplaceUserDto)
|
|
153
|
+
@Returns(UserDto)
|
|
154
|
+
@UserErrors
|
|
155
|
+
async replace(ctx: RequestContext<ReplaceUserDto, undefined, UserParamsDto>) {
|
|
156
|
+
const id = requireUserId(ctx.params.id);
|
|
157
|
+
return withSession(async (session) => {
|
|
158
|
+
const entity = await getUserOrThrow(session, id);
|
|
159
|
+
entity.name = ctx.body.name;
|
|
160
|
+
entity.email = ctx.body.email ?? null;
|
|
161
|
+
await session.commit();
|
|
162
|
+
return entity as UserDto;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@Patch("/:id")
|
|
167
|
+
@Params(UserParamsDto)
|
|
168
|
+
@Body(UpdateUserDto)
|
|
169
|
+
@Returns(UserDto)
|
|
170
|
+
@UserErrors
|
|
171
|
+
async update(ctx: RequestContext<UpdateUserDto, undefined, UserParamsDto>) {
|
|
172
|
+
const id = requireUserId(ctx.params.id);
|
|
173
|
+
return withSession(async (session) => {
|
|
174
|
+
const entity = await getUserOrThrow(session, id);
|
|
175
|
+
if (ctx.body.name !== undefined) {
|
|
176
|
+
entity.name = ctx.body.name;
|
|
177
|
+
}
|
|
178
|
+
if (ctx.body.email !== undefined) {
|
|
179
|
+
entity.email = ctx.body.email ?? null;
|
|
180
|
+
}
|
|
181
|
+
await session.commit();
|
|
182
|
+
return entity as UserDto;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Get("/:id/posts")
|
|
187
|
+
@Params(UserParamsDto)
|
|
188
|
+
@Returns(t.array(t.ref(PostDto)))
|
|
189
|
+
@UserErrors
|
|
190
|
+
async listPosts(ctx: RequestContext<unknown, undefined, UserParamsDto>) {
|
|
191
|
+
const id = requireUserId(ctx.params.id);
|
|
192
|
+
return withSession(async (session) => {
|
|
193
|
+
const user = await getUserOrThrow(session, id);
|
|
194
|
+
return (await user.posts.load()) as PostDto[];
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@Post("/:id/posts")
|
|
199
|
+
@Params(UserParamsDto)
|
|
200
|
+
@Body(CreateUserPostDtoClass)
|
|
201
|
+
@Returns({ status: 201, schema: PostDto })
|
|
202
|
+
@UserErrors
|
|
203
|
+
async createPost(
|
|
204
|
+
ctx: RequestContext<CreateUserPostDto, undefined, UserParamsDto>
|
|
205
|
+
) {
|
|
206
|
+
const id = requireUserId(ctx.params.id);
|
|
207
|
+
return withSession(async (session) => {
|
|
208
|
+
const user = await getUserOrThrow(session, id);
|
|
209
|
+
const post = user.posts.add({
|
|
210
|
+
title: ctx.body.title,
|
|
211
|
+
body: ctx.body.body ?? null,
|
|
212
|
+
createdAt: new Date().toISOString()
|
|
213
|
+
});
|
|
214
|
+
await session.commit();
|
|
215
|
+
return post as PostDto;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@Delete("/:id")
|
|
220
|
+
@Params(UserParamsDto)
|
|
221
|
+
@Returns({ status: 204 })
|
|
222
|
+
@UserErrors
|
|
223
|
+
async remove(ctx: RequestContext<unknown, undefined, UserParamsDto>) {
|
|
224
|
+
const id = requireUserId(ctx.params.id);
|
|
225
|
+
return withSession(async (session) => {
|
|
226
|
+
const user = await getUserOrThrow(session, id);
|
|
227
|
+
await session.remove(user);
|
|
228
|
+
await session.commit();
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Dto,
|
|
3
|
+
Field,
|
|
4
|
+
MergeDto,
|
|
5
|
+
Errors,
|
|
6
|
+
createMetalCrudDtoClasses,
|
|
7
|
+
createMetalDtoOverrides,
|
|
8
|
+
createPagedResponseDtoClass,
|
|
9
|
+
createPagedFilterQueryDtoClass,
|
|
10
|
+
SimpleErrorDto,
|
|
11
|
+
t
|
|
12
|
+
} from "../../src";
|
|
13
|
+
import { User } from "./user.entity";
|
|
14
|
+
import { PostDto } from "./post.dtos";
|
|
15
|
+
|
|
16
|
+
const userOverrides = createMetalDtoOverrides(User, {
|
|
17
|
+
overrides: {
|
|
18
|
+
email: t.nullable(t.string({ format: "email" }))
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const userCrud = createMetalCrudDtoClasses(User, {
|
|
23
|
+
overrides: userOverrides,
|
|
24
|
+
response: { description: "User returned by API." },
|
|
25
|
+
mutationExclude: ["id", "createdAt"]
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const {
|
|
29
|
+
response: UserDto,
|
|
30
|
+
create: CreateUserDto,
|
|
31
|
+
replace: ReplaceUserDto,
|
|
32
|
+
update: UpdateUserDto,
|
|
33
|
+
params: UserParamsDto
|
|
34
|
+
} = userCrud;
|
|
35
|
+
|
|
36
|
+
export type UserDto = Omit<User, "posts">;
|
|
37
|
+
type UserMutationDto = Omit<UserDto, "id" | "createdAt">;
|
|
38
|
+
export type CreateUserDto = UserMutationDto;
|
|
39
|
+
export type ReplaceUserDto = UserMutationDto;
|
|
40
|
+
export type UpdateUserDto = Partial<UserMutationDto>;
|
|
41
|
+
export type UserParamsDto = InstanceType<typeof UserParamsDto>;
|
|
42
|
+
|
|
43
|
+
export interface UserWithPostsDto extends UserDto {
|
|
44
|
+
posts: PostDto[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@Dto()
|
|
48
|
+
class UserPostsDto {
|
|
49
|
+
@Field(t.array(t.ref(PostDto)))
|
|
50
|
+
posts!: PostDto[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@MergeDto([UserDto, UserPostsDto], {
|
|
54
|
+
description: "User returned by API with posts."
|
|
55
|
+
})
|
|
56
|
+
export class UserWithPostsDto {}
|
|
57
|
+
|
|
58
|
+
export const UserQueryDtoClass = createPagedFilterQueryDtoClass({
|
|
59
|
+
name: "UserQueryDto",
|
|
60
|
+
filters: {
|
|
61
|
+
nameContains: { schema: t.string({ minLength: 1 }), operator: "contains" },
|
|
62
|
+
emailContains: { schema: t.string({ minLength: 1 }), operator: "contains" }
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export interface UserQueryDto {
|
|
67
|
+
page?: number;
|
|
68
|
+
pageSize?: number;
|
|
69
|
+
nameContains?: string;
|
|
70
|
+
emailContains?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const UserPagedResponseDto = createPagedResponseDtoClass({
|
|
74
|
+
name: "UserPagedResponseDto",
|
|
75
|
+
itemDto: UserDto,
|
|
76
|
+
description: "Paged user list response."
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const UserWithPostsPagedResponseDto = createPagedResponseDtoClass({
|
|
80
|
+
name: "UserWithPostsPagedResponseDto",
|
|
81
|
+
itemDto: UserWithPostsDto,
|
|
82
|
+
description: "Paged user list response with posts."
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const UserErrors = Errors(SimpleErrorDto, [
|
|
86
|
+
{ status: 400, description: "Invalid user id." },
|
|
87
|
+
{ status: 404, description: "User not found." }
|
|
88
|
+
]);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Column, Entity, HasMany, PrimaryKey, col } from "metal-orm";
|
|
2
|
+
import type { HasManyCollection } from "metal-orm";
|
|
3
|
+
import { Post } from "./post.entity";
|
|
4
|
+
|
|
5
|
+
@Entity({ tableName: "users" })
|
|
6
|
+
export class User {
|
|
7
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
8
|
+
id!: number;
|
|
9
|
+
|
|
10
|
+
@Column(col.notNull(col.text()))
|
|
11
|
+
name!: string;
|
|
12
|
+
|
|
13
|
+
@Column(col.text())
|
|
14
|
+
email?: string | null;
|
|
15
|
+
|
|
16
|
+
@Column(col.notNull(col.text()))
|
|
17
|
+
createdAt!: string;
|
|
18
|
+
|
|
19
|
+
@HasMany({ target: () => Post, foreignKey: "userId" })
|
|
20
|
+
posts!: HasManyCollection<Post>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Delete,
|
|
5
|
+
Get,
|
|
6
|
+
HttpError,
|
|
7
|
+
Params,
|
|
8
|
+
Patch,
|
|
9
|
+
Post,
|
|
10
|
+
Put,
|
|
11
|
+
Query,
|
|
12
|
+
Returns,
|
|
13
|
+
parsePagination,
|
|
14
|
+
type RequestContext
|
|
15
|
+
} from "../../src";
|
|
16
|
+
import { applyFilter, toPagedResponse } from "metal-orm";
|
|
17
|
+
import type { SimpleWhereInput } from "metal-orm";
|
|
18
|
+
import { entityRef, selectFromEntity } from "metal-orm";
|
|
19
|
+
import { createSession } from "./db";
|
|
20
|
+
import {
|
|
21
|
+
AlbumDto,
|
|
22
|
+
AlbumParamsDto,
|
|
23
|
+
AlbumQueryDto,
|
|
24
|
+
AlbumQueryDtoClass,
|
|
25
|
+
AlbumPagedResponseDto,
|
|
26
|
+
AlbumErrors,
|
|
27
|
+
CreateAlbumDto,
|
|
28
|
+
ReplaceAlbumDto,
|
|
29
|
+
UpdateAlbumDto,
|
|
30
|
+
CreateAlbumTrackDto,
|
|
31
|
+
CreateAlbumTrackDtoClass
|
|
32
|
+
} from "./album.dtos";
|
|
33
|
+
import {
|
|
34
|
+
TrackDto,
|
|
35
|
+
TrackQueryDto,
|
|
36
|
+
TrackQueryDtoClass,
|
|
37
|
+
TrackPagedResponseDto
|
|
38
|
+
} from "./track.dtos";
|
|
39
|
+
import { Album as AlbumEntity } from "./album.entity";
|
|
40
|
+
import { Artist } from "./artist.entity";
|
|
41
|
+
import { Track as TrackEntity } from "./track.entity";
|
|
42
|
+
|
|
43
|
+
type IntegerOptions = {
|
|
44
|
+
min?: number;
|
|
45
|
+
max?: number;
|
|
46
|
+
clamp?: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function parseInteger(value: unknown, options: IntegerOptions = {}): number | undefined {
|
|
50
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const result = value;
|
|
54
|
+
if (options.min !== undefined && result < options.min) {
|
|
55
|
+
return options.clamp ? options.min : undefined;
|
|
56
|
+
}
|
|
57
|
+
if (options.max !== undefined && result > options.max) {
|
|
58
|
+
return options.clamp ? options.max : undefined;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function requireAlbumId(value: unknown): number {
|
|
64
|
+
const id = parseInteger(value, { min: 1 });
|
|
65
|
+
if (id === undefined) {
|
|
66
|
+
const message = "Invalid album id.";
|
|
67
|
+
throw new HttpError(
|
|
68
|
+
400,
|
|
69
|
+
message,
|
|
70
|
+
buildErrorBody(message, "INVALID_ALBUM_ID", [{ field: "id", message }])
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return id;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const albumRef = entityRef(AlbumEntity);
|
|
77
|
+
const trackRef = entityRef(TrackEntity);
|
|
78
|
+
|
|
79
|
+
type AlbumFilter = SimpleWhereInput<typeof AlbumEntity, "title" | "releaseYear" | "artistId">;
|
|
80
|
+
type AlbumTrackFilter = SimpleWhereInput<typeof TrackEntity, "albumId">;
|
|
81
|
+
type OrmSession = ReturnType<typeof createSession>;
|
|
82
|
+
|
|
83
|
+
type ErrorDetail = { field: string; message: string };
|
|
84
|
+
|
|
85
|
+
function buildErrorBody(message: string, code: string, errors?: ErrorDetail[]) {
|
|
86
|
+
return { message, code, errors };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function withSession<T>(handler: (session: OrmSession) => Promise<T>) {
|
|
90
|
+
const session = createSession();
|
|
91
|
+
try {
|
|
92
|
+
return await handler(session);
|
|
93
|
+
} finally {
|
|
94
|
+
await session.dispose();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function getAlbumOrThrow(session: OrmSession, id: number): Promise<AlbumEntity> {
|
|
99
|
+
const album = await session.find(AlbumEntity, id);
|
|
100
|
+
if (!album) {
|
|
101
|
+
const message = "Album not found.";
|
|
102
|
+
throw new HttpError(404, message, buildErrorBody(message, "ALBUM_NOT_FOUND"));
|
|
103
|
+
}
|
|
104
|
+
return album;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function getArtistOrThrow(session: OrmSession, id: number): Promise<Artist> {
|
|
108
|
+
const artist = await session.find(Artist, id);
|
|
109
|
+
if (!artist) {
|
|
110
|
+
const message = "Artist not found.";
|
|
111
|
+
throw new HttpError(404, message, buildErrorBody(message, "ARTIST_NOT_FOUND"));
|
|
112
|
+
}
|
|
113
|
+
return artist;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildAlbumFilter(query?: AlbumQueryDto): AlbumFilter | undefined {
|
|
117
|
+
if (!query) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const filter: AlbumFilter = {};
|
|
121
|
+
if (query.titleContains) {
|
|
122
|
+
filter.title = { contains: query.titleContains };
|
|
123
|
+
}
|
|
124
|
+
if (query.releaseYear !== undefined) {
|
|
125
|
+
(filter as any).releaseYear = { equals: query.releaseYear };
|
|
126
|
+
}
|
|
127
|
+
if (query.artistId !== undefined) {
|
|
128
|
+
filter.artistId = { equals: query.artistId };
|
|
129
|
+
}
|
|
130
|
+
return Object.keys(filter).length ? filter : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@Controller("/albums")
|
|
134
|
+
export class AlbumController {
|
|
135
|
+
@Get("/")
|
|
136
|
+
@Query(AlbumQueryDtoClass)
|
|
137
|
+
@Returns(AlbumPagedResponseDto)
|
|
138
|
+
async list(ctx: RequestContext<unknown, AlbumQueryDto>) {
|
|
139
|
+
const paginationQuery = (ctx.query ?? {}) as Record<string, unknown>;
|
|
140
|
+
const { page, pageSize } = parsePagination(paginationQuery);
|
|
141
|
+
return withSession(async (session) => {
|
|
142
|
+
const filters = buildAlbumFilter(ctx.query);
|
|
143
|
+
const query = applyFilter(
|
|
144
|
+
selectFromEntity(AlbumEntity).orderBy(albumRef.id, "ASC"),
|
|
145
|
+
AlbumEntity,
|
|
146
|
+
filters
|
|
147
|
+
);
|
|
148
|
+
const paged = await query.executePaged(session, { page, pageSize });
|
|
149
|
+
return toPagedResponse(paged);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@Get("/:id")
|
|
154
|
+
@Params(AlbumParamsDto)
|
|
155
|
+
@Returns(AlbumDto)
|
|
156
|
+
@AlbumErrors
|
|
157
|
+
async getOne(ctx: RequestContext<unknown, undefined, AlbumParamsDto>) {
|
|
158
|
+
const id = requireAlbumId(ctx.params.id);
|
|
159
|
+
return withSession(async (session) => {
|
|
160
|
+
const album = await getAlbumOrThrow(session, id);
|
|
161
|
+
return album as AlbumDto;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@Post("/")
|
|
166
|
+
@Body(CreateAlbumDto)
|
|
167
|
+
@Returns({ status: 201, schema: AlbumDto })
|
|
168
|
+
async create(ctx: RequestContext<CreateAlbumDto>) {
|
|
169
|
+
return withSession(async (session) => {
|
|
170
|
+
await getArtistOrThrow(session, ctx.body.artistId);
|
|
171
|
+
const album = new AlbumEntity();
|
|
172
|
+
album.title = ctx.body.title;
|
|
173
|
+
album.releaseYear = ctx.body.releaseYear ?? null;
|
|
174
|
+
album.artistId = ctx.body.artistId;
|
|
175
|
+
album.createdAt = new Date().toISOString();
|
|
176
|
+
await session.persist(album);
|
|
177
|
+
await session.commit();
|
|
178
|
+
return album as AlbumDto;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@Put("/:id")
|
|
183
|
+
@Params(AlbumParamsDto)
|
|
184
|
+
@Body(ReplaceAlbumDto)
|
|
185
|
+
@Returns(AlbumDto)
|
|
186
|
+
@AlbumErrors
|
|
187
|
+
async replace(ctx: RequestContext<ReplaceAlbumDto, undefined, AlbumParamsDto>) {
|
|
188
|
+
const id = requireAlbumId(ctx.params.id);
|
|
189
|
+
return withSession(async (session) => {
|
|
190
|
+
const entity = await getAlbumOrThrow(session, id);
|
|
191
|
+
entity.title = ctx.body.title;
|
|
192
|
+
entity.releaseYear = ctx.body.releaseYear ?? null;
|
|
193
|
+
await session.commit();
|
|
194
|
+
return entity as AlbumDto;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@Patch("/:id")
|
|
199
|
+
@Params(AlbumParamsDto)
|
|
200
|
+
@Body(UpdateAlbumDto)
|
|
201
|
+
@Returns(AlbumDto)
|
|
202
|
+
@AlbumErrors
|
|
203
|
+
async update(ctx: RequestContext<UpdateAlbumDto, undefined, AlbumParamsDto>) {
|
|
204
|
+
const id = requireAlbumId(ctx.params.id);
|
|
205
|
+
return withSession(async (session) => {
|
|
206
|
+
const entity = await getAlbumOrThrow(session, id);
|
|
207
|
+
if (ctx.body.title !== undefined) {
|
|
208
|
+
entity.title = ctx.body.title;
|
|
209
|
+
}
|
|
210
|
+
if (ctx.body.releaseYear !== undefined) {
|
|
211
|
+
entity.releaseYear = ctx.body.releaseYear ?? null;
|
|
212
|
+
}
|
|
213
|
+
await session.commit();
|
|
214
|
+
return entity as AlbumDto;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
@Get("/:id/tracks")
|
|
219
|
+
@Params(AlbumParamsDto)
|
|
220
|
+
@Query(TrackQueryDtoClass)
|
|
221
|
+
@Returns(TrackPagedResponseDto)
|
|
222
|
+
@AlbumErrors
|
|
223
|
+
async listTracks(
|
|
224
|
+
ctx: RequestContext<unknown, TrackQueryDto, AlbumParamsDto>
|
|
225
|
+
) {
|
|
226
|
+
const id = requireAlbumId(ctx.params.id);
|
|
227
|
+
const paginationQuery = (ctx.query ?? {}) as Record<string, unknown>;
|
|
228
|
+
const { page, pageSize } = parsePagination(paginationQuery);
|
|
229
|
+
return withSession(async (session) => {
|
|
230
|
+
await getAlbumOrThrow(session, id);
|
|
231
|
+
const filters: AlbumTrackFilter = {
|
|
232
|
+
albumId: { equals: id }
|
|
233
|
+
};
|
|
234
|
+
const query = applyFilter(
|
|
235
|
+
selectFromEntity(TrackEntity).orderBy(trackRef.id, "ASC"),
|
|
236
|
+
TrackEntity,
|
|
237
|
+
filters
|
|
238
|
+
);
|
|
239
|
+
const paged = await query.executePaged(session, { page, pageSize });
|
|
240
|
+
return toPagedResponse(paged);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@Post("/:id/tracks")
|
|
245
|
+
@Params(AlbumParamsDto)
|
|
246
|
+
@Body(CreateAlbumTrackDtoClass)
|
|
247
|
+
@Returns({ status: 201, schema: TrackDto })
|
|
248
|
+
@AlbumErrors
|
|
249
|
+
async createTrack(
|
|
250
|
+
ctx: RequestContext<CreateAlbumTrackDto, undefined, AlbumParamsDto>
|
|
251
|
+
) {
|
|
252
|
+
const id = requireAlbumId(ctx.params.id);
|
|
253
|
+
return withSession(async (session) => {
|
|
254
|
+
const album = await getAlbumOrThrow(session, id);
|
|
255
|
+
const track = album.tracks.add({
|
|
256
|
+
title: ctx.body.title,
|
|
257
|
+
durationSeconds: ctx.body.durationSeconds ?? null,
|
|
258
|
+
trackNumber: ctx.body.trackNumber ?? null,
|
|
259
|
+
createdAt: new Date().toISOString()
|
|
260
|
+
});
|
|
261
|
+
await session.commit();
|
|
262
|
+
return track as TrackDto;
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
@Delete("/:id")
|
|
267
|
+
@Params(AlbumParamsDto)
|
|
268
|
+
@Returns({ status: 204 })
|
|
269
|
+
@AlbumErrors
|
|
270
|
+
async remove(ctx: RequestContext<unknown, undefined, AlbumParamsDto>) {
|
|
271
|
+
const id = requireAlbumId(ctx.params.id);
|
|
272
|
+
return withSession(async (session) => {
|
|
273
|
+
const album = await getAlbumOrThrow(session, id);
|
|
274
|
+
await session.remove(album);
|
|
275
|
+
await session.commit();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Errors,
|
|
3
|
+
createMetalCrudDtoClasses,
|
|
4
|
+
createMetalDtoOverrides,
|
|
5
|
+
createPagedResponseDtoClass,
|
|
6
|
+
createNestedCreateDtoClass,
|
|
7
|
+
createPagedFilterQueryDtoClass,
|
|
8
|
+
StandardErrorDto,
|
|
9
|
+
t
|
|
10
|
+
} from "../../src";
|
|
11
|
+
import { Album } from "./album.entity";
|
|
12
|
+
import { CreateAlbumTrackDto, CreateAlbumTrackDtoClass } from "./track.dtos";
|
|
13
|
+
|
|
14
|
+
const albumOverrides = createMetalDtoOverrides(Album, {
|
|
15
|
+
overrides: {
|
|
16
|
+
releaseYear: t.nullable(t.integer({ minimum: 1900, maximum: 9999 }))
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const albumCrud = createMetalCrudDtoClasses(Album, {
|
|
21
|
+
overrides: albumOverrides,
|
|
22
|
+
response: { description: "Album returned by API." },
|
|
23
|
+
mutationExclude: ["id", "createdAt"],
|
|
24
|
+
immutable: ["artistId"]
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const {
|
|
28
|
+
response: AlbumDto,
|
|
29
|
+
create: CreateAlbumDto,
|
|
30
|
+
replace: ReplaceAlbumDto,
|
|
31
|
+
update: UpdateAlbumDto,
|
|
32
|
+
params: AlbumParamsDto
|
|
33
|
+
} = albumCrud;
|
|
34
|
+
|
|
35
|
+
export type AlbumDto = Omit<Album, "artist" | "tracks">;
|
|
36
|
+
type AlbumMutationDto = Omit<AlbumDto, "id" | "createdAt">;
|
|
37
|
+
type AlbumUpdateDto = Omit<AlbumMutationDto, "artistId">;
|
|
38
|
+
export type CreateAlbumDto = AlbumMutationDto;
|
|
39
|
+
export type ReplaceAlbumDto = AlbumUpdateDto;
|
|
40
|
+
export type UpdateAlbumDto = Partial<AlbumUpdateDto>;
|
|
41
|
+
export type AlbumParamsDto = InstanceType<typeof AlbumParamsDto>;
|
|
42
|
+
|
|
43
|
+
export const CreateArtistAlbumDtoClass = createNestedCreateDtoClass(
|
|
44
|
+
Album,
|
|
45
|
+
albumOverrides,
|
|
46
|
+
{
|
|
47
|
+
name: "CreateArtistAlbumDto",
|
|
48
|
+
additionalExclude: ["artistId"]
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export interface CreateArtistAlbumDto {
|
|
53
|
+
title: string;
|
|
54
|
+
releaseYear?: number | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const AlbumQueryDtoClass = createPagedFilterQueryDtoClass({
|
|
58
|
+
name: "AlbumQueryDto",
|
|
59
|
+
filters: {
|
|
60
|
+
titleContains: { schema: t.string({ minLength: 1 }), operator: "contains" },
|
|
61
|
+
releaseYear: { schema: t.integer({ minimum: 1900, maximum: 9999 }), operator: "equals" },
|
|
62
|
+
artistId: { schema: t.integer({ minimum: 1 }), operator: "equals" }
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export interface AlbumQueryDto {
|
|
67
|
+
page?: number;
|
|
68
|
+
pageSize?: number;
|
|
69
|
+
titleContains?: string;
|
|
70
|
+
releaseYear?: number;
|
|
71
|
+
artistId?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const AlbumPagedResponseDto = createPagedResponseDtoClass({
|
|
75
|
+
name: "AlbumPagedResponseDto",
|
|
76
|
+
itemDto: AlbumDto,
|
|
77
|
+
description: "Paged album list response."
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const AlbumErrors = Errors(StandardErrorDto, [
|
|
81
|
+
{ status: 400, description: "Invalid album id." },
|
|
82
|
+
{ status: 404, description: "Album not found." }
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
export { CreateAlbumTrackDto, CreateAlbumTrackDtoClass };
|