@ddd-ts-kit/http 0.2.0 → 0.4.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 +81 -0
- package/dist/controller/controller-class.d.ts +7 -0
- package/dist/controller/controller-class.d.ts.map +1 -0
- package/dist/controller/controller-class.js +2 -0
- package/dist/controller/controller-class.js.map +1 -0
- package/dist/controller/controller-request.d.ts +11 -0
- package/dist/controller/controller-request.d.ts.map +1 -0
- package/dist/controller/controller-request.js +2 -0
- package/dist/controller/controller-request.js.map +1 -0
- package/dist/controller/controller-response.d.ts +10 -0
- package/dist/controller/controller-response.d.ts.map +1 -0
- package/dist/controller/controller-response.js +2 -0
- package/dist/controller/controller-response.js.map +1 -0
- package/dist/controller/controller-result.d.ts +7 -0
- package/dist/controller/controller-result.d.ts.map +1 -0
- package/dist/controller/controller-result.js +2 -0
- package/dist/controller/controller-result.js.map +1 -0
- package/dist/controller/controller-route.d.ts +7 -0
- package/dist/controller/controller-route.d.ts.map +1 -0
- package/dist/controller/controller-route.js +10 -0
- package/dist/controller/controller-route.js.map +1 -0
- package/dist/controller/controller-routes-options.d.ts +19 -0
- package/dist/controller/controller-routes-options.d.ts.map +1 -0
- package/dist/controller/controller-routes-options.js +2 -0
- package/dist/controller/controller-routes-options.js.map +1 -0
- package/dist/controller/controller-routes.d.ts +11 -0
- package/dist/controller/controller-routes.d.ts.map +1 -0
- package/dist/controller/controller-routes.js +72 -0
- package/dist/controller/controller-routes.js.map +1 -0
- package/dist/controller/controller-spec.d.ts +30 -0
- package/dist/controller/controller-spec.d.ts.map +1 -0
- package/dist/controller/controller-spec.js +2 -0
- package/dist/controller/controller-spec.js.map +1 -0
- package/dist/controller/controller.d.ts +20 -0
- package/dist/controller/controller.d.ts.map +1 -0
- package/dist/controller/controller.js +30 -0
- package/dist/controller/controller.js.map +1 -0
- package/dist/controller/declared-route.d.ts +13 -0
- package/dist/controller/declared-route.d.ts.map +1 -0
- package/dist/controller/declared-route.js +54 -0
- package/dist/controller/declared-route.js.map +1 -0
- package/dist/controller/erased-controller.d.ts +9 -0
- package/dist/controller/erased-controller.d.ts.map +1 -0
- package/dist/controller/erased-controller.js +2 -0
- package/dist/controller/erased-controller.js.map +1 -0
- package/dist/controller/guard-documentation.d.ts +10 -0
- package/dist/controller/guard-documentation.d.ts.map +1 -0
- package/dist/controller/guard-documentation.js +2 -0
- package/dist/controller/guard-documentation.js.map +1 -0
- package/dist/controller/guard-reference.d.ts +4 -0
- package/dist/controller/guard-reference.d.ts.map +1 -0
- package/dist/controller/guard-reference.js +2 -0
- package/dist/controller/guard-reference.js.map +1 -0
- package/dist/controller/guard-request.d.ts +20 -0
- package/dist/controller/guard-request.d.ts.map +1 -0
- package/dist/controller/guard-request.js +2 -0
- package/dist/controller/guard-request.js.map +1 -0
- package/dist/controller/guard.d.ts +21 -0
- package/dist/controller/guard.d.ts.map +1 -0
- package/dist/controller/guard.js +10 -0
- package/dist/controller/guard.js.map +1 -0
- package/dist/controller/guards/authenticated.d.ts +4 -0
- package/dist/controller/guards/authenticated.d.ts.map +1 -0
- package/dist/controller/guards/authenticated.js +12 -0
- package/dist/controller/guards/authenticated.js.map +1 -0
- package/dist/controller/guards/has-role.d.ts +8 -0
- package/dist/controller/guards/has-role.d.ts.map +1 -0
- package/dist/controller/guards/has-role.js +26 -0
- package/dist/controller/guards/has-role.js.map +1 -0
- package/dist/controller/guards/public.d.ts +7 -0
- package/dist/controller/guards/public.d.ts.map +1 -0
- package/dist/controller/guards/public.js +11 -0
- package/dist/controller/guards/public.js.map +1 -0
- package/dist/controller/http-controller.d.ts +14 -0
- package/dist/controller/http-controller.d.ts.map +1 -0
- package/dist/controller/http-controller.js +2 -0
- package/dist/controller/http-controller.js.map +1 -0
- package/dist/controller/http-request.d.ts +27 -0
- package/dist/controller/http-request.d.ts.map +1 -0
- package/dist/controller/http-request.js +2 -0
- package/dist/controller/http-request.js.map +1 -0
- package/dist/controller/presentation-registry.d.ts +10 -0
- package/dist/controller/presentation-registry.d.ts.map +1 -0
- package/dist/controller/presentation-registry.js +21 -0
- package/dist/controller/presentation-registry.js.map +1 -0
- package/dist/controller/request-mapper.d.ts +12 -0
- package/dist/controller/request-mapper.d.ts.map +1 -0
- package/dist/controller/request-mapper.js +6 -0
- package/dist/controller/request-mapper.js.map +1 -0
- package/dist/controller/resolved-controller.d.ts +18 -0
- package/dist/controller/resolved-controller.d.ts.map +1 -0
- package/dist/controller/resolved-controller.js +47 -0
- package/dist/controller/resolved-controller.js.map +1 -0
- package/dist/controller/route-group-class.d.ts +3 -0
- package/dist/controller/route-group-class.d.ts.map +1 -0
- package/dist/controller/route-group-class.js +2 -0
- package/dist/controller/route-group-class.js.map +1 -0
- package/dist/controller/route-group-options.d.ts +14 -0
- package/dist/controller/route-group-options.d.ts.map +1 -0
- package/dist/controller/route-group-options.js +2 -0
- package/dist/controller/route-group-options.js.map +1 -0
- package/dist/controller/route-group.d.ts +10 -0
- package/dist/controller/route-group.d.ts.map +1 -0
- package/dist/controller/route-group.js +25 -0
- package/dist/controller/route-group.js.map +1 -0
- package/dist/controller/serializer.d.ts +11 -0
- package/dist/controller/serializer.d.ts.map +1 -0
- package/dist/controller/serializer.js +6 -0
- package/dist/controller/serializer.js.map +1 -0
- package/dist/error/problem.d.ts +3 -0
- package/dist/error/problem.d.ts.map +1 -1
- package/dist/error/to-problem.d.ts.map +1 -1
- package/dist/error/to-problem.js +43 -2
- package/dist/error/to-problem.js.map +1 -1
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/openapi/problem-schema.d.ts.map +1 -1
- package/dist/openapi/problem-schema.js +5 -0
- package/dist/openapi/problem-schema.js.map +1 -1
- package/dist/pipeline/execute-route.js +1 -0
- package/dist/pipeline/execute-route.js.map +1 -1
- package/dist/route/define-route.d.ts.map +1 -1
- package/dist/route/define-route.js +2 -2
- package/dist/route/define-route.js.map +1 -1
- package/dist/route/request-context.d.ts +3 -0
- package/dist/route/request-context.d.ts.map +1 -1
- package/dist/route/route-path-pattern.d.ts +3 -0
- package/dist/route/route-path-pattern.d.ts.map +1 -0
- package/dist/route/route-path-pattern.js +3 -0
- package/dist/route/route-path-pattern.js.map +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -50,6 +50,59 @@ export const orderRoutes = (orders: OrderQueries) =>
|
|
|
50
50
|
- `defineRoutes` and every mount refuse a duplicate `operationId` or method and path.
|
|
51
51
|
- `prefixRoutes('/api', routes)` moves routes under a prefix, for the server and the contract.
|
|
52
52
|
|
|
53
|
+
## Controllers, route groups and guards
|
|
54
|
+
|
|
55
|
+
A route may also be a class: one class per route, one method, `handle`. Its decorator is the
|
|
56
|
+
declaration, and the compiler holds `handle` to it: a part the schemas do not produce, or
|
|
57
|
+
another body than the declared response, is an error on the decorator.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { Authenticated, Controller, RouteGroup, type HttpRequest } from '@ddd-ts-kit/http';
|
|
61
|
+
|
|
62
|
+
@RouteGroup({ prefix: '/orders', tags: ['orders'], guards: [Authenticated], errors: ['FORBIDDEN'] })
|
|
63
|
+
export class OrdersRoutes {}
|
|
64
|
+
|
|
65
|
+
@Controller({
|
|
66
|
+
group: OrdersRoutes,
|
|
67
|
+
method: 'POST',
|
|
68
|
+
path: '/',
|
|
69
|
+
summary: 'Place an order',
|
|
70
|
+
body,
|
|
71
|
+
response: { status: 201, body: placed },
|
|
72
|
+
errors: ['OUT_OF_STOCK'],
|
|
73
|
+
})
|
|
74
|
+
export class PlaceOrderController {
|
|
75
|
+
constructor(private readonly commands: CommandBus) {}
|
|
76
|
+
|
|
77
|
+
async handle({ body, actor }: HttpRequest<{ body: Infer<typeof body> }>): Promise<Infer<typeof placed>> {
|
|
78
|
+
return { id: await this.commands.execute(new PlaceOrder(actor, body.customerId)) };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const routes = controllerRoutes([PlaceOrderController], { resolve: application.resolve });
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
| Export | What it is |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `Controller(spec)` | the decorator of a controller: `group`, `method`, `path` (after the prefix of the group), `operationId` (the class name without `Controller` by default), `summary`, `description`, `tags`, `params`, `query`, `headers`, `body`, `response: { status, description?, body? }`, `errors`, `guards`, `deprecated` |
|
|
88
|
+
| `RouteGroup({ parent, prefix, tags, guards, errors })` | names a set of routes on an empty class; a child inherits from its `parent` and adds |
|
|
89
|
+
| `Guard` | the interface `{ documents?, check(request: GuardRequest) }`, and the decorator of a guard class that takes dependencies |
|
|
90
|
+
| `Authenticated`, `Public`, `HasRole(...roles)` | the guards the package ships, over the `Actor` of the request: 401 to the anonymous caller, 403 without one of the roles; `Public` lets everybody in and stands alone |
|
|
91
|
+
| `RequestMapper<Request, Message>`, `Serializer<Result, Body>` | the interfaces of the two optional translators (`map`, `serialize`), and the decorators that declare them |
|
|
92
|
+
| `HttpRequest<{ params, query, headers, body }>` | the request as `handle`, a mapper or a guard names it, with `actor` and `context` |
|
|
93
|
+
| `controllerRoutes(controllers, { resolve, security, guards })` | the `Route` of each controller, given as a class (built by `resolve`) or as an instance; `security` names the schemes of the application (`['bearer']` by default); `guards: 'skip'` is for the spec of a controller alone |
|
|
94
|
+
| `controllerRoute(controller, options)` | the one route of one controller, for a spec |
|
|
95
|
+
|
|
96
|
+
A controller with no group and no guard is refused when its class is defined. Guards run in
|
|
97
|
+
order, after validation and before `handle`, inside the execution scope of the request, and
|
|
98
|
+
what they throw is answered as a problem like any refusal. A guard is a control at the edge;
|
|
99
|
+
a permission is declared on the use case (`@Requires`), where a job and an event handler meet
|
|
100
|
+
it too. Controllers are declared classes of the edge (`Edge` of
|
|
101
|
+
`@ddd-ts-kit/kernel/declarations`): `@ddd-ts-kit/di` binds each one under itself and lists them
|
|
102
|
+
in `DeclaredEdges`. A controller has one response and answers its body: a route with several
|
|
103
|
+
success statuses, response headers or a raw body stays a `defineRoute` function. The reasons
|
|
104
|
+
are on the [presentation layer](../../docs/concepts/presentation-layer.md) page.
|
|
105
|
+
|
|
53
106
|
## Errors
|
|
54
107
|
|
|
55
108
|
A handler throws a `DomainError`; its `code` is mapped to a status in one place and answered as
|
|
@@ -60,6 +113,27 @@ A handler throws a `DomainError`; its `code` is mapped to a status in one place
|
|
|
60
113
|
"detail": "Order 7 does not exist.", "requestId": "..." }
|
|
61
114
|
```
|
|
62
115
|
|
|
116
|
+
A refusal built by a factory of a `ConceptError` carries facts, and they are private: the
|
|
117
|
+
document holds, under `facts`, only the ones its factory exposed by name, and no `facts` member
|
|
118
|
+
when it exposed none.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
static slotTaken(calendarId: string, startsAt: Date) {
|
|
122
|
+
return AppointmentError.refusal('SLOT_TAKEN', 'The slot is already held.', { calendarId, startsAt }).exposing('startsAt');
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{ "type": "urn:problem:slot-taken", "title": "Conflict", "status": 409, "code": "SLOT_TAKEN",
|
|
128
|
+
"detail": "The slot is already held.", "facts": { "startsAt": "2026-03-02T09:00:00.000Z" }, "requestId": "..." }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`facts` is an optional object of the `Problem` schema of the OpenAPI document. A client
|
|
132
|
+
branches on `code` and reads data from `facts`, never from `detail`, which is a sentence for a
|
|
133
|
+
human and may change. The message is answered as it was written: it says nothing the caller
|
|
134
|
+
did not send, and nothing about another person. A refusal mapped to a 5xx answers neither its
|
|
135
|
+
message nor its facts.
|
|
136
|
+
|
|
63
137
|
Defaults: `VALIDATION_FAILED` 422 (with `issues`, paths prefixed by the part:
|
|
64
138
|
`body.items.0.sku`), `NOT_FOUND` 404, `CONFLICT` 409, `INVARIANT_VIOLATION` 422, `UNAUTHORIZED`
|
|
65
139
|
401, `FORBIDDEN` 403. A domain code that is not mapped answers 422.
|
|
@@ -77,6 +151,13 @@ answered 5xx goes to the kernel `ErrorReporter`, once, with the correlation, act
|
|
|
77
151
|
ids of its execution context (`context.execution`, present once the request got that far); a
|
|
78
152
|
4xx never does.
|
|
79
153
|
|
|
154
|
+
A refusal built by a factory (`ConceptError`) answers its message as `detail` unless the message
|
|
155
|
+
quotes a fact the factory did not expose: then the client reads a sentence made from the code
|
|
156
|
+
(`INVALID_EMAIL` gives `Invalid email.`) and the message stays in the logs and the error
|
|
157
|
+
reports. A fact is found in the message as a text, a number or an ISO instant; a fact written
|
|
158
|
+
another way (a formatted date, a part of an object) is not seen, so expose what a client may
|
|
159
|
+
read and keep the rest out of the sentence.
|
|
160
|
+
|
|
80
161
|
## Options shared by the adapters
|
|
81
162
|
|
|
82
163
|
```ts
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A class given as a controller. Any class is accepted by the types, so that the classes an
|
|
3
|
+
* application lists as declared controllers are handed over as they are; one that carries no
|
|
4
|
+
* `@Controller` is refused when the routes are built.
|
|
5
|
+
*/
|
|
6
|
+
export type ControllerClass = abstract new (...args: never) => unknown;
|
|
7
|
+
//# sourceMappingURL=controller-class.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-class.d.ts","sourceRoot":"","sources":["../../src/controller/controller-class.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,MAAM,GAAG,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-class.js","sourceRoot":"","sources":["../../src/controller/controller-class.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Actor } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import type { HandlerInput } from '../route/handler-input.js';
|
|
3
|
+
import type { ControllerSpec } from './controller-spec.js';
|
|
4
|
+
/**
|
|
5
|
+
* What `handle` receives for a given declaration: each part has the type its schema produces,
|
|
6
|
+
* and the actor is the one of the execution context of the request.
|
|
7
|
+
*/
|
|
8
|
+
export type ControllerRequest<Spec extends ControllerSpec = ControllerSpec> = HandlerInput<Spec> & {
|
|
9
|
+
readonly actor: Actor;
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=controller-request.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-request.d.ts","sourceRoot":"","sources":["../../src/controller/controller-request.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;GAGG;AACH,MAAM,MAAM,iBAAiB,CAAC,IAAI,SAAS,cAAc,GAAG,cAAc,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG;IACjG,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACvB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-request.js","sourceRoot":"","sources":["../../src/controller/controller-request.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Schema } from '@ddd-ts-kit/validation';
|
|
2
|
+
/** The one answer of a controller when its use case succeeds. */
|
|
3
|
+
export interface ControllerResponse {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
/** Left out, `The answer.` with a body and `Done.` without. */
|
|
6
|
+
readonly description?: string;
|
|
7
|
+
/** Left out, the controller answers no body and `handle` returns nothing. */
|
|
8
|
+
readonly body?: Schema<unknown>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=controller-response.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-response.d.ts","sourceRoot":"","sources":["../../src/controller/controller-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAErD,iEAAiE;AACjE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;CACjC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-response.js","sourceRoot":"","sources":["../../src/controller/controller-response.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Schema } from '@ddd-ts-kit/validation';
|
|
2
|
+
import type { ControllerSpec } from './controller-spec.js';
|
|
3
|
+
/** What `handle` returns for a given declaration: the body its response schema describes, or nothing. */
|
|
4
|
+
export type ControllerResult<Spec extends ControllerSpec = ControllerSpec> = Spec['response'] extends {
|
|
5
|
+
readonly body: Schema<infer Body>;
|
|
6
|
+
} ? Body : undefined;
|
|
7
|
+
//# sourceMappingURL=controller-result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-result.d.ts","sourceRoot":"","sources":["../../src/controller/controller-result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,yGAAyG;AACzG,MAAM,MAAM,gBAAgB,CAAC,IAAI,SAAS,cAAc,GAAG,cAAc,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS;IACpG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;CACnC,GACG,IAAI,GACJ,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-result.js","sourceRoot":"","sources":["../../src/controller/controller-result.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Route } from '../route/route.js';
|
|
2
|
+
import type { ControllerClass } from './controller-class.js';
|
|
3
|
+
import type { ErasedController } from './erased-controller.js';
|
|
4
|
+
import type { ControllerRoutesOptions } from './controller-routes-options.js';
|
|
5
|
+
/** The route of one controller: what a spec hands to `executeRoute`. */
|
|
6
|
+
export declare function controllerRoute(controller: ControllerClass | ErasedController, options?: ControllerRoutesOptions): Route;
|
|
7
|
+
//# sourceMappingURL=controller-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-route.d.ts","sourceRoot":"","sources":["../../src/controller/controller-route.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAG9E,wEAAwE;AACxE,wBAAgB,eAAe,CAC7B,UAAU,EAAE,eAAe,GAAG,gBAAgB,EAC9C,OAAO,GAAE,uBAA4B,GACpC,KAAK,CAQP"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { controllerRoutes } from './controller-routes.js';
|
|
2
|
+
/** The route of one controller: what a spec hands to `executeRoute`. */
|
|
3
|
+
export function controllerRoute(controller, options = {}) {
|
|
4
|
+
const [route] = controllerRoutes([controller], options);
|
|
5
|
+
if (route === undefined) {
|
|
6
|
+
throw new Error('A controller declares one route, and none was built.');
|
|
7
|
+
}
|
|
8
|
+
return route;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=controller-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-route.js","sourceRoot":"","sources":["../../src/controller/controller-route.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAC7B,UAA8C,EAC9C,OAAO,GAA4B,EAAE;IAErC,MAAM,CAAC,KAAK,CAAC,GAAG,gBAAgB,CAAC,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC,CAAC;IAExD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ControllerRoutesOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Builds what was given as a class, a controller or a guard: `resolve` of the application,
|
|
4
|
+
* which injects each constructor parameter. Left out, only instances can be given.
|
|
5
|
+
*/
|
|
6
|
+
readonly resolve?: <T>(target: abstract new (...args: never) => T) => T;
|
|
7
|
+
/**
|
|
8
|
+
* The security schemes of the application, as its contract names them: every route that is
|
|
9
|
+
* not public documents them, unless one of its guards names its own. Default: `['bearer']`.
|
|
10
|
+
*/
|
|
11
|
+
readonly security?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* `'skip'` leaves the guards out, for the spec of a controller alone: what it validates, what
|
|
14
|
+
* it hands its use case, what it answers. The guards have specs of their own, and an
|
|
15
|
+
* application never sets this.
|
|
16
|
+
*/
|
|
17
|
+
readonly guards?: 'skip';
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=controller-routes-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-routes-options.d.ts","sourceRoot":"","sources":["../../src/controller/controller-routes-options.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,MAAM,GAAG,IAAI,EAAE,KAAK,KAAK,CAAC,KAAK,CAAC,CAAC;IACxE;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-routes-options.js","sourceRoot":"","sources":["../../src/controller/controller-routes-options.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Route } from '../route/route.js';
|
|
2
|
+
import type { ControllerClass } from './controller-class.js';
|
|
3
|
+
import type { ErasedController } from './erased-controller.js';
|
|
4
|
+
import type { ControllerRoutesOptions } from './controller-routes-options.js';
|
|
5
|
+
/**
|
|
6
|
+
* The routes of controllers, for any adapter and for the contract generator: each controller
|
|
7
|
+
* becomes one `Route`, so validation, error mapping, tracing and problem documents are those
|
|
8
|
+
* of every route. Controllers are given as classes with `resolve`, or as instances.
|
|
9
|
+
*/
|
|
10
|
+
export declare function controllerRoutes(controllers: readonly (ControllerClass | ErasedController)[], options?: ControllerRoutesOptions): readonly Route[];
|
|
11
|
+
//# sourceMappingURL=controller-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-routes.d.ts","sourceRoot":"","sources":["../../src/controller/controller-routes.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAO9E;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,SAAS,CAAC,eAAe,GAAG,gBAAgB,CAAC,EAAE,EAC5D,OAAO,GAAE,uBAA4B,GACpC,SAAS,KAAK,EAAE,CAMlB"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { assertUniqueRoutes } from '../route/assert-unique-routes.js';
|
|
2
|
+
import { resolvedController } from './resolved-controller.js';
|
|
3
|
+
const DEFAULT_SECURITY = ['bearer'];
|
|
4
|
+
/**
|
|
5
|
+
* The routes of controllers, for any adapter and for the contract generator: each controller
|
|
6
|
+
* becomes one `Route`, so validation, error mapping, tracing and problem documents are those
|
|
7
|
+
* of every route. Controllers are given as classes with `resolve`, or as instances.
|
|
8
|
+
*/
|
|
9
|
+
export function controllerRoutes(controllers, options = {}) {
|
|
10
|
+
const routes = controllers.map((entry) => routeOf(resolvedController(entry, options.resolve), options));
|
|
11
|
+
assertUniqueRoutes(routes);
|
|
12
|
+
return routes;
|
|
13
|
+
}
|
|
14
|
+
function routeOf(resolved, options) {
|
|
15
|
+
const { controller, spec, declared } = resolved;
|
|
16
|
+
const guards = options.guards === 'skip' ? [] : resolved.guards;
|
|
17
|
+
const { status, body, description } = spec.response;
|
|
18
|
+
const identity = { method: spec.method, path: declared.path, operationId: declared.operationId };
|
|
19
|
+
return {
|
|
20
|
+
...identity,
|
|
21
|
+
summary: spec.summary,
|
|
22
|
+
...(spec.description === undefined ? {} : { description: spec.description }),
|
|
23
|
+
...(declared.tags.length === 0 ? {} : { tags: declared.tags }),
|
|
24
|
+
...requestOf(spec),
|
|
25
|
+
responses: { [status]: { description: description ?? defaultDescription(body), ...bodyOf(body) } },
|
|
26
|
+
errors: [...new Set([...declared.errors, ...resolved.guards.flatMap((guard) => guard.documents?.errors ?? [])])],
|
|
27
|
+
security: securityOf(resolved.guards, options.security ?? DEFAULT_SECURITY),
|
|
28
|
+
...(spec.deprecated === true ? { deprecated: true } : {}),
|
|
29
|
+
async handle(input) {
|
|
30
|
+
for (const guard of guards) {
|
|
31
|
+
await guard.check(guardRequest(identity, input));
|
|
32
|
+
}
|
|
33
|
+
const answer = await controller.handle({ ...input, actor: input.context.execution.actor });
|
|
34
|
+
return body === undefined ? { status } : { status, body: answer };
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function requestOf(parts) {
|
|
39
|
+
const declared = Object.entries({
|
|
40
|
+
params: parts.params,
|
|
41
|
+
query: parts.query,
|
|
42
|
+
headers: parts.headers,
|
|
43
|
+
body: parts.body,
|
|
44
|
+
}).filter(([, schema]) => schema !== undefined);
|
|
45
|
+
return declared.length === 0 ? {} : { request: Object.fromEntries(declared) };
|
|
46
|
+
}
|
|
47
|
+
function bodyOf(body) {
|
|
48
|
+
return body === undefined ? {} : { body };
|
|
49
|
+
}
|
|
50
|
+
function defaultDescription(body) {
|
|
51
|
+
return body === undefined ? 'Done.' : 'The answer.';
|
|
52
|
+
}
|
|
53
|
+
function securityOf(guards, schemes) {
|
|
54
|
+
if (guards.some((guard) => guard.documents?.public === true)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
const named = guards.flatMap((guard) => guard.documents?.security ?? []);
|
|
58
|
+
return [...new Set(named.length === 0 ? schemes : named)];
|
|
59
|
+
}
|
|
60
|
+
function guardRequest(identity, input) {
|
|
61
|
+
const { context } = input;
|
|
62
|
+
const { actor, session } = context.execution;
|
|
63
|
+
return {
|
|
64
|
+
...identity,
|
|
65
|
+
params: typeof input.params === 'object' && input.params !== null ? { ...input.params } : {},
|
|
66
|
+
headers: context.rawHeaders ?? {},
|
|
67
|
+
actor,
|
|
68
|
+
...(session === undefined ? {} : { session }),
|
|
69
|
+
context,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=controller-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-routes.js","sourceRoot":"","sources":["../../src/controller/controller-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Schema } from '@ddd-ts-kit/validation';
|
|
2
|
+
import type { HttpMethod } from '../route/http-method.js';
|
|
3
|
+
import type { ControllerResponse } from './controller-response.js';
|
|
4
|
+
import type { GuardReference } from './guard-reference.js';
|
|
5
|
+
import type { RouteGroupClass } from './route-group-class.js';
|
|
6
|
+
/** Everything a route says, in the decorator of its controller and nowhere else. */
|
|
7
|
+
export interface ControllerSpec {
|
|
8
|
+
/** The group the route belongs to: it gives a prefix, tags, guards and error codes. */
|
|
9
|
+
readonly group?: RouteGroupClass;
|
|
10
|
+
readonly method: HttpMethod;
|
|
11
|
+
/** Literal segments and `:name` parameters, after the prefix of the group; `/` for the prefix itself. */
|
|
12
|
+
readonly path: string;
|
|
13
|
+
/** Left out, the name of the class without `Controller`, in camelCase. */
|
|
14
|
+
readonly operationId?: string;
|
|
15
|
+
readonly summary: string;
|
|
16
|
+
readonly description?: string;
|
|
17
|
+
/** Added to the tags of the group. */
|
|
18
|
+
readonly tags?: readonly string[];
|
|
19
|
+
readonly params?: Schema<unknown>;
|
|
20
|
+
readonly query?: Schema<unknown>;
|
|
21
|
+
readonly headers?: Schema<unknown>;
|
|
22
|
+
readonly body?: Schema<unknown>;
|
|
23
|
+
readonly response: ControllerResponse;
|
|
24
|
+
/** Domain error codes the route may answer, added to those of its group and of its guards. */
|
|
25
|
+
readonly errors?: readonly string[];
|
|
26
|
+
/** Run after the guards of the group. With no group, at least one is required: `[Public]` says the route is open. */
|
|
27
|
+
readonly guards?: readonly GuardReference[];
|
|
28
|
+
readonly deprecated?: boolean;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=controller-spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-spec.d.ts","sourceRoot":"","sources":["../../src/controller/controller-spec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,oFAAoF;AACpF,MAAM,WAAW,cAAc;IAC7B,uFAAuF;IACvF,QAAQ,CAAC,KAAK,CAAC,EAAE,eAAe,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,yGAAyG;IACzG,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,sCAAsC;IACtC,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAClC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACjC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IACnC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,qHAAqH;IACrH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC5C,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller-spec.js","sourceRoot":"","sources":["../../src/controller/controller-spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ClassDeclarator } from '@ddd-ts-kit/kernel/declarations';
|
|
2
|
+
import type { ControllerSpec } from './controller-spec.js';
|
|
3
|
+
import type { HttpController } from './http-controller.js';
|
|
4
|
+
/**
|
|
5
|
+
* Declares one route on the class that answers it:
|
|
6
|
+
*
|
|
7
|
+
* @Controller({ group: AppointmentRoutes, method: 'POST', path: '/', summary: 'Book appointment',
|
|
8
|
+
* body, response: { status: 201, body: accepted }, errors: ['SLOT_TAKEN'] })
|
|
9
|
+
* export class BookAppointmentController {
|
|
10
|
+
* constructor(private readonly commands: CommandBus) {}
|
|
11
|
+
* async handle({ body, actor }: HttpRequest<{ body: BookAppointmentBody }>): Promise<BookAppointmentAccepted> { ... }
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* The declaration is the single statement of the route: the server validates with its schemas
|
|
15
|
+
* and the contract is generated from it. The compiler holds `handle` to it: a part the
|
|
16
|
+
* schemas do not produce, or another body than the declared response, is refused on the
|
|
17
|
+
* decorator. The constructor states the dependencies, as for every declared class.
|
|
18
|
+
*/
|
|
19
|
+
export declare function Controller<const Spec extends ControllerSpec>(spec: Spec): ClassDeclarator<HttpController<Spec>, undefined>;
|
|
20
|
+
//# sourceMappingURL=controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/controller/controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAC/F,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAG3D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,KAAK,CAAC,IAAI,SAAS,cAAc,EAC1D,IAAI,EAAE,IAAI,GACT,eAAe,CAAC,cAAc,CAAC,IAAI,CAAC,EAAE,SAAS,CAAC,CAclD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { DeclarationError, Edge } from '@ddd-ts-kit/kernel/declarations';
|
|
2
|
+
import { declaredRoute } from './declared-route.js';
|
|
3
|
+
import { presentationRegistry } from './presentation-registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* Declares one route on the class that answers it:
|
|
6
|
+
*
|
|
7
|
+
* @Controller({ group: AppointmentRoutes, method: 'POST', path: '/', summary: 'Book appointment',
|
|
8
|
+
* body, response: { status: 201, body: accepted }, errors: ['SLOT_TAKEN'] })
|
|
9
|
+
* export class BookAppointmentController {
|
|
10
|
+
* constructor(private readonly commands: CommandBus) {}
|
|
11
|
+
* async handle({ body, actor }: HttpRequest<{ body: BookAppointmentBody }>): Promise<BookAppointmentAccepted> { ... }
|
|
12
|
+
* }
|
|
13
|
+
*
|
|
14
|
+
* The declaration is the single statement of the route: the server validates with its schemas
|
|
15
|
+
* and the contract is generated from it. The compiler holds `handle` to it: a part the
|
|
16
|
+
* schemas do not produce, or another body than the declared response, is refused on the
|
|
17
|
+
* decorator. The constructor states the dependencies, as for every declared class.
|
|
18
|
+
*/
|
|
19
|
+
export function Controller(spec) {
|
|
20
|
+
const declare = Edge('controller');
|
|
21
|
+
return (target, context) => {
|
|
22
|
+
if (!target.name.endsWith('Controller')) {
|
|
23
|
+
throw new DeclarationError(`${target.name} carries @Controller: name the class after its use case, ending in Controller.`);
|
|
24
|
+
}
|
|
25
|
+
declaredRoute(target.name, spec);
|
|
26
|
+
declare(target, context);
|
|
27
|
+
presentationRegistry().controllers.set(target, spec);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controller.js","sourceRoot":"","sources":["../../src/controller/controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,IAAI,EAAwB,MAAM,iCAAiC,CAAC;AAE/F,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAElE;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,UAAU,CACxB,IAAU;IAEV,MAAM,OAAO,GAAG,IAAI,CAAuB,YAAY,CAAC,CAAC;IAEzD,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;QACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,gBAAgB,CACxB,GAAG,MAAM,CAAC,IAAI,gFAAgF,CAC/F,CAAC;QACJ,CAAC;QAED,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACjC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACzB,oBAAoB,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ControllerSpec } from './controller-spec.js';
|
|
2
|
+
import type { GuardReference } from './guard-reference.js';
|
|
3
|
+
/** What a controller says once the groups above it are folded in. */
|
|
4
|
+
export interface DeclaredRoute {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly operationId: string;
|
|
7
|
+
readonly tags: readonly string[];
|
|
8
|
+
readonly guards: readonly GuardReference[];
|
|
9
|
+
readonly errors: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/** Folds the groups of a controller into its declaration, and refuses one that cannot stand. */
|
|
12
|
+
export declare function declaredRoute(className: string, spec: ControllerSpec): DeclaredRoute;
|
|
13
|
+
//# sourceMappingURL=declared-route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declared-route.d.ts","sourceRoot":"","sources":["../../src/controller/declared-route.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,qEAAqE;AACrE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC;IAC3C,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAWD,gGAAgG;AAChG,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,aAAa,CAepF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { DeclarationError } from '@ddd-ts-kit/kernel/declarations';
|
|
2
|
+
import { ROUTE_PATH_PATTERN } from '../route/route-path-pattern.js';
|
|
3
|
+
import { Public } from './guards/public.js';
|
|
4
|
+
import { presentationRegistry } from './presentation-registry.js';
|
|
5
|
+
const NOTHING = { prefix: '', tags: [], guards: [], errors: [] };
|
|
6
|
+
/** Folds the groups of a controller into its declaration, and refuses one that cannot stand. */
|
|
7
|
+
export function declaredRoute(className, spec) {
|
|
8
|
+
const inherited = spec.group === undefined ? NOTHING : inheritedFrom(className, spec.group);
|
|
9
|
+
const guards = [...inherited.guards, ...(spec.guards ?? [])];
|
|
10
|
+
const path = `${inherited.prefix}${spec.path === '/' && inherited.prefix !== '' ? '' : spec.path}`;
|
|
11
|
+
assertPath(className, spec.path, path);
|
|
12
|
+
assertAccess(className, guards);
|
|
13
|
+
return {
|
|
14
|
+
path,
|
|
15
|
+
operationId: spec.operationId ?? operationIdOf(className),
|
|
16
|
+
tags: distinct([...inherited.tags, ...(spec.tags ?? [])]),
|
|
17
|
+
guards,
|
|
18
|
+
errors: distinct([...inherited.errors, ...(spec.errors ?? [])]),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function inheritedFrom(className, group) {
|
|
22
|
+
const options = presentationRegistry().groups.get(group);
|
|
23
|
+
if (options === undefined) {
|
|
24
|
+
throw new DeclarationError(`Controller ${className}: its group ${group.name} carries no @RouteGroup.`);
|
|
25
|
+
}
|
|
26
|
+
const above = options.parent === undefined ? NOTHING : inheritedFrom(className, options.parent);
|
|
27
|
+
return {
|
|
28
|
+
prefix: `${above.prefix}${options.prefix ?? ''}`,
|
|
29
|
+
tags: [...above.tags, ...(options.tags ?? [])],
|
|
30
|
+
guards: [...above.guards, ...(options.guards ?? [])],
|
|
31
|
+
errors: [...above.errors, ...(options.errors ?? [])],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function assertPath(className, written, path) {
|
|
35
|
+
if (!ROUTE_PATH_PATTERN.test(written) || !ROUTE_PATH_PATTERN.test(path)) {
|
|
36
|
+
throw new DeclarationError(`Controller ${className}: the path "${written}" must be made of literal segments and ":name" parameters.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function assertAccess(className, guards) {
|
|
40
|
+
if (guards.length === 0) {
|
|
41
|
+
throw new DeclarationError(`Controller ${className} names no group with guards and no guard of its own: nothing says who may call it. Name its group, or declare \`guards: [Public]\` when anybody may.`);
|
|
42
|
+
}
|
|
43
|
+
if (guards.length > 1 && guards.includes(Public)) {
|
|
44
|
+
throw new DeclarationError(`Controller ${className} is behind Public and another guard: a route is open or it is guarded. Move it to a group of its kind.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function operationIdOf(className) {
|
|
48
|
+
const name = className.replace(/Controller$/, '');
|
|
49
|
+
return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
|
|
50
|
+
}
|
|
51
|
+
function distinct(values) {
|
|
52
|
+
return [...new Set(values)];
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=declared-route.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"declared-route.js","sourceRoot":"","sources":["../../src/controller/declared-route.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Actor } from '@ddd-ts-kit/kernel';
|
|
2
|
+
import type { RouteInput } from '../route/route-input.js';
|
|
3
|
+
/** A controller once its schema types are erased, as the pipeline calls it. */
|
|
4
|
+
export interface ErasedController {
|
|
5
|
+
handle(request: RouteInput & {
|
|
6
|
+
readonly actor: Actor;
|
|
7
|
+
}): unknown;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=erased-controller.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erased-controller.d.ts","sourceRoot":"","sources":["../../src/controller/erased-controller.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE1D,+EAA+E;AAC/E,MAAM,WAAW,gBAAgB;IAE/B,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG;QAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;KAAE,GAAG,OAAO,CAAC;CAClE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"erased-controller.js","sourceRoot":"","sources":["../../src/controller/erased-controller.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** What a guard says in the contract of every route behind it, so no route repeats it. */
|
|
2
|
+
export interface GuardDocumentation {
|
|
3
|
+
/** `true` on the guard that lets everybody in: the route is documented with no security requirement. */
|
|
4
|
+
readonly public?: boolean;
|
|
5
|
+
/** The security schemes the guard authenticates with, when they are not the ones of the application. */
|
|
6
|
+
readonly security?: readonly string[];
|
|
7
|
+
/** The error codes the guard may answer: they join the declared codes of the route. */
|
|
8
|
+
readonly errors?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=guard-documentation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-documentation.d.ts","sourceRoot":"","sources":["../../src/controller/guard-documentation.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,MAAM,WAAW,kBAAkB;IACjC,wGAAwG;IACxG,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,wGAAwG;IACxG,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,uFAAuF;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-documentation.js","sourceRoot":"","sources":["../../src/controller/guard-documentation.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Guard } from './guard.js';
|
|
2
|
+
/** A guard as a group or a controller names it: the value itself, or the declared class the composition root builds. */
|
|
3
|
+
export type GuardReference = Guard | (abstract new (...args: never) => Guard);
|
|
4
|
+
//# sourceMappingURL=guard-reference.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-reference.d.ts","sourceRoot":"","sources":["../../src/controller/guard-reference.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAExC,wHAAwH;AACxH,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,CAAC,QAAQ,MAAM,GAAG,IAAI,EAAE,KAAK,KAAK,KAAK,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-reference.js","sourceRoot":"","sources":["../../src/controller/guard-reference.ts"],"names":[],"mappings":""}
|