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