adorn-api 1.0.23 → 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 -124
- package/dist/cli/progress.d.ts.map +0 -1
- package/dist/cli.cjs +0 -4622
- 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 -4603
- 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 -10
- 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 -95
- 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/serviceCallAnalyzer.d.ts +0 -102
- package/dist/compiler/schema/serviceCallAnalyzer.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,221 @@
|
|
|
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
|
+
CreateTrackDto,
|
|
22
|
+
ReplaceTrackDto,
|
|
23
|
+
UpdateTrackDto,
|
|
24
|
+
TrackDto,
|
|
25
|
+
TrackErrors,
|
|
26
|
+
TrackParamsDto,
|
|
27
|
+
TrackPagedResponseDto,
|
|
28
|
+
TrackQueryDto,
|
|
29
|
+
TrackQueryDtoClass
|
|
30
|
+
} from "./track.dtos";
|
|
31
|
+
import { Track as TrackEntity } from "./track.entity";
|
|
32
|
+
import { Album } from "./album.entity";
|
|
33
|
+
|
|
34
|
+
type IntegerOptions = {
|
|
35
|
+
min?: number;
|
|
36
|
+
max?: number;
|
|
37
|
+
clamp?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function parseInteger(value: unknown, options: IntegerOptions = {}): number | undefined {
|
|
41
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const result = value;
|
|
45
|
+
if (options.min !== undefined && result < options.min) {
|
|
46
|
+
return options.clamp ? options.min : undefined;
|
|
47
|
+
}
|
|
48
|
+
if (options.max !== undefined && result > options.max) {
|
|
49
|
+
return options.clamp ? options.max : undefined;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function requireTrackId(value: unknown): number {
|
|
55
|
+
const id = parseInteger(value, { min: 1 });
|
|
56
|
+
if (id === undefined) {
|
|
57
|
+
const message = "Invalid track id.";
|
|
58
|
+
throw new HttpError(
|
|
59
|
+
400,
|
|
60
|
+
message,
|
|
61
|
+
buildErrorBody(message, "INVALID_TRACK_ID", [{ field: "id", message }])
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const trackRef = entityRef(TrackEntity);
|
|
68
|
+
|
|
69
|
+
type TrackFilter = SimpleWhereInput<typeof TrackEntity, "title" | "albumId">;
|
|
70
|
+
type OrmSession = ReturnType<typeof createSession>;
|
|
71
|
+
|
|
72
|
+
type ErrorDetail = { field: string; message: string };
|
|
73
|
+
|
|
74
|
+
function buildErrorBody(message: string, code: string, errors?: ErrorDetail[]) {
|
|
75
|
+
return { message, code, errors };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function withSession<T>(handler: (session: OrmSession) => Promise<T>) {
|
|
79
|
+
const session = createSession();
|
|
80
|
+
try {
|
|
81
|
+
return await handler(session);
|
|
82
|
+
} finally {
|
|
83
|
+
await session.dispose();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function getTrackOrThrow(session: OrmSession, id: number): Promise<TrackEntity> {
|
|
88
|
+
const track = await session.find(TrackEntity, id);
|
|
89
|
+
if (!track) {
|
|
90
|
+
const message = "Track not found.";
|
|
91
|
+
throw new HttpError(404, message, buildErrorBody(message, "TRACK_NOT_FOUND"));
|
|
92
|
+
}
|
|
93
|
+
return track;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function getAlbumOrThrow(session: OrmSession, id: number): Promise<Album> {
|
|
97
|
+
const album = await session.find(Album, id);
|
|
98
|
+
if (!album) {
|
|
99
|
+
const message = "Album not found.";
|
|
100
|
+
throw new HttpError(404, message, buildErrorBody(message, "ALBUM_NOT_FOUND"));
|
|
101
|
+
}
|
|
102
|
+
return album;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function buildTrackFilter(query?: TrackQueryDto): TrackFilter | undefined {
|
|
106
|
+
if (!query) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const filter: TrackFilter = {};
|
|
110
|
+
if (query.titleContains) {
|
|
111
|
+
filter.title = { contains: query.titleContains };
|
|
112
|
+
}
|
|
113
|
+
if (query.albumId !== undefined) {
|
|
114
|
+
filter.albumId = { equals: query.albumId };
|
|
115
|
+
}
|
|
116
|
+
return Object.keys(filter).length ? filter : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Controller("/tracks")
|
|
120
|
+
export class TrackController {
|
|
121
|
+
@Get("/")
|
|
122
|
+
@Query(TrackQueryDtoClass)
|
|
123
|
+
@Returns(TrackPagedResponseDto)
|
|
124
|
+
async list(ctx: RequestContext<unknown, TrackQueryDto>) {
|
|
125
|
+
const paginationQuery = (ctx.query ?? {}) as Record<string, unknown>;
|
|
126
|
+
const { page, pageSize } = parsePagination(paginationQuery);
|
|
127
|
+
return withSession(async (session) => {
|
|
128
|
+
const filters = buildTrackFilter(ctx.query);
|
|
129
|
+
const query = applyFilter(
|
|
130
|
+
selectFromEntity(TrackEntity).orderBy(trackRef.id, "ASC"),
|
|
131
|
+
TrackEntity,
|
|
132
|
+
filters
|
|
133
|
+
);
|
|
134
|
+
const paged = await query.executePaged(session, { page, pageSize });
|
|
135
|
+
return toPagedResponse(paged);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@Get("/:id")
|
|
140
|
+
@Params(TrackParamsDto)
|
|
141
|
+
@Returns(TrackDto)
|
|
142
|
+
@TrackErrors
|
|
143
|
+
async getOne(ctx: RequestContext<unknown, undefined, TrackParamsDto>) {
|
|
144
|
+
const id = requireTrackId(ctx.params.id);
|
|
145
|
+
return withSession(async (session) => {
|
|
146
|
+
const track = await getTrackOrThrow(session, id);
|
|
147
|
+
return track as TrackDto;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@Post("/")
|
|
152
|
+
@Body(CreateTrackDto)
|
|
153
|
+
@Returns({ status: 201, schema: TrackDto })
|
|
154
|
+
async create(ctx: RequestContext<CreateTrackDto>) {
|
|
155
|
+
return withSession(async (session) => {
|
|
156
|
+
await getAlbumOrThrow(session, ctx.body.albumId);
|
|
157
|
+
const track = new TrackEntity();
|
|
158
|
+
track.title = ctx.body.title;
|
|
159
|
+
track.durationSeconds = ctx.body.durationSeconds ?? null;
|
|
160
|
+
track.trackNumber = ctx.body.trackNumber ?? null;
|
|
161
|
+
track.albumId = ctx.body.albumId;
|
|
162
|
+
track.createdAt = new Date().toISOString();
|
|
163
|
+
await session.persist(track);
|
|
164
|
+
await session.commit();
|
|
165
|
+
return track as TrackDto;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Put("/:id")
|
|
170
|
+
@Params(TrackParamsDto)
|
|
171
|
+
@Body(ReplaceTrackDto)
|
|
172
|
+
@Returns(TrackDto)
|
|
173
|
+
@TrackErrors
|
|
174
|
+
async replace(ctx: RequestContext<ReplaceTrackDto, undefined, TrackParamsDto>) {
|
|
175
|
+
const id = requireTrackId(ctx.params.id);
|
|
176
|
+
return withSession(async (session) => {
|
|
177
|
+
const entity = await getTrackOrThrow(session, id);
|
|
178
|
+
entity.title = ctx.body.title;
|
|
179
|
+
entity.durationSeconds = ctx.body.durationSeconds ?? null;
|
|
180
|
+
entity.trackNumber = ctx.body.trackNumber ?? null;
|
|
181
|
+
await session.commit();
|
|
182
|
+
return entity as TrackDto;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@Patch("/:id")
|
|
187
|
+
@Params(TrackParamsDto)
|
|
188
|
+
@Body(UpdateTrackDto)
|
|
189
|
+
@Returns(TrackDto)
|
|
190
|
+
@TrackErrors
|
|
191
|
+
async update(ctx: RequestContext<UpdateTrackDto, undefined, TrackParamsDto>) {
|
|
192
|
+
const id = requireTrackId(ctx.params.id);
|
|
193
|
+
return withSession(async (session) => {
|
|
194
|
+
const entity = await getTrackOrThrow(session, id);
|
|
195
|
+
if (ctx.body.title !== undefined) {
|
|
196
|
+
entity.title = ctx.body.title;
|
|
197
|
+
}
|
|
198
|
+
if (ctx.body.durationSeconds !== undefined) {
|
|
199
|
+
entity.durationSeconds = ctx.body.durationSeconds ?? null;
|
|
200
|
+
}
|
|
201
|
+
if (ctx.body.trackNumber !== undefined) {
|
|
202
|
+
entity.trackNumber = ctx.body.trackNumber ?? null;
|
|
203
|
+
}
|
|
204
|
+
await session.commit();
|
|
205
|
+
return entity as TrackDto;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
@Delete("/:id")
|
|
210
|
+
@Params(TrackParamsDto)
|
|
211
|
+
@Returns({ status: 204 })
|
|
212
|
+
@TrackErrors
|
|
213
|
+
async remove(ctx: RequestContext<unknown, undefined, TrackParamsDto>) {
|
|
214
|
+
const id = requireTrackId(ctx.params.id);
|
|
215
|
+
return withSession(async (session) => {
|
|
216
|
+
const track = await getTrackOrThrow(session, id);
|
|
217
|
+
await session.remove(track);
|
|
218
|
+
await session.commit();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Errors,
|
|
3
|
+
createMetalCrudDtoClasses,
|
|
4
|
+
createMetalDtoOverrides,
|
|
5
|
+
createPagedResponseDtoClass,
|
|
6
|
+
createNestedCreateDtoClass,
|
|
7
|
+
createPagedFilterQueryDtoClass,
|
|
8
|
+
StandardErrorDto,
|
|
9
|
+
t
|
|
10
|
+
} from "../../src";
|
|
11
|
+
import { Track } from "./track.entity";
|
|
12
|
+
|
|
13
|
+
const trackOverrides = createMetalDtoOverrides(Track, {
|
|
14
|
+
overrides: {
|
|
15
|
+
durationSeconds: t.nullable(t.integer({ minimum: 0 })),
|
|
16
|
+
trackNumber: t.nullable(t.integer({ minimum: 1 }))
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const trackCrud = createMetalCrudDtoClasses(Track, {
|
|
21
|
+
overrides: trackOverrides,
|
|
22
|
+
response: { description: "Track returned by API." },
|
|
23
|
+
mutationExclude: ["id", "createdAt"],
|
|
24
|
+
immutable: ["albumId"]
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const {
|
|
28
|
+
response: TrackDto,
|
|
29
|
+
create: CreateTrackDto,
|
|
30
|
+
replace: ReplaceTrackDto,
|
|
31
|
+
update: UpdateTrackDto,
|
|
32
|
+
params: TrackParamsDto
|
|
33
|
+
} = trackCrud;
|
|
34
|
+
|
|
35
|
+
export type TrackDto = Omit<Track, "album">;
|
|
36
|
+
type TrackMutationDto = Omit<TrackDto, "id" | "createdAt">;
|
|
37
|
+
type TrackUpdateDto = Omit<TrackMutationDto, "albumId">;
|
|
38
|
+
export type CreateTrackDto = TrackMutationDto;
|
|
39
|
+
export type ReplaceTrackDto = TrackUpdateDto;
|
|
40
|
+
export type UpdateTrackDto = Partial<TrackUpdateDto>;
|
|
41
|
+
export type TrackParamsDto = InstanceType<typeof TrackParamsDto>;
|
|
42
|
+
|
|
43
|
+
export const CreateAlbumTrackDtoClass = createNestedCreateDtoClass(
|
|
44
|
+
Track,
|
|
45
|
+
trackOverrides,
|
|
46
|
+
{
|
|
47
|
+
name: "CreateAlbumTrackDto",
|
|
48
|
+
additionalExclude: ["albumId"]
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export interface CreateAlbumTrackDto {
|
|
53
|
+
title: string;
|
|
54
|
+
durationSeconds?: number | null;
|
|
55
|
+
trackNumber?: number | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const TrackQueryDtoClass = createPagedFilterQueryDtoClass({
|
|
59
|
+
name: "TrackQueryDto",
|
|
60
|
+
filters: {
|
|
61
|
+
titleContains: { schema: t.string({ minLength: 1 }), operator: "contains" },
|
|
62
|
+
albumId: { schema: t.integer({ minimum: 1 }), operator: "equals" }
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export interface TrackQueryDto {
|
|
67
|
+
page?: number;
|
|
68
|
+
pageSize?: number;
|
|
69
|
+
titleContains?: string;
|
|
70
|
+
albumId?: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const TrackPagedResponseDto = createPagedResponseDtoClass({
|
|
74
|
+
name: "TrackPagedResponseDto",
|
|
75
|
+
itemDto: TrackDto,
|
|
76
|
+
description: "Paged track list response."
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export const TrackErrors = Errors(StandardErrorDto, [
|
|
80
|
+
{ status: 400, description: "Invalid track id." },
|
|
81
|
+
{ status: 404, description: "Track not found." }
|
|
82
|
+
]);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { BelongsTo, Column, Entity, PrimaryKey, col } from "metal-orm";
|
|
2
|
+
import type { BelongsToReference } from "metal-orm";
|
|
3
|
+
import { Album } from "./album.entity";
|
|
4
|
+
|
|
5
|
+
@Entity({ tableName: "tracks" })
|
|
6
|
+
export class Track {
|
|
7
|
+
@PrimaryKey(col.autoIncrement(col.int()))
|
|
8
|
+
id!: number;
|
|
9
|
+
|
|
10
|
+
@Column(col.notNull(col.text()))
|
|
11
|
+
title!: string;
|
|
12
|
+
|
|
13
|
+
@Column(col.int())
|
|
14
|
+
durationSeconds?: number | null;
|
|
15
|
+
|
|
16
|
+
@Column(col.int())
|
|
17
|
+
trackNumber?: number | null;
|
|
18
|
+
|
|
19
|
+
@Column(col.notNull(col.int()))
|
|
20
|
+
albumId!: number;
|
|
21
|
+
|
|
22
|
+
@Column(col.notNull(col.text()))
|
|
23
|
+
createdAt!: string;
|
|
24
|
+
|
|
25
|
+
@BelongsTo({ target: () => Album, foreignKey: "albumId" })
|
|
26
|
+
album!: BelongsToReference<Album>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildOpenApi } from "../../src";
|
|
2
|
+
import { HealthController } from "./health.controller";
|
|
3
|
+
|
|
4
|
+
const doc = buildOpenApi({
|
|
5
|
+
info: {
|
|
6
|
+
title: "Health API",
|
|
7
|
+
version: "1.0.0"
|
|
8
|
+
},
|
|
9
|
+
controllers: [HealthController]
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createExpressApp } from "../../src";
|
|
2
|
+
import { TaskController } from "./task.controller";
|
|
3
|
+
|
|
4
|
+
export function createApp() {
|
|
5
|
+
return createExpressApp({
|
|
6
|
+
controllers: [TaskController],
|
|
7
|
+
openApi: {
|
|
8
|
+
info: {
|
|
9
|
+
title: "Tasks API",
|
|
10
|
+
version: "1.0.0"
|
|
11
|
+
},
|
|
12
|
+
docs: true
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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
|
+
coerce,
|
|
14
|
+
t,
|
|
15
|
+
type RequestContext
|
|
16
|
+
} from "../../src";
|
|
17
|
+
import {
|
|
18
|
+
CreateTaskDto,
|
|
19
|
+
ReplaceTaskDto,
|
|
20
|
+
TaskDto,
|
|
21
|
+
TaskErrors,
|
|
22
|
+
TaskParamsDto,
|
|
23
|
+
TaskQueryDto,
|
|
24
|
+
UpdateTaskDto
|
|
25
|
+
} from "./task.dtos";
|
|
26
|
+
import {
|
|
27
|
+
createTask,
|
|
28
|
+
getTask,
|
|
29
|
+
listTasks,
|
|
30
|
+
removeTask,
|
|
31
|
+
replaceTask,
|
|
32
|
+
updateTask
|
|
33
|
+
} from "./task.store";
|
|
34
|
+
|
|
35
|
+
function parseTaskId(value: string) {
|
|
36
|
+
const id = coerce.id(value);
|
|
37
|
+
if (id === undefined) {
|
|
38
|
+
throw new HttpError(400, "Invalid task id.");
|
|
39
|
+
}
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Controller("/tasks")
|
|
44
|
+
export class TaskController {
|
|
45
|
+
@Get("/")
|
|
46
|
+
@Query(TaskQueryDto)
|
|
47
|
+
@Returns(t.array(t.ref(TaskDto)))
|
|
48
|
+
list(ctx: RequestContext<unknown, TaskQueryDto>) {
|
|
49
|
+
const offset =
|
|
50
|
+
coerce.integer(ctx.query?.offset, { min: 0, clamp: true }) ?? 0;
|
|
51
|
+
const limit =
|
|
52
|
+
coerce.integer(ctx.query?.limit, { min: 1, max: 100, clamp: true }) ??
|
|
53
|
+
25;
|
|
54
|
+
const completed = coerce.boolean(ctx.query?.completed);
|
|
55
|
+
|
|
56
|
+
return listTasks({ offset, limit, completed });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Get("/:id")
|
|
60
|
+
@Params(TaskParamsDto)
|
|
61
|
+
@Returns(TaskDto)
|
|
62
|
+
@TaskErrors
|
|
63
|
+
getOne(ctx: RequestContext<unknown, undefined, { id: string }>) {
|
|
64
|
+
const id = parseTaskId(ctx.params.id);
|
|
65
|
+
const task = getTask(id);
|
|
66
|
+
if (!task) {
|
|
67
|
+
throw new HttpError(404, "Task not found.");
|
|
68
|
+
}
|
|
69
|
+
return task;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Post("/")
|
|
73
|
+
@Body(CreateTaskDto)
|
|
74
|
+
@Returns({ status: 201, schema: TaskDto, description: "Created." })
|
|
75
|
+
create(ctx: RequestContext<CreateTaskDto>) {
|
|
76
|
+
return createTask(ctx.body);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@Put("/:id")
|
|
80
|
+
@Params(TaskParamsDto)
|
|
81
|
+
@Body(ReplaceTaskDto)
|
|
82
|
+
@Returns(TaskDto)
|
|
83
|
+
@TaskErrors
|
|
84
|
+
replace(ctx: RequestContext<ReplaceTaskDto, undefined, { id: string }>) {
|
|
85
|
+
const id = parseTaskId(ctx.params.id);
|
|
86
|
+
const updated = replaceTask(id, ctx.body);
|
|
87
|
+
if (!updated) {
|
|
88
|
+
throw new HttpError(404, "Task not found.");
|
|
89
|
+
}
|
|
90
|
+
return updated;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@Patch("/:id")
|
|
94
|
+
@Params(TaskParamsDto)
|
|
95
|
+
@Body(UpdateTaskDto)
|
|
96
|
+
@Returns(TaskDto)
|
|
97
|
+
@TaskErrors
|
|
98
|
+
update(ctx: RequestContext<UpdateTaskDto, undefined, { id: string }>) {
|
|
99
|
+
const id = parseTaskId(ctx.params.id);
|
|
100
|
+
const task = updateTask(id, ctx.body);
|
|
101
|
+
if (!task) {
|
|
102
|
+
throw new HttpError(404, "Task not found.");
|
|
103
|
+
}
|
|
104
|
+
return task;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
@Delete("/:id")
|
|
108
|
+
@Params(TaskParamsDto)
|
|
109
|
+
@Returns({ status: 204, description: "Deleted." })
|
|
110
|
+
@TaskErrors
|
|
111
|
+
remove(ctx: RequestContext<unknown, undefined, { id: string }>) {
|
|
112
|
+
const id = parseTaskId(ctx.params.id);
|
|
113
|
+
const removed = removeTask(id);
|
|
114
|
+
if (!removed) {
|
|
115
|
+
throw new HttpError(404, "Task not found.");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Dto, Errors, Field, OmitDto, PartialDto, PickDto, t } from "../../src";
|
|
2
|
+
|
|
3
|
+
@Dto({ description: "Task returned by the API." })
|
|
4
|
+
export class TaskDto {
|
|
5
|
+
@Field(t.integer({ description: "Task identifier." }))
|
|
6
|
+
id!: number;
|
|
7
|
+
|
|
8
|
+
@Field(t.string({ minLength: 1 }))
|
|
9
|
+
title!: string;
|
|
10
|
+
|
|
11
|
+
@Field(t.boolean())
|
|
12
|
+
completed!: boolean;
|
|
13
|
+
|
|
14
|
+
@Field(t.dateTime({ description: "Creation timestamp." }))
|
|
15
|
+
createdAt!: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CreateTaskDto
|
|
19
|
+
extends Omit<TaskDto, "id" | "createdAt" | "completed"> {
|
|
20
|
+
completed?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@OmitDto(TaskDto, ["id", "createdAt"], {
|
|
24
|
+
overrides: {
|
|
25
|
+
completed: { optional: true }
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
export class CreateTaskDto {}
|
|
29
|
+
|
|
30
|
+
export interface ReplaceTaskDto extends Omit<TaskDto, "id" | "createdAt"> {}
|
|
31
|
+
|
|
32
|
+
@OmitDto(TaskDto, ["id", "createdAt"])
|
|
33
|
+
export class ReplaceTaskDto {}
|
|
34
|
+
|
|
35
|
+
export interface UpdateTaskDto extends Partial<ReplaceTaskDto> {}
|
|
36
|
+
|
|
37
|
+
@PartialDto(ReplaceTaskDto)
|
|
38
|
+
export class UpdateTaskDto {}
|
|
39
|
+
|
|
40
|
+
export interface TaskParamsDto extends Pick<TaskDto, "id"> {}
|
|
41
|
+
|
|
42
|
+
@PickDto(TaskDto, ["id"])
|
|
43
|
+
export class TaskParamsDto {}
|
|
44
|
+
|
|
45
|
+
@Dto()
|
|
46
|
+
export class TaskQueryDto {
|
|
47
|
+
@Field(t.optional(t.integer({ minimum: 0 })))
|
|
48
|
+
offset?: number;
|
|
49
|
+
|
|
50
|
+
@Field(t.optional(t.integer({ minimum: 1, maximum: 100 })))
|
|
51
|
+
limit?: number;
|
|
52
|
+
|
|
53
|
+
@Field(t.optional(t.boolean()))
|
|
54
|
+
completed?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
@Dto()
|
|
58
|
+
class ErrorDto {
|
|
59
|
+
@Field(t.string())
|
|
60
|
+
message!: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export const TaskErrors = Errors(ErrorDto, [
|
|
64
|
+
{ status: 400, description: "Invalid id." },
|
|
65
|
+
{ status: 404, description: "Not found." }
|
|
66
|
+
]);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type TaskRecord = {
|
|
2
|
+
id: number;
|
|
3
|
+
title: string;
|
|
4
|
+
completed: boolean;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
let nextId = 1;
|
|
9
|
+
const tasks: TaskRecord[] = [
|
|
10
|
+
{
|
|
11
|
+
id: nextId++,
|
|
12
|
+
title: "Sketch the REST surface",
|
|
13
|
+
completed: false,
|
|
14
|
+
createdAt: new Date().toISOString()
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: nextId++,
|
|
18
|
+
title: "Ship the first version",
|
|
19
|
+
completed: true,
|
|
20
|
+
createdAt: new Date().toISOString()
|
|
21
|
+
}
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
type TaskFilters = {
|
|
25
|
+
offset: number;
|
|
26
|
+
limit: number;
|
|
27
|
+
completed?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function listTasks({ offset, limit, completed }: TaskFilters) {
|
|
31
|
+
let result = tasks;
|
|
32
|
+
if (completed !== undefined) {
|
|
33
|
+
result = result.filter((task) => task.completed === completed);
|
|
34
|
+
}
|
|
35
|
+
return result.slice(offset, offset + limit);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getTask(id: number) {
|
|
39
|
+
return tasks.find((entry) => entry.id === id);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createTask(data: { title: string; completed?: boolean }) {
|
|
43
|
+
const task: TaskRecord = {
|
|
44
|
+
id: nextId++,
|
|
45
|
+
title: data.title,
|
|
46
|
+
completed: data.completed ?? false,
|
|
47
|
+
createdAt: new Date().toISOString()
|
|
48
|
+
};
|
|
49
|
+
tasks.push(task);
|
|
50
|
+
return task;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function replaceTask(
|
|
54
|
+
id: number,
|
|
55
|
+
data: { title: string; completed: boolean }
|
|
56
|
+
) {
|
|
57
|
+
const index = tasks.findIndex((entry) => entry.id === id);
|
|
58
|
+
if (index === -1) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const updated: TaskRecord = {
|
|
62
|
+
id,
|
|
63
|
+
title: data.title,
|
|
64
|
+
completed: data.completed,
|
|
65
|
+
createdAt: tasks[index].createdAt
|
|
66
|
+
};
|
|
67
|
+
tasks[index] = updated;
|
|
68
|
+
return updated;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function updateTask(
|
|
72
|
+
id: number,
|
|
73
|
+
data: { title?: string; completed?: boolean }
|
|
74
|
+
) {
|
|
75
|
+
const task = tasks.find((entry) => entry.id === id);
|
|
76
|
+
if (!task) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (data.title !== undefined) {
|
|
80
|
+
task.title = data.title;
|
|
81
|
+
}
|
|
82
|
+
if (data.completed !== undefined) {
|
|
83
|
+
task.completed = data.completed;
|
|
84
|
+
}
|
|
85
|
+
return task;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function removeTask(id: number) {
|
|
89
|
+
const index = tasks.findIndex((entry) => entry.id === id);
|
|
90
|
+
if (index === -1) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
tasks.splice(index, 1);
|
|
94
|
+
return true;
|
|
95
|
+
}
|