@ddd-ts-kit/http 0.0.0-stage → 0.1.0
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/README.md +388 -2
- package/dist/criteria/coerce-value.d.ts +8 -0
- package/dist/criteria/coerce-value.d.ts.map +1 -0
- package/dist/criteria/coerce-value.js +29 -0
- package/dist/criteria/coerce-value.js.map +1 -0
- package/dist/criteria/criteria-from.d.ts +11 -0
- package/dist/criteria/criteria-from.d.ts.map +1 -0
- package/dist/criteria/criteria-from.js +17 -0
- package/dist/criteria/criteria-from.js.map +1 -0
- package/dist/criteria/criteria-parameters.d.ts +10 -0
- package/dist/criteria/criteria-parameters.d.ts.map +1 -0
- package/dist/criteria/criteria-parameters.js +45 -0
- package/dist/criteria/criteria-parameters.js.map +1 -0
- package/dist/criteria/criteria-query-options.d.ts +6 -0
- package/dist/criteria/criteria-query-options.d.ts.map +1 -0
- package/dist/criteria/criteria-query-options.js +2 -0
- package/dist/criteria/criteria-query-options.js.map +1 -0
- package/dist/criteria/criteria-query-schema.d.ts +10 -0
- package/dist/criteria/criteria-query-schema.d.ts.map +1 -0
- package/dist/criteria/criteria-query-schema.js +12 -0
- package/dist/criteria/criteria-query-schema.js.map +1 -0
- package/dist/criteria/filter-entries.d.ts +9 -0
- package/dist/criteria/filter-entries.d.ts.map +1 -0
- package/dist/criteria/filter-entries.js +28 -0
- package/dist/criteria/filter-entries.js.map +1 -0
- package/dist/criteria/filter-entry.d.ts +6 -0
- package/dist/criteria/filter-entry.d.ts.map +1 -0
- package/dist/criteria/filter-entry.js +2 -0
- package/dist/criteria/filter-entry.js.map +1 -0
- package/dist/criteria/parse-condition.d.ts +7 -0
- package/dist/criteria/parse-condition.d.ts.map +1 -0
- package/dist/criteria/parse-condition.js +52 -0
- package/dist/criteria/parse-condition.js.map +1 -0
- package/dist/criteria/parse-criteria-query.d.ts +5 -0
- package/dist/criteria/parse-criteria-query.d.ts.map +1 -0
- package/dist/criteria/parse-criteria-query.js +31 -0
- package/dist/criteria/parse-criteria-query.js.map +1 -0
- package/dist/criteria/parse-filters.d.ts +15 -0
- package/dist/criteria/parse-filters.d.ts.map +1 -0
- package/dist/criteria/parse-filters.js +52 -0
- package/dist/criteria/parse-filters.js.map +1 -0
- package/dist/criteria/sortable-fields.d.ts +4 -0
- package/dist/criteria/sortable-fields.d.ts.map +1 -0
- package/dist/criteria/sortable-fields.js +6 -0
- package/dist/criteria/sortable-fields.js.map +1 -0
- package/dist/criteria/split-list.d.ts +3 -0
- package/dist/criteria/split-list.d.ts.map +1 -0
- package/dist/criteria/split-list.js +19 -0
- package/dist/criteria/split-list.js.map +1 -0
- package/dist/docs/docs-page.d.ts +11 -0
- package/dist/docs/docs-page.d.ts.map +1 -0
- package/dist/docs/docs-page.js +18 -0
- package/dist/docs/docs-page.js.map +1 -0
- package/dist/docs/docs-ui.d.ts +2 -0
- package/dist/docs/docs-ui.d.ts.map +1 -0
- package/dist/docs/docs-ui.js +2 -0
- package/dist/docs/docs-ui.js.map +1 -0
- package/dist/docs/open-api-docs-routes.d.ts +22 -0
- package/dist/docs/open-api-docs-routes.d.ts.map +1 -0
- package/dist/docs/open-api-docs-routes.js +54 -0
- package/dist/docs/open-api-docs-routes.js.map +1 -0
- package/dist/docs.d.ts +3 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +2 -0
- package/dist/docs.js.map +1 -0
- package/dist/error/create-error-mapper.d.ts +10 -0
- package/dist/error/create-error-mapper.d.ts.map +1 -0
- package/dist/error/create-error-mapper.js +12 -0
- package/dist/error/create-error-mapper.js.map +1 -0
- package/dist/error/default-error-mapping.d.ts +3 -0
- package/dist/error/default-error-mapping.d.ts.map +1 -0
- package/dist/error/default-error-mapping.js +9 -0
- package/dist/error/default-error-mapping.js.map +1 -0
- package/dist/error/error-mapper.d.ts +12 -0
- package/dist/error/error-mapper.d.ts.map +1 -0
- package/dist/error/error-mapper.js +8 -0
- package/dist/error/error-mapper.js.map +1 -0
- package/dist/error/error-mapping.d.ts +3 -0
- package/dist/error/error-mapping.d.ts.map +1 -0
- package/dist/error/error-mapping.js +2 -0
- package/dist/error/error-mapping.js.map +1 -0
- package/dist/error/forbidden-error.d.ts +5 -0
- package/dist/error/forbidden-error.d.ts.map +1 -0
- package/dist/error/forbidden-error.js +5 -0
- package/dist/error/forbidden-error.js.map +1 -0
- package/dist/error/internal-error-code.d.ts +2 -0
- package/dist/error/internal-error-code.d.ts.map +1 -0
- package/dist/error/internal-error-code.js +2 -0
- package/dist/error/internal-error-code.js.map +1 -0
- package/dist/error/problem.d.ts +11 -0
- package/dist/error/problem.d.ts.map +1 -0
- package/dist/error/problem.js +2 -0
- package/dist/error/problem.js.map +1 -0
- package/dist/error/response-contract-error.d.ts +7 -0
- package/dist/error/response-contract-error.d.ts.map +1 -0
- package/dist/error/response-contract-error.js +10 -0
- package/dist/error/response-contract-error.js.map +1 -0
- package/dist/error/status-title.d.ts +2 -0
- package/dist/error/status-title.d.ts.map +1 -0
- package/dist/error/status-title.js +28 -0
- package/dist/error/status-title.js.map +1 -0
- package/dist/error/to-problem.d.ts +4 -0
- package/dist/error/to-problem.d.ts.map +1 -0
- package/dist/error/to-problem.js +34 -0
- package/dist/error/to-problem.js.map +1 -0
- package/dist/error/unauthorized-error.d.ts +5 -0
- package/dist/error/unauthorized-error.d.ts.map +1 -0
- package/dist/error/unauthorized-error.js +5 -0
- package/dist/error/unauthorized-error.js.map +1 -0
- package/dist/express/mount-express.d.ts +10 -0
- package/dist/express/mount-express.d.ts.map +1 -0
- package/dist/express/mount-express.js +38 -0
- package/dist/express/mount-express.js.map +1 -0
- package/dist/express.d.ts +2 -0
- package/dist/express.d.ts.map +1 -0
- package/dist/express.js +2 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify/fastify-mount-options.d.ts +3 -0
- package/dist/fastify/fastify-mount-options.d.ts.map +1 -0
- package/dist/fastify/fastify-mount-options.js +2 -0
- package/dist/fastify/fastify-mount-options.js.map +1 -0
- package/dist/fastify/fastify-routes.d.ts +6 -0
- package/dist/fastify/fastify-routes.d.ts.map +1 -0
- package/dist/fastify/fastify-routes.js +10 -0
- package/dist/fastify/fastify-routes.js.map +1 -0
- package/dist/fastify/mount-fastify.d.ts +6 -0
- package/dist/fastify/mount-fastify.d.ts.map +1 -0
- package/dist/fastify/mount-fastify.js +19 -0
- package/dist/fastify/mount-fastify.js.map +1 -0
- package/dist/fastify.d.ts +4 -0
- package/dist/fastify.d.ts.map +1 -0
- package/dist/fastify.js +3 -0
- package/dist/fastify.js.map +1 -0
- package/dist/index.d.ts +50 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/nest/create-routes-module.d.ts +15 -0
- package/dist/nest/create-routes-module.d.ts.map +1 -0
- package/dist/nest/create-routes-module.js +54 -0
- package/dist/nest/create-routes-module.js.map +1 -0
- package/dist/nest/routes-factory.d.ts +8 -0
- package/dist/nest/routes-factory.d.ts.map +1 -0
- package/dist/nest/routes-factory.js +2 -0
- package/dist/nest/routes-factory.js.map +1 -0
- package/dist/nest/routes-mounter.d.ts +9 -0
- package/dist/nest/routes-mounter.d.ts.map +1 -0
- package/dist/nest/routes-mounter.js +8 -0
- package/dist/nest/routes-mounter.js.map +1 -0
- package/dist/nest.d.ts +4 -0
- package/dist/nest.d.ts.map +1 -0
- package/dist/nest.js +3 -0
- package/dist/nest.js.map +1 -0
- package/dist/openapi/check-open-api-drift.d.ts +6 -0
- package/dist/openapi/check-open-api-drift.d.ts.map +1 -0
- package/dist/openapi/check-open-api-drift.js +6 -0
- package/dist/openapi/check-open-api-drift.js.map +1 -0
- package/dist/openapi/compare.d.ts +3 -0
- package/dist/openapi/compare.d.ts.map +1 -0
- package/dist/openapi/compare.js +5 -0
- package/dist/openapi/compare.js.map +1 -0
- package/dist/openapi/definitions.d.ts +14 -0
- package/dist/openapi/definitions.d.ts.map +1 -0
- package/dist/openapi/definitions.js +61 -0
- package/dist/openapi/definitions.js.map +1 -0
- package/dist/openapi/generate-open-api.d.ts +21 -0
- package/dist/openapi/generate-open-api.d.ts.map +1 -0
- package/dist/openapi/generate-open-api.js +43 -0
- package/dist/openapi/generate-open-api.js.map +1 -0
- package/dist/openapi/open-api-document.d.ts +18 -0
- package/dist/openapi/open-api-document.d.ts.map +1 -0
- package/dist/openapi/open-api-document.js +2 -0
- package/dist/openapi/open-api-document.js.map +1 -0
- package/dist/openapi/open-api-info.d.ts +6 -0
- package/dist/openapi/open-api-info.d.ts.map +1 -0
- package/dist/openapi/open-api-info.js +2 -0
- package/dist/openapi/open-api-info.js.map +1 -0
- package/dist/openapi/open-api-path.d.ts +2 -0
- package/dist/openapi/open-api-path.d.ts.map +1 -0
- package/dist/openapi/open-api-path.js +4 -0
- package/dist/openapi/open-api-path.js.map +1 -0
- package/dist/openapi/open-api-server.d.ts +5 -0
- package/dist/openapi/open-api-server.d.ts.map +1 -0
- package/dist/openapi/open-api-server.js +2 -0
- package/dist/openapi/open-api-server.js.map +1 -0
- package/dist/openapi/path-items.d.ts +7 -0
- package/dist/openapi/path-items.d.ts.map +1 -0
- package/dist/openapi/path-items.js +43 -0
- package/dist/openapi/path-items.js.map +1 -0
- package/dist/openapi/path-parameters.d.ts +4 -0
- package/dist/openapi/path-parameters.d.ts.map +1 -0
- package/dist/openapi/path-parameters.js +13 -0
- package/dist/openapi/path-parameters.js.map +1 -0
- package/dist/openapi/problem-schema.d.ts +3 -0
- package/dist/openapi/problem-schema.d.ts.map +1 -0
- package/dist/openapi/problem-schema.js +27 -0
- package/dist/openapi/problem-schema.js.map +1 -0
- package/dist/openapi/requirement.d.ts +3 -0
- package/dist/openapi/requirement.d.ts.map +1 -0
- package/dist/openapi/requirement.js +5 -0
- package/dist/openapi/requirement.js.map +1 -0
- package/dist/openapi/responses.d.ts +7 -0
- package/dist/openapi/responses.d.ts.map +1 -0
- package/dist/openapi/responses.js +50 -0
- package/dist/openapi/responses.js.map +1 -0
- package/dist/openapi/schema-parameters.d.ts +5 -0
- package/dist/openapi/schema-parameters.d.ts.map +1 -0
- package/dist/openapi/schema-parameters.js +24 -0
- package/dist/openapi/schema-parameters.js.map +1 -0
- package/dist/openapi/stringify-open-api.d.ts +4 -0
- package/dist/openapi/stringify-open-api.d.ts.map +1 -0
- package/dist/openapi/stringify-open-api.js +5 -0
- package/dist/openapi/stringify-open-api.js.map +1 -0
- package/dist/openapi.d.ts +7 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +4 -0
- package/dist/openapi.js.map +1 -0
- package/dist/pagination/issue.d.ts +3 -0
- package/dist/pagination/issue.d.ts.map +1 -0
- package/dist/pagination/issue.js +4 -0
- package/dist/pagination/issue.js.map +1 -0
- package/dist/pagination/limits-of.d.ts +3 -0
- package/dist/pagination/limits-of.d.ts.map +1 -0
- package/dist/pagination/limits-of.js +8 -0
- package/dist/pagination/limits-of.js.map +1 -0
- package/dist/pagination/page-body.d.ts +7 -0
- package/dist/pagination/page-body.d.ts.map +1 -0
- package/dist/pagination/page-body.js +2 -0
- package/dist/pagination/page-body.js.map +1 -0
- package/dist/pagination/page-query-options.d.ts +8 -0
- package/dist/pagination/page-query-options.d.ts.map +1 -0
- package/dist/pagination/page-query-options.js +2 -0
- package/dist/pagination/page-query-options.js.map +1 -0
- package/dist/pagination/page-query-schema.d.ts +6 -0
- package/dist/pagination/page-query-schema.d.ts.map +1 -0
- package/dist/pagination/page-query-schema.js +8 -0
- package/dist/pagination/page-query-schema.js.map +1 -0
- package/dist/pagination/page-request-from.d.ts +9 -0
- package/dist/pagination/page-request-from.d.ts.map +1 -0
- package/dist/pagination/page-request-from.js +16 -0
- package/dist/pagination/page-request-from.js.map +1 -0
- package/dist/pagination/page-request-parameters.d.ts +9 -0
- package/dist/pagination/page-request-parameters.d.ts.map +1 -0
- package/dist/pagination/page-request-parameters.js +30 -0
- package/dist/pagination/page-request-parameters.js.map +1 -0
- package/dist/pagination/page-response.d.ts +5 -0
- package/dist/pagination/page-response.d.ts.map +1 -0
- package/dist/pagination/page-response.js +9 -0
- package/dist/pagination/page-response.js.map +1 -0
- package/dist/pagination/page-schema-of.d.ts +9 -0
- package/dist/pagination/page-schema-of.d.ts.map +1 -0
- package/dist/pagination/page-schema-of.js +60 -0
- package/dist/pagination/page-schema-of.js.map +1 -0
- package/dist/pagination/parse-page-query.d.ts +5 -0
- package/dist/pagination/parse-page-query.d.ts.map +1 -0
- package/dist/pagination/parse-page-query.js +108 -0
- package/dist/pagination/parse-page-query.js.map +1 -0
- package/dist/pipeline/assert-declared-response.d.ts +4 -0
- package/dist/pipeline/assert-declared-response.d.ts.map +1 -0
- package/dist/pipeline/assert-declared-response.js +18 -0
- package/dist/pipeline/assert-declared-response.js.map +1 -0
- package/dist/pipeline/correlation-header.d.ts +2 -0
- package/dist/pipeline/correlation-header.d.ts.map +1 -0
- package/dist/pipeline/correlation-header.js +2 -0
- package/dist/pipeline/correlation-header.js.map +1 -0
- package/dist/pipeline/correlation-id-of.d.ts +4 -0
- package/dist/pipeline/correlation-id-of.d.ts.map +1 -0
- package/dist/pipeline/correlation-id-of.js +7 -0
- package/dist/pipeline/correlation-id-of.js.map +1 -0
- package/dist/pipeline/error-context.d.ts +15 -0
- package/dist/pipeline/error-context.d.ts.map +1 -0
- package/dist/pipeline/error-context.js +2 -0
- package/dist/pipeline/error-context.js.map +1 -0
- package/dist/pipeline/execute-options.d.ts +40 -0
- package/dist/pipeline/execute-options.d.ts.map +1 -0
- package/dist/pipeline/execute-options.js +2 -0
- package/dist/pipeline/execute-options.js.map +1 -0
- package/dist/pipeline/execute-route.d.ts +6 -0
- package/dist/pipeline/execute-route.d.ts.map +1 -0
- package/dist/pipeline/execute-route.js +108 -0
- package/dist/pipeline/execute-route.js.map +1 -0
- package/dist/pipeline/execution-of-request.d.ts +16 -0
- package/dist/pipeline/execution-of-request.d.ts.map +1 -0
- package/dist/pipeline/execution-of-request.js +44 -0
- package/dist/pipeline/execution-of-request.js.map +1 -0
- package/dist/pipeline/raw-request.d.ts +10 -0
- package/dist/pipeline/raw-request.d.ts.map +1 -0
- package/dist/pipeline/raw-request.js +2 -0
- package/dist/pipeline/raw-request.js.map +1 -0
- package/dist/pipeline/report-http-errors.d.ts +11 -0
- package/dist/pipeline/report-http-errors.d.ts.map +1 -0
- package/dist/pipeline/report-http-errors.js +17 -0
- package/dist/pipeline/report-http-errors.js.map +1 -0
- package/dist/pipeline/route-around.d.ts +5 -0
- package/dist/pipeline/route-around.d.ts.map +1 -0
- package/dist/pipeline/route-around.js +2 -0
- package/dist/pipeline/route-around.js.map +1 -0
- package/dist/pipeline/route-response.d.ts +7 -0
- package/dist/pipeline/route-response.d.ts.map +1 -0
- package/dist/pipeline/route-response.js +2 -0
- package/dist/pipeline/route-response.js.map +1 -0
- package/dist/pipeline/serialize-body.d.ts +7 -0
- package/dist/pipeline/serialize-body.d.ts.map +1 -0
- package/dist/pipeline/serialize-body.js +16 -0
- package/dist/pipeline/serialize-body.js.map +1 -0
- package/dist/pipeline/validate-request.d.ts +8 -0
- package/dist/pipeline/validate-request.d.ts.map +1 -0
- package/dist/pipeline/validate-request.js +30 -0
- package/dist/pipeline/validate-request.js.map +1 -0
- package/dist/pipeline/well-formed-identifier.d.ts +3 -0
- package/dist/pipeline/well-formed-identifier.d.ts.map +1 -0
- package/dist/pipeline/well-formed-identifier.js +6 -0
- package/dist/pipeline/well-formed-identifier.js.map +1 -0
- package/dist/route/assert-unique-routes.d.ts +3 -0
- package/dist/route/assert-unique-routes.d.ts.map +1 -0
- package/dist/route/assert-unique-routes.js +22 -0
- package/dist/route/assert-unique-routes.js.map +1 -0
- package/dist/route/define-route.d.ts +7 -0
- package/dist/route/define-route.d.ts.map +1 -0
- package/dist/route/define-route.js +17 -0
- package/dist/route/define-route.js.map +1 -0
- package/dist/route/define-routes.d.ts +4 -0
- package/dist/route/define-routes.d.ts.map +1 -0
- package/dist/route/define-routes.js +11 -0
- package/dist/route/define-routes.js.map +1 -0
- package/dist/route/duplicate-route-error.d.ts +4 -0
- package/dist/route/duplicate-route-error.d.ts.map +1 -0
- package/dist/route/duplicate-route-error.js +7 -0
- package/dist/route/duplicate-route-error.js.map +1 -0
- package/dist/route/handler-input.d.ts +23 -0
- package/dist/route/handler-input.d.ts.map +1 -0
- package/dist/route/handler-input.js +2 -0
- package/dist/route/handler-input.js.map +1 -0
- package/dist/route/handler-result.d.ts +20 -0
- package/dist/route/handler-result.d.ts.map +1 -0
- package/dist/route/handler-result.js +2 -0
- package/dist/route/handler-result.js.map +1 -0
- package/dist/route/http-method.d.ts +2 -0
- package/dist/route/http-method.d.ts.map +1 -0
- package/dist/route/http-method.js +2 -0
- package/dist/route/http-method.js.map +1 -0
- package/dist/route/prefix-routes.d.ts +4 -0
- package/dist/route/prefix-routes.d.ts.map +1 -0
- package/dist/route/prefix-routes.js +10 -0
- package/dist/route/prefix-routes.js.map +1 -0
- package/dist/route/raw-headers.d.ts +2 -0
- package/dist/route/raw-headers.d.ts.map +1 -0
- package/dist/route/raw-headers.js +2 -0
- package/dist/route/raw-headers.js.map +1 -0
- package/dist/route/request-context.d.ts +13 -0
- package/dist/route/request-context.d.ts.map +1 -0
- package/dist/route/request-context.js +2 -0
- package/dist/route/request-context.js.map +1 -0
- package/dist/route/request-schemas.d.ts +8 -0
- package/dist/route/request-schemas.d.ts.map +1 -0
- package/dist/route/request-schemas.js +2 -0
- package/dist/route/request-schemas.js.map +1 -0
- package/dist/route/response-spec.d.ts +6 -0
- package/dist/route/response-spec.d.ts.map +1 -0
- package/dist/route/response-spec.js +2 -0
- package/dist/route/response-spec.js.map +1 -0
- package/dist/route/responses-spec.d.ts +5 -0
- package/dist/route/responses-spec.d.ts.map +1 -0
- package/dist/route/responses-spec.js +2 -0
- package/dist/route/responses-spec.js.map +1 -0
- package/dist/route/route-handler.d.ts +6 -0
- package/dist/route/route-handler.d.ts.map +1 -0
- package/dist/route/route-handler.js +2 -0
- package/dist/route/route-handler.js.map +1 -0
- package/dist/route/route-input.d.ts +10 -0
- package/dist/route/route-input.d.ts.map +1 -0
- package/dist/route/route-input.js +2 -0
- package/dist/route/route-input.js.map +1 -0
- package/dist/route/route-output.d.ts +6 -0
- package/dist/route/route-output.d.ts.map +1 -0
- package/dist/route/route-output.js +2 -0
- package/dist/route/route-output.js.map +1 -0
- package/dist/route/route-spec.d.ts +23 -0
- package/dist/route/route-spec.d.ts.map +1 -0
- package/dist/route/route-spec.js +2 -0
- package/dist/route/route-spec.js.map +1 -0
- package/dist/route/route.d.ts +11 -0
- package/dist/route/route.d.ts.map +1 -0
- package/dist/route/route.js +2 -0
- package/dist/route/route.js.map +1 -0
- package/dist/route/without-trailing-slashes.d.ts +3 -0
- package/dist/route/without-trailing-slashes.d.ts.map +1 -0
- package/dist/route/without-trailing-slashes.js +9 -0
- package/dist/route/without-trailing-slashes.js.map +1 -0
- package/dist/support/is-record.d.ts +2 -0
- package/dist/support/is-record.d.ts.map +1 -0
- package/dist/support/is-record.js +4 -0
- package/dist/support/is-record.js.map +1 -0
- package/package.json +82 -4
package/README.md
CHANGED
|
@@ -1,3 +1,389 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ddd-ts-kit/http
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Declare a route once, without a framework. Mount it on Express 5, Fastify 5 or NestJS, and
|
|
4
|
+
generate the OpenAPI 3.1 contract from the same declaration.
|
|
5
|
+
|
|
6
|
+
Every adapter goes through one pipeline (`executeRoute`): validation, handler, error mapping.
|
|
7
|
+
The behaviour is therefore the same on the three frameworks, and one test suite proves it.
|
|
8
|
+
|
|
9
|
+
## Declare
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { defineRoute, defineRoutes } from '@ddd-ts-kit/http';
|
|
14
|
+
import { zodSchema } from '@ddd-ts-kit/validation/zod';
|
|
15
|
+
|
|
16
|
+
const order = zodSchema(z.object({ id: z.string(), total: z.number() }), { io: 'output' });
|
|
17
|
+
|
|
18
|
+
export const orderRoutes = (orders: OrderQueries) =>
|
|
19
|
+
defineRoutes(
|
|
20
|
+
defineRoute(
|
|
21
|
+
{
|
|
22
|
+
method: 'GET',
|
|
23
|
+
path: '/orders/:orderId',
|
|
24
|
+
operationId: 'getOrder',
|
|
25
|
+
summary: 'Read an order',
|
|
26
|
+
tags: ['orders'],
|
|
27
|
+
request: { params: zodSchema(z.object({ orderId: z.string().uuid() })) },
|
|
28
|
+
responses: { 200: { description: 'The order.', body: order } },
|
|
29
|
+
errors: ['NOT_FOUND'],
|
|
30
|
+
security: ['bearer'],
|
|
31
|
+
},
|
|
32
|
+
async ({ params, context }) => ({
|
|
33
|
+
status: 200, // must be a declared status, and the body must match its schema
|
|
34
|
+
body: await orders.byId(params.orderId, context.principal),
|
|
35
|
+
}),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `params`, `query`, `headers` and `body` are typed from the schemas. Any `Schema` of
|
|
41
|
+
`@ddd-ts-kit/validation` works (zod, class-validator, Standard Schema).
|
|
42
|
+
- Paths are literal segments and `:name` parameters, the form Express and Fastify share.
|
|
43
|
+
- Frameworks hand query values over as strings: coerce in the schema (`z.coerce.number()`).
|
|
44
|
+
- `context` is `{ requestId, execution, principal?, raw }`. `execution` is the kernel
|
|
45
|
+
`ExecutionContext` of the request (see "The execution context of a request"); `raw` is the
|
|
46
|
+
framework's request object.
|
|
47
|
+
- `security` is required: the names of the security schemes that apply, or `false` for a route
|
|
48
|
+
anybody may call. A route is never public by omission: leaving it out does not compile, and
|
|
49
|
+
`defineRoute` refuses an empty list, which would say the same as `false` without saying it.
|
|
50
|
+
- `defineRoutes` and every mount refuse a duplicate `operationId` or method and path.
|
|
51
|
+
- `prefixRoutes('/api', routes)` moves routes under a prefix, for the server and the contract.
|
|
52
|
+
|
|
53
|
+
## Errors
|
|
54
|
+
|
|
55
|
+
A handler throws a `DomainError`; its `code` is mapped to a status in one place and answered as
|
|
56
|
+
`application/problem+json` (RFC 9457):
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "type": "urn:problem:not-found", "title": "Not Found", "status": 404, "code": "NOT_FOUND",
|
|
60
|
+
"detail": "Order 7 does not exist.", "requestId": "..." }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Defaults: `VALIDATION_FAILED` 422 (with `issues`, paths prefixed by the part:
|
|
64
|
+
`body.items.0.sku`), `NOT_FOUND` 404, `CONFLICT` 409, `INVARIANT_VIOLATION` 422, `UNAUTHORIZED`
|
|
65
|
+
401, `FORBIDDEN` 403. A domain code that is not mapped answers 422.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createErrorMapper } from '@ddd-ts-kit/http';
|
|
69
|
+
|
|
70
|
+
const errorMapper = createErrorMapper({ ORDER_ALREADY_SHIPPED: 409, QUOTA_EXCEEDED: 429 });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Anything that is not a `DomainError` answers 500 `Internal Server Error` with no detail: the
|
|
74
|
+
message of an unexpected error never leaves the process. Log it from the `onError` hook, and
|
|
75
|
+
report it to the error tracker with `onError: reportHttpErrors(errorReporter)`: every request
|
|
76
|
+
answered 5xx goes to the kernel `ErrorReporter`, once, with the correlation, actor and tenant
|
|
77
|
+
ids of its execution context (`context.execution`, present once the request got that far); a
|
|
78
|
+
4xx never does.
|
|
79
|
+
|
|
80
|
+
## Options shared by the adapters
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import type { ExecuteOptions } from '@ddd-ts-kit/http';
|
|
84
|
+
|
|
85
|
+
const options: ExecuteOptions = {
|
|
86
|
+
errorMapper,
|
|
87
|
+
resolvePrincipal: (raw) => authenticate(raw), // throw UnauthorizedError to refuse
|
|
88
|
+
onError: (error, { requestId, correlationId, operationId, problem }) => {
|
|
89
|
+
if (problem.status >= 500) {
|
|
90
|
+
logger.error({ requestId, correlationId, operationId, error });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
validateResponses: process.env.NODE_ENV === 'test', // checks answers against the declaration
|
|
94
|
+
around: tracedRoutes(), // of @ddd-ts-kit/tracing: the span of the request, named by its route template
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`around(route, respond)` wraps the whole handling of a request, from authentication to the
|
|
99
|
+
answer, refusals included, and is told the route that was matched (`method`, `path` as
|
|
100
|
+
declared, `operationId`). It is where a tracer names a request `POST /orders/:id/lines` instead
|
|
101
|
+
of by its URL; this package depends on no tracing library.
|
|
102
|
+
|
|
103
|
+
The request id is the incoming `x-request-id` when well formed, else a UUID; it is echoed in
|
|
104
|
+
the `x-request-id` response header and in every problem.
|
|
105
|
+
|
|
106
|
+
## The execution context of a request
|
|
107
|
+
|
|
108
|
+
`executeRoute` builds a kernel `ExecutionContext` for every request and hands it to the handler
|
|
109
|
+
as `context.execution`:
|
|
110
|
+
|
|
111
|
+
| Field | Value |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `correlationId` | the incoming `x-correlation-id` when well formed (letters, digits, `._:-`, at most 128), else the request id; echoed in the `x-correlation-id` response header, on problems too, and given to `onError` |
|
|
114
|
+
| `origin` | `http` |
|
|
115
|
+
| `actor`, `session` | from what `resolvePrincipal` answered, read structurally: a `Session` gives both, an `Actor` gives the actor, anything else is the anonymous actor |
|
|
116
|
+
| `tenantId` | the tenant of the actor |
|
|
117
|
+
| `locale` | the locale of the session, else the preferred language of `Accept-Language` |
|
|
118
|
+
| `deadline` | arrival plus `requestTimeoutMs`, when the option is set |
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { Clock, ExecutionScope } from '@ddd-ts-kit/kernel';
|
|
122
|
+
|
|
123
|
+
const options: ExecuteOptions = {
|
|
124
|
+
resolvePrincipal: principalResolver(authenticator), // of @ddd-ts-kit/auth: answers a Session
|
|
125
|
+
executionScope: app.resolve(ExecutionScope), // the scope the application runs its use cases in
|
|
126
|
+
clock: app.resolve(Clock),
|
|
127
|
+
requestTimeoutMs: 10_000,
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
With `executionScope`, every handler runs inside `scope.run(context.execution, ...)`: loggers,
|
|
132
|
+
audit columns, tenant scoping and outgoing headers read the context of the request with no
|
|
133
|
+
wrapper around the handler, and no route can forget it. `withPrincipal` stays, for a scope of
|
|
134
|
+
your own; it runs inside the execution scope. The pipeline does not interrupt a handler at its
|
|
135
|
+
deadline: a timeout wrapper or an outgoing call reads `deadline` and honours it. `clock` is the
|
|
136
|
+
option that says when a request arrived, the system clock by default.
|
|
137
|
+
|
|
138
|
+
Use cases still receive the actor explicitly: `new PlaceOrder(context.execution.actor, body)`,
|
|
139
|
+
or `actorOf(context.principal)` of `@ddd-ts-kit/auth`, which reads the same actor.
|
|
140
|
+
|
|
141
|
+
## In the composition root
|
|
142
|
+
|
|
143
|
+
The package binds and resolves nothing: routes are a plain list and `ExecuteOptions` a plain
|
|
144
|
+
object, both built where the server starts. What it takes from the application are ports,
|
|
145
|
+
resolved by their class:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { createApplication, defineContextModule, kernelModule } from '@ddd-ts-kit/di';
|
|
149
|
+
import { ErrorMapper, createErrorMapper, type ExecuteOptions } from '@ddd-ts-kit/http';
|
|
150
|
+
import { Clock, ExecutionScope } from '@ddd-ts-kit/kernel';
|
|
151
|
+
|
|
152
|
+
const httpModule = defineContextModule({
|
|
153
|
+
name: 'http',
|
|
154
|
+
register(builder) {
|
|
155
|
+
builder.bind(ErrorMapper).toValue(createErrorMapper({ ORDER_ALREADY_SHIPPED: 409 }));
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const app = createApplication({ modules: [kernelModule(), httpModule, orderingModule] });
|
|
160
|
+
|
|
161
|
+
const options: ExecuteOptions = {
|
|
162
|
+
errorMapper: app.resolve(ErrorMapper),
|
|
163
|
+
executionScope: app.resolve(ExecutionScope),
|
|
164
|
+
clock: app.resolve(Clock),
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| What | Is | Why |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `ErrorMapper` | a port: an abstract class, the class is the token | one per application, and the server and the contract generator must be given the same one; binding it is optional, `createErrorMapper(...)` in a constant works as well |
|
|
171
|
+
| `ExecutionScope`, `Clock` | ports of the kernel | resolved from the application and handed over in the options |
|
|
172
|
+
| `resolvePrincipal`, `withPrincipal`, `onError`, `requestId` | functions of `ExecuteOptions` | hooks of one server, written where it starts; the injectable side of authentication is the `Authenticator` of `@ddd-ts-kit/auth`, which `principalResolver` turns into `resolvePrincipal` |
|
|
173
|
+
| the route list | a value, `readonly Route[]` | the result of `defineRoutes(...)`, passed to the mount and to `generateOpenApi` |
|
|
174
|
+
| `Schema` | an interface of `@ddd-ts-kit/validation` | one value per request part, declared in the route |
|
|
175
|
+
|
|
176
|
+
## Pagination
|
|
177
|
+
|
|
178
|
+
One query vocabulary and one answer for every search, on top of the kernel `PageRequest` and
|
|
179
|
+
`Page`: `limit`, `offset` or `cursor`, and `sort` (`placedAt,-total`: comma-separated fields, a
|
|
180
|
+
leading `-` for descending).
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { defineRoute, pageQuerySchema, pageResponse, pageSchemaOf } from '@ddd-ts-kit/http';
|
|
184
|
+
|
|
185
|
+
const searchOrders = defineRoute(
|
|
186
|
+
{
|
|
187
|
+
method: 'GET',
|
|
188
|
+
path: '/orders',
|
|
189
|
+
operationId: 'searchOrders',
|
|
190
|
+
summary: 'Search orders',
|
|
191
|
+
request: { query: pageQuerySchema({ sortable: ['placedAt', 'total'], maxSize: 50 }) },
|
|
192
|
+
responses: { 200: { description: 'A page of orders.', body: pageSchemaOf(order) } },
|
|
193
|
+
security: ['bearer'],
|
|
194
|
+
},
|
|
195
|
+
async ({ query }) => ({
|
|
196
|
+
status: 200, // query is a PageRequest<'placedAt' | 'total'>
|
|
197
|
+
body: pageResponse(await orders.search(query), toOrderDto),
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
| Helper | Does |
|
|
203
|
+
|---|---|
|
|
204
|
+
| `pageRequestFrom(query, options?)` | reads the four parameters, as strings or already coerced, into a `PageRequest`; throws the `ValidationError` of the pipeline, so a wrong parameter answers 422 with one issue per parameter (`query.limit`, `query.sort`...). For a route whose query schema also has filters: declare the four parameters in that schema and call it in the handler |
|
|
205
|
+
| `pageQuerySchema(options?)` | the same reading as a `Schema`, for a route whose whole query is the page |
|
|
206
|
+
| `pageResponse(page, map)` | the JSON body `{ items, total?, nextCursor? }`, each item through `map` |
|
|
207
|
+
| `pageRequestParameters(options?)` | the JSON Schema of the query (one OpenAPI parameter per property); `properties` merges into a wider schema |
|
|
208
|
+
| `pageSchemaOf(itemSchema)` | the `Schema` of a page of items: validates the envelope and each item, describes both in the contract |
|
|
209
|
+
|
|
210
|
+
`options` are the `PageLimits` of the kernel (`defaultSize` 20, `maxSize` 100) plus `sortable`,
|
|
211
|
+
the closed list of fields a client may sort by, and `kind`, the variant of a query that names
|
|
212
|
+
neither `offset` nor `cursor` (`offset` by default). Without `sortable`, a `sort` parameter is
|
|
213
|
+
refused: a client never chooses a column the read port did not offer. `offset` and `cursor`
|
|
214
|
+
together are refused. These helpers depend on no validation library; `pageQuerySchema` and
|
|
215
|
+
`pageSchemaOf` are plain `Schema`s and sit next to zod or class-validator schemas.
|
|
216
|
+
|
|
217
|
+
## Filters and sorting
|
|
218
|
+
|
|
219
|
+
One query grammar for every search route, on top of the kernel `Criteria`: the read port declares
|
|
220
|
+
what it accepts with `defineCriteria`, and the route reads the query into a criteria that
|
|
221
|
+
definition accepts, or answers 422.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { criteriaQuerySchema, defineRoute, pageResponse, pageSchemaOf } from '@ddd-ts-kit/http';
|
|
225
|
+
|
|
226
|
+
const searchAppointments = defineRoute(
|
|
227
|
+
{
|
|
228
|
+
method: 'GET',
|
|
229
|
+
path: '/appointments',
|
|
230
|
+
operationId: 'searchAppointments',
|
|
231
|
+
summary: 'Search appointments',
|
|
232
|
+
request: { query: criteriaQuerySchema(appointmentCriteria) },
|
|
233
|
+
responses: { 200: { description: 'A page of appointments.', body: pageSchemaOf(appointment) } },
|
|
234
|
+
security: ['bearer'],
|
|
235
|
+
},
|
|
236
|
+
async ({ query, context }) => ({
|
|
237
|
+
status: 200, // query is a Criteria<AppointmentRow>
|
|
238
|
+
body: pageResponse(await queries.execute(new SearchAppointments(context.execution.actor, query)), toAppointmentDto),
|
|
239
|
+
}),
|
|
240
|
+
);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
GET /appointments?filter[status]=in:booked,confirmed&filter[startsAt]=gte:2026-10-01T00:00:00Z
|
|
245
|
+
&filter[or][practitionerId]=eq:p-7&filter[or][remote]=true
|
|
246
|
+
&sort=-startsAt,id&limit=20&cursor=eyJzdGFydHNBdCI6...
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
| Written | Means |
|
|
250
|
+
|---|---|
|
|
251
|
+
| `filter[<field>]=<operator>:<value>` | one condition. The operators are those of the kernel: `eq`, `ne`, `in`, `gt`, `gte`, `lt`, `lte`, `contains`, `startsWith`, `isNull`, `between` |
|
|
252
|
+
| `filter[<field>]=<value>` | `eq`. The text before the first colon is an operator only when it is the name of one: `filter[title]=10:30 review` is an `eq`, and a value that starts like an operator is written `eq:in:the morning` |
|
|
253
|
+
| several `filter[...]` | joined by AND; the same field may be repeated: `filter[seats]=gte:2&filter[seats]=lte:4` |
|
|
254
|
+
| `in:<a>,<b>,<c>` | a list, separated by commas. A comma inside a value is written `\,`, a backslash `\\`; a colon needs nothing |
|
|
255
|
+
| `between:<from>,<to>` | two values, both bounds included |
|
|
256
|
+
| `isNull:true`, `isNull:false`, `isNull` | without a value, or with one; `isNull` alone means `true` |
|
|
257
|
+
| `contains:<text>`, `startsWith:<text>` | the text as it is, commas included; case sensitive |
|
|
258
|
+
| `filter[or][<field>]=...` | the conditions written under `or` are alternatives; under `not`, the rows that satisfy them all are refused; `and` exists for symmetry |
|
|
259
|
+
| `filter[or][0][<field>]=...&filter[or][0][<other>]=...&filter[or][1][<field>]=...` | an index holds several conditions together inside a group: `or(and(...), ...)`. It counts as one more level of depth, which the definition allows or not (`maxDepth`, 1 by default) |
|
|
260
|
+
| `sort=-startsAt,id` | comma-separated or repeated, a leading `-` for descending, among the fields declared sortable and the tie-breaker |
|
|
261
|
+
| `limit`, `offset`, `cursor` | the page, read by the helpers of "Pagination" with the limits of the definition |
|
|
262
|
+
|
|
263
|
+
A value is read by the type its field declares: a number in plain digits (no exponent), `true`
|
|
264
|
+
or `false`, a decimal kept as written, an instant in ISO 8601 with its zone
|
|
265
|
+
(`2026-10-01T08:00:00Z`, `2026-10-01T10:00:00+02:00`) or a date alone, taken at midnight UTC.
|
|
266
|
+
Percent-encoding is the transport's: `+` in a zone is sent `%2B`. `and`, `or`, `not` and
|
|
267
|
+
numbers are reserved names: a field called so cannot be filtered. Both ways a framework hands
|
|
268
|
+
a query are read: the keys as written (`'filter[status]'`, Express 5 and Fastify by default)
|
|
269
|
+
and already nested (`filter: { status }`, a `qs` parser).
|
|
270
|
+
|
|
271
|
+
Anything the definition does not declare is refused with the 422 problem of the pipeline, one
|
|
272
|
+
issue per mistake, none quoting a value:
|
|
273
|
+
|
|
274
|
+
| `path` | `code` |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `query.filter.<field>` | `unknown_field`, `operator_not_allowed`, `invalid_value`, `in_list_too_long`, `invalid_filter` (not written `filter[<field>]=<operator>:<value>`) |
|
|
277
|
+
| `query.filter` | `too_deep`, `empty_group`, `too_many_filters`, `invalid_filter` |
|
|
278
|
+
| `query.sort` | `unsortable_field`, `invalid_sort` |
|
|
279
|
+
| `query.limit`, `query.offset`, `query.cursor` | `invalid_limit`, `invalid_offset`, `invalid_cursor`, `offset_with_cursor` |
|
|
280
|
+
|
|
281
|
+
| Helper | Does |
|
|
282
|
+
|---|---|
|
|
283
|
+
| `criteriaQuerySchema(definition, { kind? })` | the reading as a `Schema`, for a route whose whole query is the criteria |
|
|
284
|
+
| `criteriaFrom(query, definition, { kind? })` | the same reading in a handler, for a route whose query schema holds other parameters too; throws the `ValidationError` of the pipeline |
|
|
285
|
+
| `criteriaParameters(definition)` | the JSON Schema of the query, one OpenAPI parameter per property: `filter[<field>]` for each field with an operator, with the operators in its description and in `x-filter-operators`, its type in `x-filter-type`; `sort` as the list of the values it accepts; `limit`, `offset` and `cursor` |
|
|
286
|
+
|
|
287
|
+
The scope of the actor is not a parameter: the handler of the query joins it with `scoped`,
|
|
288
|
+
and nothing in a query string can remove it.
|
|
289
|
+
|
|
290
|
+
## Express 5
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
import express from 'express';
|
|
294
|
+
import { mountExpress } from '@ddd-ts-kit/http/express';
|
|
295
|
+
|
|
296
|
+
const app = express();
|
|
297
|
+
app.use(express.json()); // required: the adapter does not parse bodies
|
|
298
|
+
mountExpress(app, orderRoutes(orders), options); // an express.Router() works too
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## Fastify 5
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import Fastify from 'fastify';
|
|
305
|
+
import { fastifyRoutes, mountFastify } from '@ddd-ts-kit/http/fastify';
|
|
306
|
+
|
|
307
|
+
const app = Fastify();
|
|
308
|
+
mountFastify(app, orderRoutes(orders), options);
|
|
309
|
+
// or, as a plugin:
|
|
310
|
+
await app.register(fastifyRoutes(orderRoutes(orders), options), { prefix: '/api' });
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## NestJS
|
|
314
|
+
|
|
315
|
+
No decorator per route: a module mounts the routes on the underlying HTTP server at module
|
|
316
|
+
init, whether Nest runs on Express or on Fastify. Handlers get their dependencies by closure.
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { Module } from '@nestjs/common';
|
|
320
|
+
import { createRoutesModule } from '@ddd-ts-kit/http/nest';
|
|
321
|
+
|
|
322
|
+
@Module({
|
|
323
|
+
imports: [
|
|
324
|
+
createRoutesModule(
|
|
325
|
+
{ inject: [OrderQueries], useFactory: (orders: OrderQueries) => orderRoutes(orders) },
|
|
326
|
+
{ ...options, imports: [OrdersModule] }, // the modules exporting the injected tokens
|
|
327
|
+
),
|
|
328
|
+
],
|
|
329
|
+
})
|
|
330
|
+
export class AppModule {}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
A plain array works as well: `createRoutesModule(routes, options)`. These routes bypass Nest's
|
|
334
|
+
pipes, guards, interceptors, exception filters and global prefix: validation and error mapping
|
|
335
|
+
are those described above. Use `prefixRoutes` for a prefix.
|
|
336
|
+
|
|
337
|
+
## OpenAPI 3.1
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
341
|
+
import { checkOpenApiDrift, generateOpenApi, stringifyOpenApi } from '@ddd-ts-kit/http/openapi';
|
|
342
|
+
|
|
343
|
+
const document = generateOpenApi({
|
|
344
|
+
info: { title: 'Orders', version: '1.0.0' },
|
|
345
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
346
|
+
routes: orderRoutes(orders),
|
|
347
|
+
securitySchemes: { bearer: { type: 'http', scheme: 'bearer' } },
|
|
348
|
+
errorMapper, // the one the server runs with
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
writeFileSync('openapi.json', stringifyOpenApi(document));
|
|
352
|
+
|
|
353
|
+
// In CI: regenerate and fail on a difference.
|
|
354
|
+
if (checkOpenApiDrift(document, readFileSync('openapi.json', 'utf8')).drift) {
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
What is generated: `{param}` paths, one parameter per property of the params, query and headers
|
|
360
|
+
schemas, the request body, the declared responses, the `security` of every operation (an empty
|
|
361
|
+
list for a public route), one `application/problem+json` response per
|
|
362
|
+
status of the declared `errors` (codes listed in the description and in `x-error-codes`), 422
|
|
363
|
+
whenever the route validates input, a shared `Problem` schema. Local `$defs` of the schemas are
|
|
364
|
+
moved to `components.schemas`. Paths, statuses and component keys are sorted: the output does
|
|
365
|
+
not depend on the order of the routes, so the committed file diffs cleanly.
|
|
366
|
+
|
|
367
|
+
## Serving the contract and its page
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import { openApiDocsRoutes } from '@ddd-ts-kit/http/docs';
|
|
371
|
+
|
|
372
|
+
const routes = [...orderRoutes(orders), ...openApiDocsRoutes(document, { ui: 'scalar' })];
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Two public routes (`security: false`), mounted next to the application routes and absent
|
|
376
|
+
from the contract they display:
|
|
377
|
+
the JSON document (`jsonPath`, default `/openapi.json`) and an interactive page (`uiPath`,
|
|
378
|
+
default `/docs`) rendered by `ui`: `swagger` (default), `scalar` or `redoc`. The page loads the
|
|
379
|
+
assets of the UI from a public CDN pinned to a major version; `assetsUrl` points it at
|
|
380
|
+
self-hosted files when it must work offline or under a strict content security policy. A
|
|
381
|
+
project generated by the CLI mounts them in `src/platform/http/with-documentation.ts`, with
|
|
382
|
+
the UI chosen by `--docs-ui`, unless documentation is switched off by its configuration.
|
|
383
|
+
|
|
384
|
+
## Limits
|
|
385
|
+
|
|
386
|
+
- A body the framework itself refuses (malformed JSON, payload too large) is answered by the
|
|
387
|
+
framework's own error handler, before the pipeline runs.
|
|
388
|
+
- Routes are JSON first. A handler may answer a string or bytes by setting its own
|
|
389
|
+
`content-type` header; streaming is out of scope.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FieldType, FieldValue } from '@ddd-ts-kit/kernel';
|
|
2
|
+
/**
|
|
3
|
+
* A value of a query string as the type of its field reads it, `undefined` when it does not:
|
|
4
|
+
* a number in plain digits, `true` or `false`, an ISO 8601 instant with its zone (or a date,
|
|
5
|
+
* taken at midnight UTC), a decimal kept as written.
|
|
6
|
+
*/
|
|
7
|
+
export declare function coerceValue(type: FieldType, text: string): FieldValue | undefined;
|
|
8
|
+
//# sourceMappingURL=coerce-value.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coerce-value.d.ts","sourceRoot":"","sources":["../../src/criteria/coerce-value.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAKhE;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAajF"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const NUMBER = /^-?\d+(?:\.\d+)?$/;
|
|
2
|
+
const INSTANT = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2}))?$/;
|
|
3
|
+
/**
|
|
4
|
+
* A value of a query string as the type of its field reads it, `undefined` when it does not:
|
|
5
|
+
* a number in plain digits, `true` or `false`, an ISO 8601 instant with its zone (or a date,
|
|
6
|
+
* taken at midnight UTC), a decimal kept as written.
|
|
7
|
+
*/
|
|
8
|
+
export function coerceValue(type, text) {
|
|
9
|
+
switch (type) {
|
|
10
|
+
case 'string':
|
|
11
|
+
return text;
|
|
12
|
+
case 'number':
|
|
13
|
+
return NUMBER.test(text) ? Number(text) : undefined;
|
|
14
|
+
case 'decimal':
|
|
15
|
+
return NUMBER.test(text) ? text : undefined;
|
|
16
|
+
case 'boolean':
|
|
17
|
+
return booleanOf(text);
|
|
18
|
+
case 'instant':
|
|
19
|
+
return instantOf(text);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function booleanOf(text) {
|
|
23
|
+
return text === 'true' || text === 'false' ? text === 'true' : undefined;
|
|
24
|
+
}
|
|
25
|
+
function instantOf(text) {
|
|
26
|
+
const instant = new Date(text);
|
|
27
|
+
return INSTANT.test(text) && !Number.isNaN(instant.getTime()) ? instant : undefined;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=coerce-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coerce-value.js","sourceRoot":"","sources":["../../src/criteria/coerce-value.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,mBAAmB,CAAC;AACnC,MAAM,OAAO,GAAG,oFAAoF,CAAC;AAErG;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,IAAe,EAAE,IAAY;IACvD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,QAAQ;YACX,OAAO,IAAI,CAAC;QACd,KAAK,QAAQ;YACX,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACtD,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;QAC9C,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;QACzB,KAAK,SAAS;YACZ,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3E,CAAC;AAED,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,CAAC;IAE/B,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AACtF,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Criteria, CriteriaDefinition } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import type { CriteriaQueryOptions } from './criteria-query-options.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads the criteria of a search route from its query: `filter[<field>]=<operator>:<value>`,
|
|
5
|
+
* groups, and the `sort`, `limit`, `offset` and `cursor` of a page. Anything the definition of
|
|
6
|
+
* the read port does not declare throws the `ValidationError` of the pipeline: a 422 whose
|
|
7
|
+
* issues name `query.filter.<field>` and a stable code. For a route whose query schema holds
|
|
8
|
+
* other parameters too; `criteriaQuerySchema` is the same reading as a schema.
|
|
9
|
+
*/
|
|
10
|
+
export declare function criteriaFrom<Row>(query: unknown, definition: CriteriaDefinition<Row>, options?: CriteriaQueryOptions): Criteria<Row>;
|
|
11
|
+
//# sourceMappingURL=criteria-from.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-from.d.ts","sourceRoot":"","sources":["../../src/criteria/criteria-from.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEvE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGxE;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAC9B,KAAK,EAAE,OAAO,EACd,UAAU,EAAE,kBAAkB,CAAC,GAAG,CAAC,EACnC,OAAO,GAAE,oBAAyB,GACjC,QAAQ,CAAC,GAAG,CAAC,CAWf"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ValidationError } from '@ddd-ts-kit/validation';
|
|
2
|
+
import { parseCriteriaQuery } from './parse-criteria-query.js';
|
|
3
|
+
/**
|
|
4
|
+
* Reads the criteria of a search route from its query: `filter[<field>]=<operator>:<value>`,
|
|
5
|
+
* groups, and the `sort`, `limit`, `offset` and `cursor` of a page. Anything the definition of
|
|
6
|
+
* the read port does not declare throws the `ValidationError` of the pipeline: a 422 whose
|
|
7
|
+
* issues name `query.filter.<field>` and a stable code. For a route whose query schema holds
|
|
8
|
+
* other parameters too; `criteriaQuerySchema` is the same reading as a schema.
|
|
9
|
+
*/
|
|
10
|
+
export function criteriaFrom(query, definition, options = {}) {
|
|
11
|
+
const parsed = parseCriteriaQuery(query, definition, options);
|
|
12
|
+
if (parsed.ok) {
|
|
13
|
+
return parsed.value;
|
|
14
|
+
}
|
|
15
|
+
throw new ValidationError(parsed.error.map((issue) => ({ ...issue, path: `query.${issue.path}` })), 'The request is not valid.');
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=criteria-from.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-from.js","sourceRoot":"","sources":["../../src/criteria/criteria-from.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEzD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAC1B,KAAc,EACd,UAAmC,EACnC,OAAO,GAAyB,EAAE;IAElC,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IAE9D,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;QACd,OAAO,MAAM,CAAC,KAAK,CAAC;IACtB,CAAC;IAED,MAAM,IAAI,eAAe,CACvB,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,SAAS,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EACxE,2BAA2B,CAC5B,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CriteriaDefinition } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import type { JsonSchema } from '@ddd-ts-kit/validation';
|
|
3
|
+
/**
|
|
4
|
+
* The query parameters of a criteria as a JSON Schema object, one OpenAPI parameter per
|
|
5
|
+
* property: a `filter[<field>]` for each field that can be filtered, its operators in the
|
|
6
|
+
* description and in `x-filter-operators`, its type in `x-filter-type`; `sort` as the list of
|
|
7
|
+
* the values it accepts; `limit`, `offset` and `cursor` as `pageRequestParameters` gives them.
|
|
8
|
+
*/
|
|
9
|
+
export declare function criteriaParameters<Row>(definition: CriteriaDefinition<Row>): JsonSchema;
|
|
10
|
+
//# sourceMappingURL=criteria-parameters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-parameters.d.ts","sourceRoot":"","sources":["../../src/criteria/criteria-parameters.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAoB,MAAM,oBAAoB,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAazD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,UAAU,EAAE,kBAAkB,CAAC,GAAG,CAAC,GAAG,UAAU,CAgBvF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { pageRequestParameters } from '../pagination/page-request-parameters.js';
|
|
2
|
+
import { isRecord } from '../support/is-record.js';
|
|
3
|
+
import { sortableFields } from './sortable-fields.js';
|
|
4
|
+
const WRITTEN = {
|
|
5
|
+
string: 'text',
|
|
6
|
+
number: 'a number',
|
|
7
|
+
boolean: '`true` or `false`',
|
|
8
|
+
instant: 'an ISO 8601 instant with its zone',
|
|
9
|
+
decimal: 'a decimal number',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The query parameters of a criteria as a JSON Schema object, one OpenAPI parameter per
|
|
13
|
+
* property: a `filter[<field>]` for each field that can be filtered, its operators in the
|
|
14
|
+
* description and in `x-filter-operators`, its type in `x-filter-type`; `sort` as the list of
|
|
15
|
+
* the values it accepts; `limit`, `offset` and `cursor` as `pageRequestParameters` gives them.
|
|
16
|
+
*/
|
|
17
|
+
export function criteriaParameters(definition) {
|
|
18
|
+
const { defaultPageSize, maxPageSize } = definition.limits;
|
|
19
|
+
const page = pageRequestParameters({ defaultSize: defaultPageSize, maxSize: maxPageSize })['properties'];
|
|
20
|
+
const filters = Object.entries(definition.fields).flatMap(([field, declared]) => declared.filter.length === 0 ? [] : [[`filter[${field}]`, filterParameter(field, declared)]]);
|
|
21
|
+
return {
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
...Object.fromEntries(filters),
|
|
25
|
+
sort: sortParameter(sortableFields(definition)),
|
|
26
|
+
...(isRecord(page) ? page : {}),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function filterParameter(field, { type, filter }) {
|
|
31
|
+
return {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: `Filters on \`${field}\`, written \`<operator>:<value>\`; the value alone means \`eq\`. Operators: ${filter.join(', ')}. The value is ${WRITTEN[type]}.`,
|
|
34
|
+
'x-filter-operators': [...filter],
|
|
35
|
+
'x-filter-type': type,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function sortParameter(sortable) {
|
|
39
|
+
return {
|
|
40
|
+
type: 'array',
|
|
41
|
+
items: { type: 'string', enum: sortable.flatMap((field) => [field, `-${field}`]) },
|
|
42
|
+
description: 'The fields to sort by, a leading "-" for descending, repeated or separated by commas.',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=criteria-parameters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-parameters.js","sourceRoot":"","sources":["../../src/criteria/criteria-parameters.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,qBAAqB,EAAE,MAAM,0CAA0C,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,MAAM,OAAO,GAAuD;IAClE,MAAM,EAAE,MAAM;IACd,MAAM,EAAE,UAAU;IAClB,OAAO,EAAE,mBAAmB;IAC5B,OAAO,EAAE,mCAAmC;IAC5C,OAAO,EAAE,kBAAkB;CAC5B,CAAC;AAEF;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAM,UAAmC;IACzE,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,GAAG,UAAU,CAAC,MAAM,CAAC;IAC3D,MAAM,IAAI,GAAY,qBAAqB,CAAC,EAAE,WAAW,EAAE,eAAe,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,YAAY,CAAC,CAAC;IAElH,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,QAAQ,CAAC,EAAE,EAAE,CAC9E,QAAQ,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,KAAK,GAAG,EAAE,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAC7F,CAAC;IAEF,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,GAAG,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC;YAC9B,IAAI,EAAE,aAAa,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC;YAC/C,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;SAChC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,KAAa,EAAE,EAAE,IAAI,EAAE,MAAM,EAAoB;IACxE,OAAO;QACL,IAAI,EAAE,QAAQ;QACd,WAAW,EAAE,gBAAgB,KAAK,gFAAgF,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,OAAO,CAAC,IAAI,CAAC,GAAG;QACrK,oBAAoB,EAAE,CAAC,GAAG,MAAM,CAAC;QACjC,eAAe,EAAE,IAAI;KACtB,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,QAA2B;IAChD,OAAO;QACL,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,KAAK,EAAE,CAAC,CAAC,EAAE;QAClF,WAAW,EAAE,uFAAuF;KACrG,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PageRequest } from '@ddd-ts-kit/kernel';
|
|
2
|
+
export interface CriteriaQueryOptions {
|
|
3
|
+
/** The variant of a query that names neither `offset` nor `cursor`. Default `offset`. */
|
|
4
|
+
readonly kind?: PageRequest['kind'];
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=criteria-query-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-query-options.d.ts","sourceRoot":"","sources":["../../src/criteria/criteria-query-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD,MAAM,WAAW,oBAAoB;IACnC,yFAAyF;IACzF,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACrC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-query-options.js","sourceRoot":"","sources":["../../src/criteria/criteria-query-options.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Criteria, CriteriaDefinition } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import { type Schema } from '@ddd-ts-kit/validation';
|
|
3
|
+
import type { CriteriaQueryOptions } from './criteria-query-options.js';
|
|
4
|
+
/**
|
|
5
|
+
* The query of a search route as a `Schema`: `request: { query: criteriaQuerySchema(definition) }`
|
|
6
|
+
* hands the handler a `Criteria` the read port accepts, and documents every filter, the sort
|
|
7
|
+
* and the page in the contract.
|
|
8
|
+
*/
|
|
9
|
+
export declare function criteriaQuerySchema<Row>(definition: CriteriaDefinition<Row>, options?: CriteriaQueryOptions): Schema<Criteria<Row>>;
|
|
10
|
+
//# sourceMappingURL=criteria-query-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-query-schema.d.ts","sourceRoot":"","sources":["../../src/criteria/criteria-query-schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAEnE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGxE;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EACrC,UAAU,EAAE,kBAAkB,CAAC,GAAG,CAAC,EACnC,OAAO,GAAE,oBAAyB,GACjC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAEvB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { customSchema } from '@ddd-ts-kit/validation';
|
|
2
|
+
import { criteriaParameters } from './criteria-parameters.js';
|
|
3
|
+
import { parseCriteriaQuery } from './parse-criteria-query.js';
|
|
4
|
+
/**
|
|
5
|
+
* The query of a search route as a `Schema`: `request: { query: criteriaQuerySchema(definition) }`
|
|
6
|
+
* hands the handler a `Criteria` the read port accepts, and documents every filter, the sort
|
|
7
|
+
* and the page in the contract.
|
|
8
|
+
*/
|
|
9
|
+
export function criteriaQuerySchema(definition, options = {}) {
|
|
10
|
+
return customSchema((query) => parseCriteriaQuery(query, definition, options), criteriaParameters(definition));
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=criteria-query-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"criteria-query-schema.js","sourceRoot":"","sources":["../../src/criteria/criteria-query-schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAe,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE/D;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,UAAmC,EACnC,OAAO,GAAyB,EAAE;IAElC,OAAO,YAAY,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,kBAAkB,CAAC,KAAK,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC,CAAC;AACjH,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FilterEntry } from './filter-entry.js';
|
|
2
|
+
/**
|
|
3
|
+
* The filter parameters of a query, whichever way the framework hands them: the keys as
|
|
4
|
+
* written (`{ 'filter[status]': 'eq:booked' }`, Express 5 and Fastify by default) or already
|
|
5
|
+
* nested (`{ filter: { status: 'eq:booked' } }`, a `qs` parser). A repeated parameter comes as
|
|
6
|
+
* a list of values.
|
|
7
|
+
*/
|
|
8
|
+
export declare function filterEntries(query: Readonly<Record<string, unknown>>): FilterEntry[];
|
|
9
|
+
//# sourceMappingURL=filter-entries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-entries.d.ts","sourceRoot":"","sources":["../../src/criteria/filter-entries.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,WAAW,EAAE,CAQrF"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { isRecord } from '../support/is-record.js';
|
|
2
|
+
const FLAT = /^filter((?:\[[^[\]]*\])+)$/;
|
|
3
|
+
/**
|
|
4
|
+
* The filter parameters of a query, whichever way the framework hands them: the keys as
|
|
5
|
+
* written (`{ 'filter[status]': 'eq:booked' }`, Express 5 and Fastify by default) or already
|
|
6
|
+
* nested (`{ filter: { status: 'eq:booked' } }`, a `qs` parser). A repeated parameter comes as
|
|
7
|
+
* a list of values.
|
|
8
|
+
*/
|
|
9
|
+
export function filterEntries(query) {
|
|
10
|
+
const flat = Object.entries(query).flatMap(([key, value]) => {
|
|
11
|
+
const brackets = FLAT.exec(key)?.[1];
|
|
12
|
+
return brackets === undefined ? [] : [{ path: brackets.slice(1, -1).split(']['), values: listed(value) }];
|
|
13
|
+
});
|
|
14
|
+
return query['filter'] === undefined ? flat : [...flat, ...nested(query['filter'], [])];
|
|
15
|
+
}
|
|
16
|
+
function nested(value, path) {
|
|
17
|
+
if (isRecord(value)) {
|
|
18
|
+
return Object.entries(value).flatMap(([key, inner]) => nested(inner, [...path, key]));
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value) && value.some((item) => isRecord(item))) {
|
|
21
|
+
return value.flatMap((item, index) => nested(item, [...path, String(index)]));
|
|
22
|
+
}
|
|
23
|
+
return [{ path, values: listed(value) }];
|
|
24
|
+
}
|
|
25
|
+
function listed(value) {
|
|
26
|
+
return Array.isArray(value) ? value : [value];
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=filter-entries.js.map
|