@decohere/http 0.0.1-dev.125 → 0.0.1-dev.5
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/package.json +4 -13
- package/src/app/app.ts +147 -0
- package/src/app/module.ts +19 -0
- package/src/auth/ability.ts +56 -0
- package/src/auth/auth.config.ts +34 -0
- package/src/auth/auth.extensions.ts +115 -0
- package/src/auth/auth.types.ts +124 -0
- package/src/auth/casl-authorization.service.ts +62 -0
- package/{types/src/auth/principal-provider.d.ts → src/auth/principal-provider.ts} +13 -5
- package/src/auth/safe-outbound-url.ts +117 -0
- package/src/auth/token-service.ts +192 -0
- package/src/builder/builder.ts +118 -0
- package/{es6/src/const.js → src/const.ts} +25 -17
- package/src/health/health.controller.ts +34 -0
- package/src/health/health.extensions.ts +53 -0
- package/src/health/readiness.service.ts +33 -0
- package/{types/src/health/readiness.types.d.ts → src/health/readiness.types.ts} +4 -8
- package/src/hosting/hosted-service-host.ts +102 -0
- package/src/hosting/hosted-service.ts +31 -0
- package/src/http/http.compression.ts +38 -0
- package/src/http/http.extensions.ts +244 -0
- package/{types/src/http/http.options.store.d.ts → src/http/http.options.store.ts} +11 -1
- package/src/http/http.options.ts +41 -0
- package/{types/src/http/http.security.d.ts → src/http/http.security.ts} +16 -10
- package/src/http/http.static.ts +359 -0
- package/{es6/src/index.js → src/index.ts} +1 -0
- package/src/openapi/index.ts +22 -0
- package/src/openapi/openapi.generator.ts +78 -0
- package/src/openapi/openapi.types.ts +60 -0
- package/src/openapi/request-schema.ts +155 -0
- package/src/openapi/request-validator.ts +95 -0
- package/src/providers/fastify.provider.ts +454 -0
- package/src/routing/anvil.types.ts +150 -0
- package/src/routing/decorators/argument.decorator.ts +33 -0
- package/src/routing/decorators/auth.decorator.ts +79 -0
- package/src/routing/decorators/controller.decorator.ts +9 -0
- package/src/routing/decorators/field.decorator.ts +15 -0
- package/src/routing/decorators/request.decorator.ts +34 -0
- package/src/routing/decorators/response.decorator.ts +18 -0
- package/src/routing/decorators/route.decorator.ts +52 -0
- package/src/routing/filters/exception-filter-host.ts +90 -0
- package/src/routing/filters/exception-filter.ts +29 -0
- package/src/routing/middleware/http-access-log.middleware.ts +36 -0
- package/src/routing/middleware/middleware.ts +26 -0
- package/src/routing/middleware/request-id.ts +18 -0
- package/src/routing/path.util.ts +38 -0
- package/src/routing/pipes/pipes.ts +132 -0
- package/src/routing/services/route-argument-resolver.ts +92 -0
- package/src/routing/services/route-registry.ts +229 -0
- package/src/routing/services/router.ts +166 -0
- package/src/sse/sse.decorator.ts +41 -0
- package/src/sse/sse.encoder.ts +86 -0
- package/src/sse/sse.types.ts +47 -0
- package/src/sse/sse.wire.ts +34 -0
- package/src/testing/boot-module.ts +63 -0
- package/src/testing/sse-reader.ts +120 -0
- package/tests/app.hosted-services.spec.ts +292 -0
- package/tests/app.shutdown.integration.spec.ts +160 -0
- package/tests/app.spec.ts +132 -0
- package/tests/auth.ability.spec.ts +48 -0
- package/tests/auth.decorators.spec.ts +20 -0
- package/tests/auth.fail-closed.pentest.spec.ts +77 -0
- package/tests/auth.integration.spec.ts +234 -0
- package/tests/auth.jwks-cache.spec.ts +52 -0
- package/tests/auth.missing-auth.pentest.spec.ts +56 -0
- package/tests/auth.oidc-ssrf.pentest.spec.ts +72 -0
- package/tests/auth.oidc.integration.spec.ts +161 -0
- package/tests/auth.token-service.spec.ts +251 -0
- package/tests/builder.spec.ts +12 -0
- package/tests/health.integration.spec.ts +111 -0
- package/tests/helpers/listen-ephemeral.ts +13 -0
- package/tests/helpers/local-oidc-server.ts +100 -0
- package/tests/http.access-log.integration.spec.ts +111 -0
- package/tests/http.boolean-query.integration.spec.ts +59 -0
- package/tests/http.compression.integration.spec.ts +604 -0
- package/tests/http.date-query.integration.spec.ts +71 -0
- package/tests/http.global-prefix.integration.spec.ts +284 -0
- package/tests/http.number-query.integration.spec.ts +65 -0
- package/tests/http.pino-roll-hmr.integration.spec.ts +79 -0
- package/tests/http.pipeline-order.integration.spec.ts +95 -0
- package/tests/http.request-cleanup.integration.spec.ts +57 -0
- package/tests/http.request-id-scope.pentest.spec.ts +50 -0
- package/tests/http.security.integration.spec.ts +106 -0
- package/tests/http.static.integration.spec.ts +847 -0
- package/tests/http.static.spec.ts +111 -0
- package/tests/http.surface.integration.spec.ts +167 -0
- package/tests/openapi.integration.spec.ts +193 -0
- package/tests/openapi.validator-cache.integration.spec.ts +55 -0
- package/tests/routing.decorators.spec.ts +15 -0
- package/tests/routing.registry.spec.ts +44 -0
- package/tests/routing.resolver.spec.ts +92 -0
- package/tests/sse.integration.spec.ts +421 -0
- package/tests/sse.wire.spec.ts +33 -0
- package/LICENSE +0 -15
- package/commonjs/src/app/app.js +0 -166
- package/commonjs/src/app/index.js +0 -19
- package/commonjs/src/app/module.js +0 -17
- package/commonjs/src/auth/ability.js +0 -54
- package/commonjs/src/auth/auth.config.js +0 -23
- package/commonjs/src/auth/auth.extensions.js +0 -106
- package/commonjs/src/auth/auth.types.js +0 -65
- package/commonjs/src/auth/casl-authorization.service.js +0 -36
- package/commonjs/src/auth/index.js +0 -23
- package/commonjs/src/auth/principal-provider.js +0 -19
- package/commonjs/src/auth/safe-outbound-url.js +0 -102
- package/commonjs/src/auth/token-service.js +0 -151
- package/commonjs/src/builder/builder.js +0 -95
- package/commonjs/src/builder/index.js +0 -18
- package/commonjs/src/const.js +0 -87
- package/commonjs/src/health/health.controller.js +0 -75
- package/commonjs/src/health/health.extensions.js +0 -50
- package/commonjs/src/health/index.js +0 -21
- package/commonjs/src/health/readiness.service.js +0 -67
- package/commonjs/src/health/readiness.types.js +0 -7
- package/commonjs/src/hosting/hosted-service-host.js +0 -88
- package/commonjs/src/hosting/hosted-service.js +0 -11
- package/commonjs/src/hosting/index.js +0 -19
- package/commonjs/src/http/http.compression.js +0 -27
- package/commonjs/src/http/http.extensions.js +0 -269
- package/commonjs/src/http/http.options.js +0 -26
- package/commonjs/src/http/http.options.store.js +0 -20
- package/commonjs/src/http/http.security.js +0 -27
- package/commonjs/src/http/http.static.js +0 -248
- package/commonjs/src/http/index.js +0 -22
- package/commonjs/src/index.js +0 -27
- package/commonjs/src/openapi/index.js +0 -45
- package/commonjs/src/openapi/openapi.generator.js +0 -103
- package/commonjs/src/openapi/openapi.types.js +0 -4
- package/commonjs/src/openapi/request-schema.js +0 -187
- package/commonjs/src/openapi/request-validator.js +0 -85
- package/commonjs/src/providers/fastify.provider.js +0 -391
- package/commonjs/src/providers/index.js +0 -18
- package/commonjs/src/routing/anvil.types.js +0 -99
- package/commonjs/src/routing/decorators/argument.decorator.js +0 -49
- package/commonjs/src/routing/decorators/auth.decorator.js +0 -70
- package/commonjs/src/routing/decorators/controller.decorator.js +0 -16
- package/commonjs/src/routing/decorators/field.decorator.js +0 -25
- package/commonjs/src/routing/decorators/index.js +0 -24
- package/commonjs/src/routing/decorators/request.decorator.js +0 -30
- package/commonjs/src/routing/decorators/response.decorator.js +0 -34
- package/commonjs/src/routing/decorators/route.decorator.js +0 -67
- package/commonjs/src/routing/filters/exception-filter-host.js +0 -145
- package/commonjs/src/routing/filters/exception-filter.js +0 -28
- package/commonjs/src/routing/index.js +0 -25
- package/commonjs/src/routing/middleware/http-access-log.middleware.js +0 -47
- package/commonjs/src/routing/middleware/middleware.js +0 -20
- package/commonjs/src/routing/middleware/request-id.js +0 -30
- package/commonjs/src/routing/path.util.js +0 -51
- package/commonjs/src/routing/pipes/index.js +0 -18
- package/commonjs/src/routing/pipes/pipes.js +0 -145
- package/commonjs/src/routing/services/index.js +0 -18
- package/commonjs/src/routing/services/route-argument-resolver.js +0 -77
- package/commonjs/src/routing/services/route-registry.js +0 -194
- package/commonjs/src/routing/services/router.js +0 -158
- package/commonjs/src/sse/index.js +0 -26
- package/commonjs/src/sse/sse.decorator.js +0 -42
- package/commonjs/src/sse/sse.encoder.js +0 -73
- package/commonjs/src/sse/sse.types.js +0 -36
- package/commonjs/src/sse/sse.wire.js +0 -40
- package/commonjs/src/testing/boot-module.js +0 -38
- package/commonjs/src/testing/index.js +0 -20
- package/commonjs/src/testing/sse-reader.js +0 -98
- package/es6/src/app/app.js +0 -148
- package/es6/src/app/index.js +0 -2
- package/es6/src/app/module.js +0 -7
- package/es6/src/auth/ability.js +0 -45
- package/es6/src/auth/auth.config.js +0 -19
- package/es6/src/auth/auth.extensions.js +0 -83
- package/es6/src/auth/auth.types.js +0 -35
- package/es6/src/auth/casl-authorization.service.js +0 -40
- package/es6/src/auth/index.js +0 -6
- package/es6/src/auth/principal-provider.js +0 -14
- package/es6/src/auth/safe-outbound-url.js +0 -99
- package/es6/src/auth/token-service.js +0 -141
- package/es6/src/builder/builder.js +0 -85
- package/es6/src/builder/index.js +0 -1
- package/es6/src/health/health.controller.js +0 -65
- package/es6/src/health/health.extensions.js +0 -40
- package/es6/src/health/index.js +0 -4
- package/es6/src/health/readiness.service.js +0 -57
- package/es6/src/health/readiness.types.js +0 -4
- package/es6/src/hosting/hosted-service-host.js +0 -81
- package/es6/src/hosting/hosted-service.js +0 -8
- package/es6/src/hosting/index.js +0 -2
- package/es6/src/http/http.compression.js +0 -9
- package/es6/src/http/http.extensions.js +0 -259
- package/es6/src/http/http.options.js +0 -16
- package/es6/src/http/http.options.store.js +0 -13
- package/es6/src/http/http.security.js +0 -9
- package/es6/src/http/http.static.js +0 -224
- package/es6/src/http/index.js +0 -5
- package/es6/src/openapi/index.js +0 -3
- package/es6/src/openapi/openapi.generator.js +0 -82
- package/es6/src/openapi/openapi.types.js +0 -1
- package/es6/src/openapi/request-schema.js +0 -160
- package/es6/src/openapi/request-validator.js +0 -71
- package/es6/src/providers/fastify.provider.js +0 -376
- package/es6/src/providers/index.js +0 -1
- package/es6/src/routing/anvil.types.js +0 -57
- package/es6/src/routing/decorators/argument.decorator.js +0 -28
- package/es6/src/routing/decorators/auth.decorator.js +0 -58
- package/es6/src/routing/decorators/controller.decorator.js +0 -6
- package/es6/src/routing/decorators/field.decorator.js +0 -15
- package/es6/src/routing/decorators/index.js +0 -7
- package/es6/src/routing/decorators/request.decorator.js +0 -24
- package/es6/src/routing/decorators/response.decorator.js +0 -16
- package/es6/src/routing/decorators/route.decorator.js +0 -37
- package/es6/src/routing/filters/exception-filter-host.js +0 -127
- package/es6/src/routing/filters/exception-filter.js +0 -10
- package/es6/src/routing/index.js +0 -8
- package/es6/src/routing/middleware/http-access-log.middleware.js +0 -37
- package/es6/src/routing/middleware/middleware.js +0 -10
- package/es6/src/routing/middleware/request-id.js +0 -14
- package/es6/src/routing/path.util.js +0 -33
- package/es6/src/routing/pipes/index.js +0 -1
- package/es6/src/routing/pipes/pipes.js +0 -118
- package/es6/src/routing/services/index.js +0 -1
- package/es6/src/routing/services/route-argument-resolver.js +0 -67
- package/es6/src/routing/services/route-registry.js +0 -184
- package/es6/src/routing/services/router.js +0 -148
- package/es6/src/sse/index.js +0 -2
- package/es6/src/sse/sse.decorator.js +0 -31
- package/es6/src/sse/sse.encoder.js +0 -67
- package/es6/src/sse/sse.types.js +0 -22
- package/es6/src/sse/sse.wire.js +0 -30
- package/es6/src/testing/boot-module.js +0 -35
- package/es6/src/testing/index.js +0 -2
- package/es6/src/testing/sse-reader.js +0 -91
- package/types/src/app/app.d.ts +0 -41
- package/types/src/app/module.d.ts +0 -15
- package/types/src/auth/ability.d.ts +0 -19
- package/types/src/auth/auth.config.d.ts +0 -28
- package/types/src/auth/auth.extensions.d.ts +0 -53
- package/types/src/auth/auth.types.d.ts +0 -73
- package/types/src/auth/casl-authorization.service.d.ts +0 -28
- package/types/src/auth/safe-outbound-url.d.ts +0 -9
- package/types/src/auth/token-service.d.ts +0 -31
- package/types/src/builder/builder.d.ts +0 -36
- package/types/src/const.d.ts +0 -32
- package/types/src/health/health.controller.d.ts +0 -16
- package/types/src/health/health.extensions.d.ts +0 -31
- package/types/src/health/readiness.service.d.ts +0 -9
- package/types/src/hosting/hosted-service-host.d.ts +0 -37
- package/types/src/hosting/hosted-service.d.ts +0 -31
- package/types/src/http/http.compression.d.ts +0 -32
- package/types/src/http/http.extensions.d.ts +0 -79
- package/types/src/http/http.options.d.ts +0 -39
- package/types/src/http/http.static.d.ts +0 -160
- package/types/src/index.d.ts +0 -10
- package/types/src/openapi/index.d.ts +0 -6
- package/types/src/openapi/openapi.generator.d.ts +0 -6
- package/types/src/openapi/openapi.types.d.ts +0 -58
- package/types/src/openapi/request-schema.d.ts +0 -21
- package/types/src/openapi/request-validator.d.ts +0 -9
- package/types/src/providers/fastify.provider.d.ts +0 -56
- package/types/src/routing/anvil.types.d.ts +0 -119
- package/types/src/routing/decorators/argument.decorator.d.ts +0 -11
- package/types/src/routing/decorators/auth.decorator.d.ts +0 -30
- package/types/src/routing/decorators/controller.decorator.d.ts +0 -3
- package/types/src/routing/decorators/field.decorator.d.ts +0 -2
- package/types/src/routing/decorators/request.decorator.d.ts +0 -8
- package/types/src/routing/decorators/response.decorator.d.ts +0 -4
- package/types/src/routing/decorators/route.decorator.d.ts +0 -7
- package/types/src/routing/filters/exception-filter-host.d.ts +0 -19
- package/types/src/routing/filters/exception-filter.d.ts +0 -16
- package/types/src/routing/middleware/http-access-log.middleware.d.ts +0 -8
- package/types/src/routing/middleware/middleware.d.ts +0 -10
- package/types/src/routing/middleware/request-id.d.ts +0 -6
- package/types/src/routing/path.util.d.ts +0 -9
- package/types/src/routing/pipes/pipes.d.ts +0 -21
- package/types/src/routing/services/route-argument-resolver.d.ts +0 -10
- package/types/src/routing/services/route-registry.d.ts +0 -20
- package/types/src/routing/services/router.d.ts +0 -22
- package/types/src/sse/sse.decorator.d.ts +0 -17
- package/types/src/sse/sse.encoder.d.ts +0 -16
- package/types/src/sse/sse.types.d.ts +0 -37
- package/types/src/sse/sse.wire.d.ts +0 -9
- package/types/src/testing/boot-module.d.ts +0 -34
- package/types/src/testing/sse-reader.d.ts +0 -29
- /package/{types/src/app/index.d.ts → src/app/index.ts} +0 -0
- /package/{types/src/auth/index.d.ts → src/auth/index.ts} +0 -0
- /package/{types/src/builder/index.d.ts → src/builder/index.ts} +0 -0
- /package/{types/src/health/index.d.ts → src/health/index.ts} +0 -0
- /package/{types/src/hosting/index.d.ts → src/hosting/index.ts} +0 -0
- /package/{types/src/http/index.d.ts → src/http/index.ts} +0 -0
- /package/{types/src/providers/index.d.ts → src/providers/index.ts} +0 -0
- /package/{types/src/routing/decorators/index.d.ts → src/routing/decorators/index.ts} +0 -0
- /package/{types/src/routing/index.d.ts → src/routing/index.ts} +0 -0
- /package/{types/src/routing/pipes/index.d.ts → src/routing/pipes/index.ts} +0 -0
- /package/{types/src/routing/services/index.d.ts → src/routing/services/index.ts} +0 -0
- /package/{types/src/sse/index.d.ts → src/sse/index.ts} +0 -0
- /package/{types/src/testing/index.d.ts → src/testing/index.ts} +0 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
2
|
+
import { defineArgumentMeta } from "@decohere/core/reflection";
|
|
3
|
+
import { MetadataKeys } from "../../const.js";
|
|
4
|
+
import { type ArgumentMeta, type RequestPart, RequestParts } from "../anvil.types.js";
|
|
5
|
+
import type { Pipe } from "../pipes/pipes.js";
|
|
6
|
+
|
|
7
|
+
export type ArgumentOptions = {
|
|
8
|
+
mappingKey?: string;
|
|
9
|
+
argumentLocation?: RequestPart;
|
|
10
|
+
pipes?: Constructable<Pipe>[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Query(key: string) {
|
|
14
|
+
return Argument({ mappingKey: key, argumentLocation: RequestParts.query });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function Param(key: string) {
|
|
18
|
+
return Argument({ mappingKey: key, argumentLocation: RequestParts.params });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function Argument(options: ArgumentOptions) {
|
|
22
|
+
return (target: object, functionName: string, argumentIndex: number) => {
|
|
23
|
+
const { mappingKey, argumentLocation, pipes } = options;
|
|
24
|
+
const func = target[functionName];
|
|
25
|
+
const meta: ArgumentMeta = {
|
|
26
|
+
argumentIndex,
|
|
27
|
+
mappingKey,
|
|
28
|
+
argumentLocation,
|
|
29
|
+
pipes,
|
|
30
|
+
};
|
|
31
|
+
defineArgumentMeta<ArgumentMeta>(MetadataKeys.parameter, meta, func, argumentIndex);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Constructable, FunctionType } from "@decohere/core/reflection";
|
|
2
|
+
import { defineArgumentMeta, defineMeta, getMeta } from "@decohere/core/reflection";
|
|
3
|
+
import { MetadataKeys } from "../../const.js";
|
|
4
|
+
import type { ArgumentMeta } from "../anvil.types.js";
|
|
5
|
+
|
|
6
|
+
export type AuthorizeOptions = {
|
|
7
|
+
action: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Require the principal to be able to perform `action` on `subject`.
|
|
13
|
+
* Uses an IAuthorizationService (e.g. CaslAuthorizationService) registered in DI.
|
|
14
|
+
* When applied to a class, all methods inherit the constraint.
|
|
15
|
+
* When applied to a method, it takes precedence over the class-level decorator.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* \@Authorize("delete", "Article")
|
|
19
|
+
* \@Authorize("manage", "all")
|
|
20
|
+
*/
|
|
21
|
+
export function Authorize(action: string, subject = "all") {
|
|
22
|
+
const options: AuthorizeOptions = { action, subject };
|
|
23
|
+
return (
|
|
24
|
+
target: object | Constructable,
|
|
25
|
+
methodName?: string,
|
|
26
|
+
_descriptor?: PropertyDescriptor,
|
|
27
|
+
) => {
|
|
28
|
+
if (methodName) {
|
|
29
|
+
const fn = (target as object)[methodName];
|
|
30
|
+
defineMeta(MetadataKeys.authorize, options, fn);
|
|
31
|
+
} else {
|
|
32
|
+
defineMeta(MetadataKeys.authorize, options, (target as Constructable).prototype);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Opt a specific route method out of authentication, even when the controller
|
|
39
|
+
* class is decorated with @Authorize.
|
|
40
|
+
*/
|
|
41
|
+
export function AllowAnonymous() {
|
|
42
|
+
return (target: object, methodName: string, _descriptor?: PropertyDescriptor) => {
|
|
43
|
+
const fn = (target as object)[methodName];
|
|
44
|
+
defineMeta(MetadataKeys.allowAnonymous, true, fn);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read the effective @Authorize options for a handler function. */
|
|
49
|
+
export function getAuthorizeOptions(
|
|
50
|
+
controllerPrototype: object,
|
|
51
|
+
handlerFn: object,
|
|
52
|
+
): AuthorizeOptions | undefined {
|
|
53
|
+
// Method-level overrides class-level
|
|
54
|
+
const methodLevel = getMeta<AuthorizeOptions>(MetadataKeys.authorize, handlerFn);
|
|
55
|
+
if (methodLevel !== undefined) return methodLevel;
|
|
56
|
+
return getMeta<AuthorizeOptions>(MetadataKeys.authorize, controllerPrototype);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Returns true only when the handler is explicitly marked @AllowAnonymous. */
|
|
60
|
+
export function isAllowAnonymous(_controllerPrototype: object, handlerFn: object): boolean {
|
|
61
|
+
return getMeta<boolean>(MetadataKeys.allowAnonymous, handlerFn) === true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parameter decorator — injects the authenticated Principal into this argument.
|
|
66
|
+
* The argument resolver will supply the principal verified during the auth step.
|
|
67
|
+
*/
|
|
68
|
+
export function CurrentUser() {
|
|
69
|
+
return (target: object, propertyKey: string, parameterIndex: number) => {
|
|
70
|
+
const fn = (target as Record<string, FunctionType>)[propertyKey];
|
|
71
|
+
const meta: ArgumentMeta = {
|
|
72
|
+
argumentIndex: parameterIndex,
|
|
73
|
+
argumentLocation: "principal",
|
|
74
|
+
mappingKey: undefined,
|
|
75
|
+
pipes: [],
|
|
76
|
+
};
|
|
77
|
+
defineArgumentMeta<ArgumentMeta>(MetadataKeys.parameter, meta, fn, parameterIndex);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
2
|
+
import { MetadataKeys } from "../../const.js";
|
|
3
|
+
import type { ControllerMetadata } from "../anvil.types.js";
|
|
4
|
+
|
|
5
|
+
export function Controller(options?: ControllerMetadata) {
|
|
6
|
+
return (target: Constructable) => {
|
|
7
|
+
Reflect.defineMetadata(MetadataKeys.controller, options, target.prototype);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineMeta, getFieldType, getMeta } from "@decohere/core/reflection";
|
|
2
|
+
import { MetadataKeys } from "../../const.js";
|
|
3
|
+
import type { FieldMeta, RequestPart } from "../anvil.types.js";
|
|
4
|
+
|
|
5
|
+
export function Field(fieldLocation?: RequestPart, mappingKey?: string) {
|
|
6
|
+
return (target: object, fieldName: string) => {
|
|
7
|
+
const fieldMap =
|
|
8
|
+
getMeta<Map<string, FieldMeta>>(MetadataKeys.fieldMap, target) ??
|
|
9
|
+
new Map<string, FieldMeta>();
|
|
10
|
+
|
|
11
|
+
const fieldType = getFieldType(target, fieldName);
|
|
12
|
+
fieldMap.set(fieldName, { fieldKey: fieldName, mappingKey, fieldType, fieldLocation });
|
|
13
|
+
defineMeta<Map<string, FieldMeta>>(MetadataKeys.fieldMap, fieldMap, target);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
2
|
+
import { defineMeta, getFields, getFieldType, getMeta } from "@decohere/core/reflection";
|
|
3
|
+
import { MetadataKeys } from "../../const.js";
|
|
4
|
+
import type { FieldMeta, RequestPart } from "../anvil.types.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Used to map a request object to route params
|
|
8
|
+
* @param mappingKey
|
|
9
|
+
* @returns
|
|
10
|
+
*/
|
|
11
|
+
export function Request(mappingKey?: RequestPart) {
|
|
12
|
+
return (target: Constructable) => {
|
|
13
|
+
const inst = new target();
|
|
14
|
+
|
|
15
|
+
const fieldMap =
|
|
16
|
+
getMeta<Map<string, FieldMeta>>(MetadataKeys.fieldMap, target.prototype) ??
|
|
17
|
+
new Map<string, FieldMeta>();
|
|
18
|
+
|
|
19
|
+
const fields = getFields(inst);
|
|
20
|
+
|
|
21
|
+
for (const field of fields) {
|
|
22
|
+
const fieldType = getFieldType(target.prototype, field.key);
|
|
23
|
+
const overrides = fieldMap.get(field.key);
|
|
24
|
+
fieldMap.set(field.key, {
|
|
25
|
+
fieldKey: field.key,
|
|
26
|
+
mappingKey: overrides?.mappingKey ?? field.key,
|
|
27
|
+
fieldType: overrides?.fieldType ?? fieldType,
|
|
28
|
+
fieldLocation: overrides?.fieldLocation ?? mappingKey,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
defineMeta(MetadataKeys.fieldMap, fieldMap, target.prototype);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineMeta, getMeta } from "@decohere/core/reflection";
|
|
2
|
+
import { MetadataKeys } from "../../const.js";
|
|
3
|
+
|
|
4
|
+
/** Set the HTTP status code returned on a successful handler response. */
|
|
5
|
+
export function HttpCode(statusCode: number) {
|
|
6
|
+
return (_target: object, _methodName: string, descriptor: PropertyDescriptor) => {
|
|
7
|
+
defineMeta(MetadataKeys.httpCode, statusCode, descriptor.value);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Set a response header on a successful handler response. Stackable. */
|
|
12
|
+
export function Header(name: string, value: string) {
|
|
13
|
+
return (_target: object, _methodName: string, descriptor: PropertyDescriptor) => {
|
|
14
|
+
const existing =
|
|
15
|
+
getMeta<Record<string, string>>(MetadataKeys.headers, descriptor.value) ?? {};
|
|
16
|
+
defineMeta(MetadataKeys.headers, { ...existing, [name]: value }, descriptor.value);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineMeta } from "@decohere/core/reflection";
|
|
2
|
+
import { type HTTPMethod, HTTPMethods, MetadataKeys } from "../../const.js";
|
|
3
|
+
import type { RouteMetadata } from "../anvil.types.js";
|
|
4
|
+
import { RouteConfigurationError } from "../anvil.types.js";
|
|
5
|
+
|
|
6
|
+
export function Get(path?: string) {
|
|
7
|
+
return routeDecorator(HTTPMethods.GET, path);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Post(path?: string) {
|
|
11
|
+
return routeDecorator(HTTPMethods.POST, path);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Put(path?: string) {
|
|
15
|
+
return routeDecorator(HTTPMethods.PUT, path);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Patch(path?: string) {
|
|
19
|
+
return routeDecorator(HTTPMethods.PATCH, path);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Delete(path?: string) {
|
|
23
|
+
return routeDecorator(HTTPMethods.DELETE, path);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Route(method: HTTPMethod, path?: string) {
|
|
27
|
+
return routeDecorator(method, path);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Store method + relative path only. Controller / global prefixes are applied
|
|
32
|
+
* at registration time in RouteRegistry (avoids TS decorator-order issues).
|
|
33
|
+
*/
|
|
34
|
+
function routeDecorator(method: HTTPMethod, path: string = "") {
|
|
35
|
+
return (
|
|
36
|
+
targetClass: object,
|
|
37
|
+
functionName: string,
|
|
38
|
+
descriptor?: TypedPropertyDescriptor<(...args: any[]) => any>,
|
|
39
|
+
) => {
|
|
40
|
+
if (path !== "" && !path.startsWith("/")) {
|
|
41
|
+
throw new RouteConfigurationError(
|
|
42
|
+
`Error configuring routes. Path defined in controller ${
|
|
43
|
+
targetClass.constructor.name
|
|
44
|
+
}.${functionName}() should start '/'`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { value: handler } = descriptor;
|
|
49
|
+
const routeConfig: RouteMetadata = { method, path };
|
|
50
|
+
defineMeta(MetadataKeys.route, routeConfig, handler);
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type LifetimeScope, LifetimeScopeToken } from "@decohere/di";
|
|
2
|
+
import { inject, injectable } from "@decohere/di/decorators";
|
|
3
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
4
|
+
import { scribe } from "@decohere/scribe";
|
|
5
|
+
import { HttpOptionsToken } from "../../const.js";
|
|
6
|
+
import type { HttpOptions } from "../../http/http.options.js";
|
|
7
|
+
import { HttpError, ValidationError } from "../anvil.types.js";
|
|
8
|
+
import {
|
|
9
|
+
type ExceptionContext,
|
|
10
|
+
type ExceptionFilter,
|
|
11
|
+
type ExceptionResult,
|
|
12
|
+
getCatchTypes,
|
|
13
|
+
} from "./exception-filter.js";
|
|
14
|
+
|
|
15
|
+
@injectable()
|
|
16
|
+
export class DefaultHttpExceptionFilter implements ExceptionFilter {
|
|
17
|
+
private readonly logger = scribe.logger(DefaultHttpExceptionFilter.name);
|
|
18
|
+
|
|
19
|
+
public catch(error: unknown, _ctx: ExceptionContext): ExceptionResult {
|
|
20
|
+
if (error instanceof ValidationError) {
|
|
21
|
+
return {
|
|
22
|
+
status: 400,
|
|
23
|
+
body: {
|
|
24
|
+
statusCode: 400,
|
|
25
|
+
error: "Bad Request",
|
|
26
|
+
message: error.message,
|
|
27
|
+
errors: error.errors,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (error instanceof HttpError) {
|
|
32
|
+
// Never expose internal auth failure details (header shape, issuer, etc.) to clients.
|
|
33
|
+
if (error.status === 401) {
|
|
34
|
+
this.logger.warn(error.message);
|
|
35
|
+
return { status: 401, body: { message: "Unauthorized" } };
|
|
36
|
+
}
|
|
37
|
+
// Sanitize only true 500s. Keep intentional 503 probe messages (e.g. /ready).
|
|
38
|
+
if (error.status === 500) {
|
|
39
|
+
this.logger.error(error.message);
|
|
40
|
+
return { status: 500, body: { message: "Internal Server Error" } };
|
|
41
|
+
}
|
|
42
|
+
return { status: error.status, body: { message: error.message } };
|
|
43
|
+
}
|
|
44
|
+
if (error instanceof Error) {
|
|
45
|
+
this.logger.error(error.message, error.stack);
|
|
46
|
+
return { status: 500, body: { message: "Internal Server Error" } };
|
|
47
|
+
}
|
|
48
|
+
return { status: 500, body: { message: "Internal Server Error" } };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Resolves the first matching filter for an error.
|
|
54
|
+
* Custom filters (registered via builder) are tried first; default is last.
|
|
55
|
+
*/
|
|
56
|
+
@injectable()
|
|
57
|
+
export class ExceptionFilterHost {
|
|
58
|
+
private filters?: ExceptionFilter[];
|
|
59
|
+
|
|
60
|
+
public constructor(
|
|
61
|
+
@inject(HttpOptionsToken) private readonly options?: HttpOptions,
|
|
62
|
+
@inject(LifetimeScopeToken) private readonly scope?: LifetimeScope,
|
|
63
|
+
) {}
|
|
64
|
+
|
|
65
|
+
public async handle(error: unknown, ctx: ExceptionContext): Promise<ExceptionResult> {
|
|
66
|
+
for (const filter of this.getFilters()) {
|
|
67
|
+
const filterType = filter.constructor as Constructable;
|
|
68
|
+
const types = getCatchTypes(filterType);
|
|
69
|
+
if (types.length === 0 || types.some((t) => error instanceof t)) {
|
|
70
|
+
return await filter.catch(error, ctx);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return new DefaultHttpExceptionFilter().catch(error, ctx);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private getFilters(): ExceptionFilter[] {
|
|
77
|
+
if (this.filters != null) {
|
|
78
|
+
return this.filters;
|
|
79
|
+
}
|
|
80
|
+
const custom =
|
|
81
|
+
this.options?.filterTypes.map((type) => {
|
|
82
|
+
if (this.scope == null) {
|
|
83
|
+
return new (type as Constructable<ExceptionFilter>)();
|
|
84
|
+
}
|
|
85
|
+
return this.scope.resolve<ExceptionFilter>(type);
|
|
86
|
+
}) ?? [];
|
|
87
|
+
this.filters = [...custom, new DefaultHttpExceptionFilter()];
|
|
88
|
+
return this.filters;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Constructable } from "@decohere/core/reflection";
|
|
2
|
+
import { defineMeta, getMeta } from "@decohere/core/reflection";
|
|
3
|
+
import { MetadataKeys } from "../../const.js";
|
|
4
|
+
import type { HttpRequest } from "../anvil.types.js";
|
|
5
|
+
|
|
6
|
+
export type ExceptionResult = {
|
|
7
|
+
status: number;
|
|
8
|
+
body: unknown;
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ExceptionContext = {
|
|
13
|
+
request: HttpRequest;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export interface ExceptionFilter {
|
|
17
|
+
catch(error: unknown, ctx: ExceptionContext): ExceptionResult | Promise<ExceptionResult>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Declare which error types this filter handles. Empty = catch-all. */
|
|
21
|
+
export function Catch(...types: Constructable[]) {
|
|
22
|
+
return (target: Constructable) => {
|
|
23
|
+
defineMeta(MetadataKeys.catch, types, target);
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getCatchTypes(filterType: Constructable): Constructable[] {
|
|
28
|
+
return getMeta<Constructable[]>(MetadataKeys.catch, filterType) ?? [];
|
|
29
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { injectable } from "@decohere/di/decorators";
|
|
2
|
+
import { scribe } from "@decohere/scribe";
|
|
3
|
+
import { HttpError } from "../anvil.types.js";
|
|
4
|
+
import type { Middleware, MiddlewareContext } from "./middleware.js";
|
|
5
|
+
import { REQUEST_ID_HEADER } from "./request-id.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Echoes `x-request-id` on the response and writes an HTTP access line via Scribe
|
|
9
|
+
* (method, path, status, durationMs, requestId). Enabled by default through `withHttp`.
|
|
10
|
+
*/
|
|
11
|
+
@injectable()
|
|
12
|
+
export class HttpAccessLogMiddleware implements Middleware {
|
|
13
|
+
public async use(ctx: MiddlewareContext, next: () => Promise<unknown>): Promise<unknown> {
|
|
14
|
+
const logger = scribe.logger(HttpAccessLogMiddleware.name);
|
|
15
|
+
const start = performance.now();
|
|
16
|
+
ctx.request.responseHeaders[REQUEST_ID_HEADER] = ctx.request.requestId;
|
|
17
|
+
|
|
18
|
+
let status = ctx.route.httpCode ?? 200;
|
|
19
|
+
try {
|
|
20
|
+
return await next();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
status = error instanceof HttpError ? error.status : 500;
|
|
23
|
+
throw error;
|
|
24
|
+
} finally {
|
|
25
|
+
const durationMs = Math.round(performance.now() - start);
|
|
26
|
+
logger.info({
|
|
27
|
+
msg: "access",
|
|
28
|
+
method: ctx.request.method,
|
|
29
|
+
path: ctx.request.url,
|
|
30
|
+
status,
|
|
31
|
+
durationMs,
|
|
32
|
+
requestId: ctx.request.requestId,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { HttpRequest, MappedRoute } from "../anvil.types.js";
|
|
2
|
+
|
|
3
|
+
export type MiddlewareContext = {
|
|
4
|
+
request: HttpRequest;
|
|
5
|
+
route: MappedRoute;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface Middleware {
|
|
9
|
+
use(ctx: MiddlewareContext, next: () => Promise<unknown>): Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Compose middleware around a terminal handler; returns the handler result. */
|
|
13
|
+
export function composeMiddleware(
|
|
14
|
+
middlewares: Middleware[],
|
|
15
|
+
handler: () => Promise<unknown>,
|
|
16
|
+
ctx: MiddlewareContext,
|
|
17
|
+
): Promise<unknown> {
|
|
18
|
+
const dispatch = (index: number): Promise<unknown> => {
|
|
19
|
+
if (index >= middlewares.length) {
|
|
20
|
+
return handler();
|
|
21
|
+
}
|
|
22
|
+
const middleware = middlewares[index];
|
|
23
|
+
return middleware.use(ctx, () => dispatch(index + 1));
|
|
24
|
+
};
|
|
25
|
+
return dispatch(0);
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Canonical inbound/outbound request id header (lowercase). */
|
|
4
|
+
export const REQUEST_ID_HEADER = "x-request-id";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Prefer an inbound `x-request-id`, then an existing id, otherwise generate one.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveRequestId(headers: Record<string, string>, fallback?: string): string {
|
|
10
|
+
const fromHeader = headers[REQUEST_ID_HEADER]?.trim();
|
|
11
|
+
if (fromHeader) {
|
|
12
|
+
return fromHeader;
|
|
13
|
+
}
|
|
14
|
+
if (fallback?.trim()) {
|
|
15
|
+
return fallback.trim();
|
|
16
|
+
}
|
|
17
|
+
return randomUUID();
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Normalize a prefix to `/foo` form with no trailing slash. Empty → `""`. */
|
|
2
|
+
export function normalizePrefix(prefix: string | undefined | null): string {
|
|
3
|
+
if (prefix == null || prefix === "" || prefix === "/") {
|
|
4
|
+
return "";
|
|
5
|
+
}
|
|
6
|
+
const withSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
7
|
+
return withSlash.replace(/\/+$/, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Join a controller prefix with a method-relative route path.
|
|
12
|
+
* Preserves `@Get("/")` trailing-slash semantics (`/hello` + `/` → `/hello/`).
|
|
13
|
+
*/
|
|
14
|
+
export function joinRoutePath(prefix: string, routePath: string): string {
|
|
15
|
+
const p = normalizePrefix(prefix);
|
|
16
|
+
if (routePath === "" || routePath == null) {
|
|
17
|
+
return p === "" ? "/" : p;
|
|
18
|
+
}
|
|
19
|
+
if (routePath === "/") {
|
|
20
|
+
return p === "" ? "/" : `${p}/`;
|
|
21
|
+
}
|
|
22
|
+
if (!routePath.startsWith("/")) {
|
|
23
|
+
throw new Error(`Route path must start with '/': ${routePath}`);
|
|
24
|
+
}
|
|
25
|
+
return `${p}${routePath}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Prepend a global prefix onto an already-joined controller route path. */
|
|
29
|
+
export function applyGlobalPrefix(globalPrefix: string, path: string): string {
|
|
30
|
+
const g = normalizePrefix(globalPrefix);
|
|
31
|
+
if (g === "") {
|
|
32
|
+
return path === "" ? "/" : path;
|
|
33
|
+
}
|
|
34
|
+
if (path === "/") {
|
|
35
|
+
return `${g}/`;
|
|
36
|
+
}
|
|
37
|
+
return `${g}${path.startsWith("/") ? path : `/${path}`}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Type } from "@decohere/core/reflection";
|
|
2
|
+
import { isDateType } from "@decohere/core/reflection";
|
|
3
|
+
import { TransformError, ValidationError } from "../anvil.types.js";
|
|
4
|
+
|
|
5
|
+
export type PipeContext = {
|
|
6
|
+
fieldTypes: Record<string, Type>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const invalidBooleanIssue = {
|
|
10
|
+
path: "/value",
|
|
11
|
+
message: "Invalid boolean",
|
|
12
|
+
keyword: "type",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
const invalidNumberIssue = {
|
|
16
|
+
path: "/value",
|
|
17
|
+
message: "Invalid number",
|
|
18
|
+
keyword: "type",
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
const invalidDateIssue = {
|
|
22
|
+
path: "/value",
|
|
23
|
+
message: "Invalid date",
|
|
24
|
+
keyword: "format",
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export abstract class Pipe<T = unknown> {
|
|
28
|
+
public abstract transform(value: unknown, context?: PipeContext): T;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class BooleanPipe extends Pipe<boolean> {
|
|
32
|
+
public transform(value: unknown, _context?: unknown): boolean {
|
|
33
|
+
if (typeof value === "boolean") {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalized = typeof value === "string" ? value.trim().toLowerCase() : value;
|
|
38
|
+
|
|
39
|
+
switch (normalized) {
|
|
40
|
+
case "true":
|
|
41
|
+
case "1":
|
|
42
|
+
case 1:
|
|
43
|
+
return true;
|
|
44
|
+
case "false":
|
|
45
|
+
case "0":
|
|
46
|
+
case 0:
|
|
47
|
+
return false;
|
|
48
|
+
default:
|
|
49
|
+
throw new ValidationError("Validation failed", [invalidBooleanIssue]);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class DatePipe extends Pipe<Date> {
|
|
55
|
+
public transform(value: unknown, _context?: unknown): Date {
|
|
56
|
+
if (value instanceof Date) {
|
|
57
|
+
if (Number.isNaN(value.getTime())) {
|
|
58
|
+
throw new ValidationError("Validation failed", [invalidDateIssue]);
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (typeof value === "string") {
|
|
64
|
+
const trimmed = value.trim();
|
|
65
|
+
if (trimmed === "") {
|
|
66
|
+
throw new ValidationError("Validation failed", [invalidDateIssue]);
|
|
67
|
+
}
|
|
68
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
69
|
+
return new Date(Number(trimmed));
|
|
70
|
+
}
|
|
71
|
+
const parsed = new Date(trimmed);
|
|
72
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
73
|
+
throw new ValidationError("Validation failed", [invalidDateIssue]);
|
|
74
|
+
}
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof value === "number" && !Number.isNaN(value)) {
|
|
79
|
+
return new Date(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throw new ValidationError("Validation failed", [invalidDateIssue]);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class JsonPipe extends Pipe<object> {
|
|
87
|
+
public transform(value: string, context?: PipeContext): object {
|
|
88
|
+
try {
|
|
89
|
+
const obj = JSON.parse(value, (fieldKey, fieldValue) => {
|
|
90
|
+
const val = this.reviver(context?.fieldTypes, fieldKey, fieldValue);
|
|
91
|
+
return val;
|
|
92
|
+
});
|
|
93
|
+
return obj;
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new TransformError(`Error parsing json ${err}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private reviver(fieldTypes: Record<string, Type>, fieldKey: string, value: unknown) {
|
|
100
|
+
if (!fieldKey) {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!fieldTypes) {
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const fieldType = fieldTypes[fieldKey];
|
|
109
|
+
|
|
110
|
+
if (fieldType == null) {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isDateType(fieldType)) {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return new Date(value.toString());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export class NumberPipe extends Pipe<number> {
|
|
123
|
+
/** Finite numbers only. Blank or non-numeric input is a 400, not `undefined` (#503). */
|
|
124
|
+
public transform(value: unknown, _context?: unknown): number {
|
|
125
|
+
const text = typeof value === "number" ? null : String(value).trim();
|
|
126
|
+
const number = text == null ? (value as number) : text === "" ? Number.NaN : Number(text);
|
|
127
|
+
if (!Number.isFinite(number)) {
|
|
128
|
+
throw new ValidationError("Validation failed", [invalidNumberIssue]);
|
|
129
|
+
}
|
|
130
|
+
return number;
|
|
131
|
+
}
|
|
132
|
+
}
|