@getsoda/core 0.2.1 → 0.3.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 +103 -0
- package/dist/app/app.d.ts +15 -5
- package/dist/app/app.d.ts.map +1 -1
- package/dist/app/app.js +31 -8
- package/dist/app/app.js.map +1 -1
- package/dist/app/index.d.ts +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/config/config.d.ts +57 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +74 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/index.d.ts +9 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/module.d.ts +29 -0
- package/dist/config/module.d.ts.map +1 -0
- package/dist/config/module.js +28 -0
- package/dist/config/module.js.map +1 -0
- package/dist/di/errors.d.ts.map +1 -1
- package/dist/di/errors.js +2 -0
- package/dist/di/errors.js.map +1 -1
- package/dist/http/arguments.d.ts +19 -6
- package/dist/http/arguments.d.ts.map +1 -1
- package/dist/http/arguments.js +7 -0
- package/dist/http/arguments.js.map +1 -1
- package/dist/http/cookies.d.ts.map +1 -1
- package/dist/http/cookies.js +2 -0
- package/dist/http/cookies.js.map +1 -1
- package/dist/http/decorators.d.ts +18 -6
- package/dist/http/decorators.d.ts.map +1 -1
- package/dist/http/decorators.js +7 -2
- package/dist/http/decorators.js.map +1 -1
- package/dist/http/errors.d.ts +3 -1
- package/dist/http/errors.d.ts.map +1 -1
- package/dist/http/errors.js +2 -0
- package/dist/http/errors.js.map +1 -1
- package/dist/http/index.d.ts +7 -5
- package/dist/http/index.d.ts.map +1 -1
- package/dist/http/index.js +3 -2
- package/dist/http/index.js.map +1 -1
- package/dist/http/parsers.d.ts +4 -0
- package/dist/http/parsers.d.ts.map +1 -1
- package/dist/http/parsers.js +9 -5
- package/dist/http/parsers.js.map +1 -1
- package/dist/http/pipeline.d.ts +4 -5
- package/dist/http/pipeline.d.ts.map +1 -1
- package/dist/http/pipeline.js +2 -2
- package/dist/http/pipeline.js.map +1 -1
- package/dist/http/routes.d.ts +27 -2
- package/dist/http/routes.d.ts.map +1 -1
- package/dist/http/routes.js +34 -20
- package/dist/http/routes.js.map +1 -1
- package/dist/http/schema.d.ts +2 -0
- package/dist/http/schema.d.ts.map +1 -1
- package/dist/http/schema.js +2 -1
- package/dist/http/schema.js.map +1 -1
- package/dist/http/sse.d.ts +39 -0
- package/dist/http/sse.d.ts.map +1 -0
- package/dist/http/sse.js +89 -0
- package/dist/http/sse.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/openapi/decorators.d.ts +41 -0
- package/dist/openapi/decorators.d.ts.map +1 -0
- package/dist/openapi/decorators.js +29 -0
- package/dist/openapi/decorators.js.map +1 -0
- package/dist/openapi/document.d.ts +62 -0
- package/dist/openapi/document.d.ts.map +1 -0
- package/dist/openapi/document.js +135 -0
- package/dist/openapi/document.js.map +1 -0
- package/dist/openapi/index.d.ts +11 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +8 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/module.d.ts +26 -0
- package/dist/openapi/module.d.ts.map +1 -0
- package/dist/openapi/module.js +73 -0
- package/dist/openapi/module.js.map +1 -0
- package/dist/routing/errors.d.ts.map +1 -1
- package/dist/routing/errors.js +2 -0
- package/dist/routing/errors.js.map +1 -1
- package/dist/testing/index.d.ts +46 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +49 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/utils/errors.d.ts +11 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +22 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/object.d.ts +4 -0
- package/dist/utils/object.d.ts.map +1 -1
- package/dist/utils/object.js +16 -0
- package/dist/utils/object.js.map +1 -1
- package/dist/websocket/decorators.d.ts +31 -0
- package/dist/websocket/decorators.d.ts.map +1 -0
- package/dist/websocket/decorators.js +43 -0
- package/dist/websocket/decorators.js.map +1 -0
- package/dist/websocket/gateway.d.ts +61 -0
- package/dist/websocket/gateway.d.ts.map +1 -0
- package/dist/websocket/gateway.js +142 -0
- package/dist/websocket/gateway.js.map +1 -0
- package/dist/websocket/index.d.ts +7 -0
- package/dist/websocket/index.d.ts.map +1 -0
- package/dist/websocket/index.js +6 -0
- package/dist/websocket/index.js.map +1 -0
- package/package.json +12 -1
- package/src/app/app.ts +69 -18
- package/src/app/index.ts +1 -1
- package/src/config/config.ts +90 -0
- package/src/config/index.ts +9 -0
- package/src/config/module.ts +40 -0
- package/src/di/errors.ts +3 -0
- package/src/http/arguments.ts +31 -6
- package/src/http/cookies.ts +2 -0
- package/src/http/decorators.ts +25 -10
- package/src/http/errors.ts +7 -0
- package/src/http/index.ts +7 -4
- package/src/http/parsers.ts +18 -7
- package/src/http/pipeline.ts +6 -6
- package/src/http/routes.ts +68 -29
- package/src/http/schema.ts +2 -1
- package/src/http/sse.ts +120 -0
- package/src/index.ts +2 -0
- package/src/openapi/decorators.ts +50 -0
- package/src/openapi/document.ts +221 -0
- package/src/openapi/index.ts +11 -0
- package/src/openapi/module.ts +95 -0
- package/src/routing/errors.ts +3 -0
- package/src/testing/index.ts +73 -0
- package/src/utils/errors.ts +25 -0
- package/src/utils/object.ts +23 -0
- package/src/websocket/decorators.ts +55 -0
- package/src/websocket/gateway.ts +213 -0
- package/src/websocket/index.ts +7 -0
package/src/http/arguments.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import type { FormFields, FormLimits } from './body.ts'
|
|
20
20
|
import type { ContextKey } from './context-key.ts'
|
|
21
|
-
import type { Context, ParamNameOf } from './context.ts'
|
|
21
|
+
import type { Context, ContextParams, ParamNameOf } from './context.ts'
|
|
22
22
|
import type { Parsed, ValueParser } from './parsers.ts'
|
|
23
23
|
import { isStandardSchema } from './schema.ts'
|
|
24
24
|
import type { InferOutput, StandardSchemaV1 } from './schema.ts'
|
|
@@ -33,6 +33,10 @@ export interface Argument<T, K extends ArgumentKind = ArgumentKind, N extends st
|
|
|
33
33
|
/** Parameter, query, header or cookie name; the key description for `value`; empty for `body` and `context`. */
|
|
34
34
|
readonly name: N
|
|
35
35
|
readonly read: (ctx: Context) => T | Promise<T>
|
|
36
|
+
/** The parser or schema the value goes through, for API descriptions such as OpenAPI. */
|
|
37
|
+
readonly source?: ValueParser<unknown> | StandardSchemaV1
|
|
38
|
+
/** Set by `form()`: the body is a form, not JSON. */
|
|
39
|
+
readonly multipart?: true
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
/** Arguments accepted by a route with path `P`: `param()` names must exist in it. */
|
|
@@ -40,13 +44,27 @@ export type ArgumentFor<P extends string> =
|
|
|
40
44
|
| Argument<unknown, 'param', ParamNameOf<P>>
|
|
41
45
|
| Argument<unknown, Exclude<ArgumentKind, 'param'>>
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
declare const RAW_PARAM: unique symbol
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Value of `param(name)` without parser, typed by the route path:
|
|
51
|
+
* `string` for `:id`, `string | undefined` for `:id?`.
|
|
52
|
+
*/
|
|
53
|
+
export interface RawParam {
|
|
54
|
+
readonly [RAW_PARAM]: true
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Values a method receives for an argument list on a route with path `P`. */
|
|
58
|
+
export type ArgumentValues<A extends readonly Argument<unknown>[], P extends string = string> = {
|
|
59
|
+
-readonly [K in keyof A]: A[K] extends Argument<infer T, ArgumentKind, infer N>
|
|
60
|
+
? [T] extends [RawParam]
|
|
61
|
+
? ContextParams<P>[N]
|
|
62
|
+
: T
|
|
63
|
+
: never
|
|
46
64
|
}
|
|
47
65
|
|
|
48
|
-
/** Path parameter
|
|
49
|
-
export function param<const N extends string>(name: N): Argument<
|
|
66
|
+
/** Path parameter, typed by the route path, or by `parser`. Like `@Param('id', ParseIntPipe)`. */
|
|
67
|
+
export function param<const N extends string>(name: N): Argument<RawParam, 'param', N>
|
|
50
68
|
export function param<const N extends string, V extends ValueParser<unknown>>(
|
|
51
69
|
name: N,
|
|
52
70
|
parser: V
|
|
@@ -55,6 +73,7 @@ export function param(name: string, parser?: ValueParser<unknown>): Argument<unk
|
|
|
55
73
|
return {
|
|
56
74
|
kind: 'param',
|
|
57
75
|
name,
|
|
76
|
+
source: parser,
|
|
58
77
|
read: parser === undefined ? (ctx) => ctx.param(name) : (ctx) => ctx.param(name, parser),
|
|
59
78
|
}
|
|
60
79
|
}
|
|
@@ -66,6 +85,7 @@ export function query(name: string, parser?: ValueParser<unknown>): Argument<unk
|
|
|
66
85
|
return {
|
|
67
86
|
kind: 'query',
|
|
68
87
|
name,
|
|
88
|
+
source: parser,
|
|
69
89
|
read: parser === undefined ? (ctx) => ctx.query(name) : (ctx) => ctx.query(name, parser),
|
|
70
90
|
}
|
|
71
91
|
}
|
|
@@ -77,6 +97,7 @@ export function header(name: string, parser?: ValueParser<unknown>): Argument<un
|
|
|
77
97
|
return {
|
|
78
98
|
kind: 'header',
|
|
79
99
|
name,
|
|
100
|
+
source: parser,
|
|
80
101
|
read: parser === undefined ? (ctx) => ctx.header(name) : (ctx) => ctx.header(name, parser),
|
|
81
102
|
}
|
|
82
103
|
}
|
|
@@ -88,6 +109,7 @@ export function cookie(name: string, parser?: ValueParser<unknown>): Argument<un
|
|
|
88
109
|
return {
|
|
89
110
|
kind: 'cookie',
|
|
90
111
|
name,
|
|
112
|
+
source: parser,
|
|
91
113
|
read: parser === undefined ? (ctx) => ctx.cookie(name) : (ctx) => ctx.cookie(name, parser),
|
|
92
114
|
}
|
|
93
115
|
}
|
|
@@ -99,6 +121,7 @@ export function body(schema?: StandardSchemaV1): Argument<unknown, 'body'> {
|
|
|
99
121
|
return {
|
|
100
122
|
kind: 'body',
|
|
101
123
|
name: '',
|
|
124
|
+
source: schema,
|
|
102
125
|
read: schema === undefined ? (ctx) => ctx.body() : (ctx) => ctx.body(schema),
|
|
103
126
|
}
|
|
104
127
|
}
|
|
@@ -128,6 +151,8 @@ export function form(schemaOrLimits?: StandardSchemaV1 | FormLimits, limits?: Fo
|
|
|
128
151
|
return {
|
|
129
152
|
kind: 'body',
|
|
130
153
|
name: '',
|
|
154
|
+
source: isStandardSchema(schemaOrLimits) ? schemaOrLimits : undefined,
|
|
155
|
+
multipart: true,
|
|
131
156
|
read: isStandardSchema(schemaOrLimits) ? (ctx) => ctx.form(schemaOrLimits, limits) : (ctx) => ctx.form(schemaOrLimits),
|
|
132
157
|
}
|
|
133
158
|
}
|
package/src/http/cookies.ts
CHANGED
|
@@ -84,6 +84,8 @@ type Result<O extends ParserOptions> = O extends { readonly optional: true } ? s
|
|
|
84
84
|
* forged cookie is no better than none.
|
|
85
85
|
*/
|
|
86
86
|
export const signed = <const O extends ParserOptions = {}>(signer: CookieSigner, options?: O): Parser<Result<O>> => ({
|
|
87
|
+
jsonSchema: { type: 'string' },
|
|
88
|
+
optional: options?.optional === true,
|
|
87
89
|
parse: (value, name) => {
|
|
88
90
|
const unsigned = value === undefined ? undefined : signer.unsign(value)
|
|
89
91
|
|
package/src/http/decorators.ts
CHANGED
|
@@ -41,6 +41,8 @@ export interface RouteMetadata {
|
|
|
41
41
|
readonly path: string
|
|
42
42
|
/** Declared arguments; `undefined` when the method receives the context. */
|
|
43
43
|
readonly args: readonly Argument<unknown>[] | undefined
|
|
44
|
+
/** Declared with `@Sse()`: the result is streamed as server-sent events. */
|
|
45
|
+
readonly sse: boolean
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
@@ -65,18 +67,25 @@ export type RouteHandlerMethod<This, P extends string> =
|
|
|
65
67
|
/**
|
|
66
68
|
* Decorator of a route without argument list: `@Get(':id')`.
|
|
67
69
|
*/
|
|
68
|
-
export type RouteDecorator<P extends string> = <This, M extends
|
|
69
|
-
method: M,
|
|
70
|
+
export type RouteDecorator<P extends string> = <This, M extends (this: This, ...args: any[]) => unknown>(
|
|
71
|
+
method: M extends RouteHandlerMethod<This, P> ? M : RouteHandlerHint<P>,
|
|
70
72
|
context: ClassMethodDecoratorContext<This, M>
|
|
71
73
|
) => void
|
|
72
74
|
|
|
75
|
+
/**
|
|
76
|
+
* What the compiler shows for a method that does not fit a route without
|
|
77
|
+
* argument list, such as `findOne(id: string)` under `@Get(':id')`.
|
|
78
|
+
*/
|
|
79
|
+
export type RouteHandlerHint<P extends string> =
|
|
80
|
+
`Without an argument list, the method receives the context: (ctx: Context<'${P}'>). To receive values, list them: @Get('${P}', [param('name')]).`
|
|
81
|
+
|
|
73
82
|
/**
|
|
74
83
|
* Decorator of a route with an argument list: `@Get(':id', [param('id', int())])`.
|
|
75
84
|
* The method must accept the values of the arguments, in order.
|
|
76
85
|
*/
|
|
77
|
-
export type ArgumentsRouteDecorator<A extends readonly Argument<unknown>[]> = <
|
|
86
|
+
export type ArgumentsRouteDecorator<A extends readonly Argument<unknown>[], P extends string = string> = <
|
|
78
87
|
This,
|
|
79
|
-
M extends (this: This, ...args: ArgumentValues<A>) => unknown,
|
|
88
|
+
M extends (this: This, ...args: ArgumentValues<A, P>) => unknown,
|
|
80
89
|
>(
|
|
81
90
|
method: M,
|
|
82
91
|
context: ClassMethodDecoratorContext<This, M>
|
|
@@ -94,8 +103,8 @@ export type ArgumentsRouteDecorator<A extends readonly Argument<unknown>[]> = <
|
|
|
94
103
|
*/
|
|
95
104
|
export interface RouteDecoratorFactory {
|
|
96
105
|
<const P extends string = '/'>(path?: P): RouteDecorator<P>
|
|
97
|
-
<const P extends string, const A extends readonly ArgumentFor<P>[]>(path: P, args: A): ArgumentsRouteDecorator<A>
|
|
98
|
-
<const A extends readonly ArgumentFor<'/'>[]>(args: A): ArgumentsRouteDecorator<A>
|
|
106
|
+
<const P extends string, const A extends readonly ArgumentFor<P>[]>(path: P, args: A): ArgumentsRouteDecorator<A, P>
|
|
107
|
+
<const A extends readonly ArgumentFor<'/'>[]>(args: A): ArgumentsRouteDecorator<A, '/'>
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
/**
|
|
@@ -163,7 +172,7 @@ export const Controller =
|
|
|
163
172
|
/**
|
|
164
173
|
* Creates the route decorator factory of one HTTP method (any method name, extensions included).
|
|
165
174
|
*/
|
|
166
|
-
const routeFactory = (method: string, decorator: string): RouteDecoratorFactory =>
|
|
175
|
+
const routeFactory = (method: string, decorator: string, sse = false): RouteDecoratorFactory =>
|
|
167
176
|
((pathOrArgs?: string | readonly Argument<unknown>[], args?: readonly Argument<unknown>[]) =>
|
|
168
177
|
(target: object, context: MethodContext) => {
|
|
169
178
|
assertPublicMethod(decorator, context)
|
|
@@ -172,7 +181,7 @@ const routeFactory = (method: string, decorator: string): RouteDecoratorFactory
|
|
|
172
181
|
const list = typeof pathOrArgs === 'string' ? args : pathOrArgs
|
|
173
182
|
|
|
174
183
|
// Decorators run bottom-up: unshift keeps the source order.
|
|
175
|
-
metadataOf(target).routes.unshift({ method, path, args: list })
|
|
184
|
+
metadataOf(target).routes.unshift({ method, path, args: list, sse })
|
|
176
185
|
}) as RouteDecoratorFactory
|
|
177
186
|
|
|
178
187
|
/**
|
|
@@ -184,8 +193,8 @@ export interface MethodRouteDecoratorFactory {
|
|
|
184
193
|
method: string,
|
|
185
194
|
path: P,
|
|
186
195
|
args: A
|
|
187
|
-
): ArgumentsRouteDecorator<A>
|
|
188
|
-
<const A extends readonly ArgumentFor<'/'>[]>(method: string, args: A): ArgumentsRouteDecorator<A>
|
|
196
|
+
): ArgumentsRouteDecorator<A, P>
|
|
197
|
+
<const A extends readonly ArgumentFor<'/'>[]>(method: string, args: A): ArgumentsRouteDecorator<A, '/'>
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
/**
|
|
@@ -215,6 +224,12 @@ export const Head = routeFactory('HEAD', 'Head')
|
|
|
215
224
|
/** Route for every method; a route for a concrete method on the same path wins. */
|
|
216
225
|
export const All = routeFactory('ALL', 'All')
|
|
217
226
|
|
|
227
|
+
/**
|
|
228
|
+
* `GET` route streaming server-sent events: the method returns an async
|
|
229
|
+
* iterable of `SseMessage`, such as an `async function*`. Like Nest's `@Sse()`.
|
|
230
|
+
*/
|
|
231
|
+
export const Sse = routeFactory('GET', 'Sse', true)
|
|
232
|
+
|
|
218
233
|
/**
|
|
219
234
|
* Status of the responses built from the returned value (not of a returned `Response`).
|
|
220
235
|
* Defaults to `201` for `POST` and `200` otherwise, as in Nest.
|
package/src/http/errors.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Errors raised while declaring and collecting controllers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { hideFrameworkFrames } from '../utils/errors.ts'
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* Reason of a `ControllerError`.
|
|
8
10
|
*
|
|
@@ -11,6 +13,8 @@
|
|
|
11
13
|
* - `INVALID_STATUS`: `@HttpCode()` with a status a `Response` cannot have, or twice on one method.
|
|
12
14
|
* - `INVALID_HEADER`: `@Header()` with a name or value `Headers` rejects.
|
|
13
15
|
* - `UNKNOWN_PARAM`: `param('x')` in an argument list names no parameter of the route path.
|
|
16
|
+
* - `INVALID_PATH`: a WebSocket gateway path that is not static.
|
|
17
|
+
* - `DUPLICATE_ROUTE`: a gateway on the path of another gateway or route.
|
|
14
18
|
*/
|
|
15
19
|
export type ControllerErrorCode =
|
|
16
20
|
| 'NOT_A_CONTROLLER'
|
|
@@ -18,6 +22,8 @@ export type ControllerErrorCode =
|
|
|
18
22
|
| 'INVALID_STATUS'
|
|
19
23
|
| 'INVALID_HEADER'
|
|
20
24
|
| 'UNKNOWN_PARAM'
|
|
25
|
+
| 'INVALID_PATH'
|
|
26
|
+
| 'DUPLICATE_ROUTE'
|
|
21
27
|
|
|
22
28
|
/**
|
|
23
29
|
* Error thrown by the controller decorators (when the class is defined) and by
|
|
@@ -31,5 +37,6 @@ export class ControllerError extends Error {
|
|
|
31
37
|
message: string
|
|
32
38
|
) {
|
|
33
39
|
super(message)
|
|
40
|
+
hideFrameworkFrames(this)
|
|
34
41
|
}
|
|
35
42
|
}
|
package/src/http/index.ts
CHANGED
|
@@ -4,18 +4,21 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export { body, context, cookie, form, header, param, query, value } from './arguments.ts'
|
|
7
|
-
export type { Argument, ArgumentFor, ArgumentKind, ArgumentValues } from './arguments.ts'
|
|
7
|
+
export type { Argument, ArgumentFor, ArgumentKind, ArgumentValues, RawParam } from './arguments.ts'
|
|
8
8
|
export { Context } from './context.ts'
|
|
9
9
|
export { CookieSigner, signed } from './cookies.ts'
|
|
10
10
|
export { ContextKey } from './context-key.ts'
|
|
11
11
|
export type { ContextParams, ParamNameOf } from './context.ts'
|
|
12
|
-
export { All, Controller, Delete, Get, Head, Header, HttpCode, Options, Patch, Post, Put, Route } from './decorators.ts'
|
|
12
|
+
export { All, Controller, Delete, Get, Head, Header, HttpCode, Options, Patch, Post, Put, Route, Sse } from './decorators.ts'
|
|
13
|
+
export { formatSseMessage } from './sse.ts'
|
|
14
|
+
export type { SseMessage } from './sse.ts'
|
|
13
15
|
export type {
|
|
14
16
|
ArgumentsRouteDecorator,
|
|
15
17
|
MethodMetadataDecorator,
|
|
16
18
|
MethodRouteDecoratorFactory,
|
|
17
19
|
RouteDecorator,
|
|
18
20
|
RouteDecoratorFactory,
|
|
21
|
+
RouteHandlerHint,
|
|
19
22
|
RouteHandlerMethod,
|
|
20
23
|
} from './decorators.ts'
|
|
21
24
|
export { ControllerError } from './errors.ts'
|
|
@@ -37,8 +40,8 @@ export {
|
|
|
37
40
|
export type { HttpExceptionBody, HttpExceptionOptions } from './exceptions.ts'
|
|
38
41
|
export { boolean, int, number, uuid } from './parsers.ts'
|
|
39
42
|
export type { Parsed, Parser, ParserOptions, ValueParser } from './parsers.ts'
|
|
40
|
-
export { buildRoutes, controllerRoutes } from './routes.ts'
|
|
41
|
-
export type { BuiltRoutes, ControllerRoutesOptions, MappedController } from './routes.ts'
|
|
43
|
+
export { buildRoutes, controllerRoutes, ROUTES } from './routes.ts'
|
|
44
|
+
export type { BuiltRoutes, ControllerRoutesOptions, MappedController, MappedRoute, RouteRegistry } from './routes.ts'
|
|
42
45
|
export type { InferOutput, StandardSchemaV1 } from './schema.ts'
|
|
43
46
|
export type { AnyParams, PathParams } from './types.ts'
|
|
44
47
|
export { Catch, createDecorator, UseFilters, UseGuards, UseInterceptors } from './enhancers.ts'
|
package/src/http/parsers.ts
CHANGED
|
@@ -20,6 +20,10 @@ import type { InferOutput, StandardSchemaV1 } from './schema.ts'
|
|
|
20
20
|
*/
|
|
21
21
|
export interface Parser<T> {
|
|
22
22
|
readonly parse: (value: string | undefined, name: string) => T
|
|
23
|
+
/** JSON Schema of the accepted strings, for API descriptions such as OpenAPI. */
|
|
24
|
+
readonly jsonSchema?: Readonly<Record<string, unknown>>
|
|
25
|
+
/** `true` when a missing value is accepted. */
|
|
26
|
+
readonly optional?: boolean
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
/** What `param()`, `query()` and `header()` accept to convert a value. */
|
|
@@ -46,8 +50,11 @@ export const failure = (name: string, expected: string): BadRequestException =>
|
|
|
46
50
|
const createParser = <T>(
|
|
47
51
|
expected: string,
|
|
48
52
|
convert: (value: string) => T | undefined,
|
|
49
|
-
options: ParserOptions
|
|
53
|
+
options: ParserOptions,
|
|
54
|
+
jsonSchema: Readonly<Record<string, unknown>>
|
|
50
55
|
): Parser<T | undefined> => ({
|
|
56
|
+
jsonSchema,
|
|
57
|
+
optional: options.optional === true,
|
|
51
58
|
parse: (value, name) => {
|
|
52
59
|
if (value === undefined) {
|
|
53
60
|
if (options.optional === true) {
|
|
@@ -79,7 +86,8 @@ export const int = <const O extends ParserOptions = {}>(options?: O): Parser<Res
|
|
|
79
86
|
|
|
80
87
|
return Number.isSafeInteger(number) ? number : undefined
|
|
81
88
|
},
|
|
82
|
-
options ?? {}
|
|
89
|
+
options ?? {},
|
|
90
|
+
{ type: 'integer' }
|
|
83
91
|
) as Parser<Result<number, O>>
|
|
84
92
|
|
|
85
93
|
/** Finite decimal number: `'1.5'`, `'-2'`, `'1e3'`. */
|
|
@@ -91,18 +99,21 @@ export const number = <const O extends ParserOptions = {}>(options?: O): Parser<
|
|
|
91
99
|
|
|
92
100
|
return Number.isFinite(parsed) ? parsed : undefined
|
|
93
101
|
},
|
|
94
|
-
options ?? {}
|
|
102
|
+
options ?? {},
|
|
103
|
+
{ type: 'number' }
|
|
95
104
|
) as Parser<Result<number, O>>
|
|
96
105
|
|
|
97
106
|
/** `'true'` or `'false'`. */
|
|
98
107
|
export const boolean = <const O extends ParserOptions = {}>(options?: O): Parser<Result<boolean, O>> =>
|
|
99
|
-
createParser('boolean string', (value) => (value === 'true' ? true : value === 'false' ? false : undefined), options ?? {}
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
createParser('boolean string', (value) => (value === 'true' ? true : value === 'false' ? false : undefined), options ?? {}, {
|
|
109
|
+
type: 'boolean',
|
|
110
|
+
}) as Parser<Result<boolean, O>>
|
|
102
111
|
|
|
103
112
|
/** UUID of any version, case-insensitive. */
|
|
104
113
|
export const uuid = <const O extends ParserOptions = {}>(options?: O): Parser<Result<string, O>> =>
|
|
105
|
-
createParser('UUID string', (value) => (UUID.test(value) ? value : undefined), options ?? {}) as Parser<
|
|
114
|
+
createParser('UUID string', (value) => (UUID.test(value) ? value : undefined), options ?? {}, { type: 'string', format: 'uuid' }) as Parser<
|
|
115
|
+
Result<string, O>
|
|
116
|
+
>
|
|
106
117
|
|
|
107
118
|
/**
|
|
108
119
|
* Applies a parser or a synchronous Standard Schema to a raw value.
|
package/src/http/pipeline.ts
CHANGED
|
@@ -21,10 +21,10 @@ import { getCaughtExceptions } from './enhancers.ts'
|
|
|
21
21
|
import type { CallHandler, CanActivate, ExceptionFilter, FilterLike, Guard, Interceptor, InterceptorLike } from './enhancers.ts'
|
|
22
22
|
import { errorResponse, ForbiddenException } from './exceptions.ts'
|
|
23
23
|
|
|
24
|
-
type GuardStep = (ctx: Context) => unknown
|
|
25
|
-
type InterceptorStep = (ctx: Context, next: CallHandler) => unknown
|
|
24
|
+
export type GuardStep = (ctx: Context) => unknown
|
|
25
|
+
export type InterceptorStep = (ctx: Context, next: CallHandler) => unknown
|
|
26
26
|
|
|
27
|
-
interface FilterStep {
|
|
27
|
+
export interface FilterStep {
|
|
28
28
|
/** Empty: catches everything. */
|
|
29
29
|
readonly types: readonly ClassToken<unknown>[]
|
|
30
30
|
readonly handle: (error: unknown, ctx: Context) => Response | Promise<Response>
|
|
@@ -174,7 +174,7 @@ export interface PipelineParts {
|
|
|
174
174
|
/** In the order they are tried: method, class, global. */
|
|
175
175
|
readonly filters: readonly FilterStep[]
|
|
176
176
|
readonly invoke: (ctx: Context) => unknown
|
|
177
|
-
readonly respond: (value: unknown) => Response
|
|
177
|
+
readonly respond: (value: unknown, ctx: Context) => Response
|
|
178
178
|
readonly onError: (error: unknown, req: Request) => void
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -230,11 +230,11 @@ export const createPipeline = (parts: PipelineParts): Handler => {
|
|
|
230
230
|
|
|
231
231
|
if (isThenable(result)) {
|
|
232
232
|
return Promise.resolve(result)
|
|
233
|
-
.then(respond)
|
|
233
|
+
.then((value) => respond(value, ctx))
|
|
234
234
|
.catch((error: unknown) => fail(error, ctx))
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
return respond(result)
|
|
237
|
+
return respond(result, ctx)
|
|
238
238
|
} catch (error) {
|
|
239
239
|
return fail(error, ctx)
|
|
240
240
|
}
|
package/src/http/routes.ts
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { InjectionToken } from '../di/index.ts'
|
|
14
15
|
import type { Constructor, Container, ControllerEntry, ModuleClass } from '../di/index.ts'
|
|
15
16
|
import type { RouteDefinition } from '../routing/index.ts'
|
|
16
17
|
import { parsePath } from '../routing/path.ts'
|
|
18
|
+
import { methodNames } from '../utils/object.ts'
|
|
17
19
|
import { isThenable } from '../utils/promise.ts'
|
|
18
20
|
import type { Argument } from './arguments.ts'
|
|
19
21
|
import type { Context, RouteInfo } from './context.ts'
|
|
@@ -25,6 +27,10 @@ import { ControllerError } from './errors.ts'
|
|
|
25
27
|
import { createEnhancerResolver, createPipeline } from './pipeline.ts'
|
|
26
28
|
import type { EnhancerResolver } from './pipeline.ts'
|
|
27
29
|
import { createResponder } from './response.ts'
|
|
30
|
+
import { createSseResponder } from './sse.ts'
|
|
31
|
+
import { getGatewayPath } from '../websocket/decorators.ts'
|
|
32
|
+
import { buildGateway } from '../websocket/gateway.ts'
|
|
33
|
+
import type { BuiltGateway } from '../websocket/gateway.ts'
|
|
28
34
|
|
|
29
35
|
/**
|
|
30
36
|
* Options of `controllerRoutes`.
|
|
@@ -62,29 +68,6 @@ const joinPaths = (...parts: readonly string[]): string => {
|
|
|
62
68
|
return path || '/'
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
/**
|
|
66
|
-
* Public method names along the prototype chain, most derived first, without duplicates.
|
|
67
|
-
*/
|
|
68
|
-
const methodNames = (instance: object): string[] => {
|
|
69
|
-
const names: string[] = []
|
|
70
|
-
const seen = new Set<string>(['constructor'])
|
|
71
|
-
|
|
72
|
-
for (
|
|
73
|
-
let prototype: object | null = Object.getPrototypeOf(instance);
|
|
74
|
-
prototype !== null && prototype !== Object.prototype;
|
|
75
|
-
prototype = Object.getPrototypeOf(prototype)
|
|
76
|
-
) {
|
|
77
|
-
for (const name of Object.getOwnPropertyNames(prototype)) {
|
|
78
|
-
if (!seen.has(name)) {
|
|
79
|
-
seen.add(name)
|
|
80
|
-
names.push(name)
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return names
|
|
86
|
-
}
|
|
87
|
-
|
|
88
71
|
/** Every `param()` must name a parameter of the full path, controller prefix included. */
|
|
89
72
|
const assertParamsExist = (args: readonly Argument<unknown>[], path: string, name: string): void => {
|
|
90
73
|
const names = new Set<string>()
|
|
@@ -157,6 +140,9 @@ const resolveAll = <T, S>(
|
|
|
157
140
|
class RoutesBuilder {
|
|
158
141
|
readonly routes: RouteDefinition[] = []
|
|
159
142
|
readonly controllers: MappedController[] = []
|
|
143
|
+
readonly gateways: BuiltGateway[] = []
|
|
144
|
+
/** Routes of the controller being added. */
|
|
145
|
+
#mapped: MappedRoute[] = []
|
|
160
146
|
/** Guards, interceptors and filters created from classes, in creation order. */
|
|
161
147
|
readonly enhancers: unknown[] = []
|
|
162
148
|
readonly #options: ControllerRoutesOptions
|
|
@@ -174,6 +160,14 @@ class RoutesBuilder {
|
|
|
174
160
|
}
|
|
175
161
|
|
|
176
162
|
addController(entry: ControllerEntry): void {
|
|
163
|
+
const gatewayPath = getGatewayPath(entry.controller)
|
|
164
|
+
|
|
165
|
+
if (gatewayPath !== undefined) {
|
|
166
|
+
this.#addGateway(entry, gatewayPath)
|
|
167
|
+
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
177
171
|
const controllerPath = getControllerPath(entry.controller)
|
|
178
172
|
|
|
179
173
|
if (controllerPath === undefined) {
|
|
@@ -185,14 +179,32 @@ class RoutesBuilder {
|
|
|
185
179
|
|
|
186
180
|
const basePath = joinPaths(this.#options.prefix ?? '', controllerPath)
|
|
187
181
|
|
|
188
|
-
|
|
182
|
+
this.#mapped = []
|
|
189
183
|
|
|
190
184
|
// The most derived function: an override without route decorators has no metadata.
|
|
191
185
|
for (const name of methodNames(entry.instance as object)) {
|
|
192
186
|
this.#addMethod(entry, basePath, name)
|
|
193
187
|
}
|
|
194
188
|
|
|
195
|
-
this.controllers.push({ controller: entry.controller, path: basePath, routes: this
|
|
189
|
+
this.controllers.push({ controller: entry.controller, path: basePath, routes: this.#mapped })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** A gateway: its handshake gets the guards and filters of the class and the global ones. */
|
|
193
|
+
#addGateway(entry: ControllerEntry, gatewayPath: string): void {
|
|
194
|
+
const local = this.#resolverOf(entry)
|
|
195
|
+
const controller = getClassEnhancers(entry.controller)
|
|
196
|
+
const options = this.#options
|
|
197
|
+
|
|
198
|
+
this.gateways.push(
|
|
199
|
+
buildGateway({
|
|
200
|
+
entry,
|
|
201
|
+
path: joinPaths(options.prefix ?? '', gatewayPath),
|
|
202
|
+
guards: resolveAll([options.guards ?? [], controller.guards, []], this.#global.guard, local.guard),
|
|
203
|
+
filters: resolveAll([options.filters ?? [], controller.filters, []], this.#global.filter, local.filter).reverse(),
|
|
204
|
+
metadata: controller.metadata,
|
|
205
|
+
onError: this.#onError,
|
|
206
|
+
})
|
|
207
|
+
)
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
#resolverOf(entry: ControllerEntry): EnhancerResolver {
|
|
@@ -238,6 +250,9 @@ class RoutesBuilder {
|
|
|
238
250
|
assertParamsExist(route.args, path, `${entry.controller.name}.${name}`)
|
|
239
251
|
}
|
|
240
252
|
|
|
253
|
+
const status = statusOf(metadata, route)
|
|
254
|
+
|
|
255
|
+
this.#mapped.push({ info, args: route.args, status, sse: route.sse })
|
|
241
256
|
this.routes.push({
|
|
242
257
|
method: route.method,
|
|
243
258
|
path,
|
|
@@ -248,7 +263,7 @@ class RoutesBuilder {
|
|
|
248
263
|
interceptors,
|
|
249
264
|
filters,
|
|
250
265
|
invoke: createInvoker(instance, method, route.args),
|
|
251
|
-
respond:
|
|
266
|
+
respond: route.sse ? createSseResponder(metadata.headers, this.#onError) : createResponder(status, metadata.headers),
|
|
252
267
|
onError: this.#onError,
|
|
253
268
|
}),
|
|
254
269
|
})
|
|
@@ -256,14 +271,36 @@ class RoutesBuilder {
|
|
|
256
271
|
}
|
|
257
272
|
}
|
|
258
273
|
|
|
259
|
-
/** A
|
|
274
|
+
/** A route as declared: what startup logs and API descriptions (OpenAPI) show. */
|
|
275
|
+
export interface MappedRoute {
|
|
276
|
+
/** Controller, method name, HTTP method, full path and custom metadata. */
|
|
277
|
+
readonly info: RouteInfo
|
|
278
|
+
/** Declared arguments; `undefined` when the method receives the context. */
|
|
279
|
+
readonly args: readonly Argument<unknown>[] | undefined
|
|
280
|
+
/** Status of the responses built from returned values. */
|
|
281
|
+
readonly status: number
|
|
282
|
+
/** Declared with `@Sse()`. */
|
|
283
|
+
readonly sse: boolean
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** A controller and the routes made from it. */
|
|
260
287
|
export interface MappedController {
|
|
261
288
|
readonly controller: Constructor<unknown>
|
|
262
289
|
/** Path of the controller, with the global prefix. */
|
|
263
290
|
readonly path: string
|
|
264
|
-
readonly routes: readonly
|
|
291
|
+
readonly routes: readonly MappedRoute[]
|
|
265
292
|
}
|
|
266
293
|
|
|
294
|
+
/**
|
|
295
|
+
* The routes of the application, grouped by controller: `inject(ROUTES)`.
|
|
296
|
+
* Filled once the routes are built, so read it when serving, not in a constructor.
|
|
297
|
+
*/
|
|
298
|
+
export interface RouteRegistry {
|
|
299
|
+
readonly controllers: readonly MappedController[]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const ROUTES = new InjectionToken<RouteRegistry>('ROUTES')
|
|
303
|
+
|
|
267
304
|
/**
|
|
268
305
|
* Routes of every controller, and the guards, interceptors and filters created
|
|
269
306
|
* for them from classes (whose lifecycle hooks the application runs).
|
|
@@ -272,6 +309,8 @@ export interface BuiltRoutes {
|
|
|
272
309
|
readonly routes: RouteDefinition[]
|
|
273
310
|
/** In module order. */
|
|
274
311
|
readonly controllers: readonly MappedController[]
|
|
312
|
+
/** WebSocket gateways, in module order. */
|
|
313
|
+
readonly gateways: readonly BuiltGateway[]
|
|
275
314
|
/** In creation order. */
|
|
276
315
|
readonly enhancers: readonly unknown[]
|
|
277
316
|
}
|
|
@@ -286,7 +325,7 @@ export const buildRoutes = (container: Container, options: ControllerRoutesOptio
|
|
|
286
325
|
builder.addController(entry)
|
|
287
326
|
}
|
|
288
327
|
|
|
289
|
-
return { routes: builder.routes, controllers: builder.controllers, enhancers: builder.enhancers }
|
|
328
|
+
return { routes: builder.routes, controllers: builder.controllers, gateways: builder.gateways, enhancers: builder.enhancers }
|
|
290
329
|
}
|
|
291
330
|
|
|
292
331
|
/**
|
package/src/http/schema.ts
CHANGED
|
@@ -67,7 +67,8 @@ const OMITTED = 'Further validation errors were omitted'
|
|
|
67
67
|
export const isStandardSchema = (value: unknown): value is StandardSchemaV1 =>
|
|
68
68
|
typeof value === 'object' && value !== null && '~standard' in value
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
/** Messages of `issues`, each with the path of its value under `prefix`. */
|
|
71
|
+
export const formatIssues = (issues: readonly StandardSchemaV1.Issue[], prefix: string): string[] => {
|
|
71
72
|
const messages: string[] = []
|
|
72
73
|
let remaining = MAX_MESSAGES_LENGTH
|
|
73
74
|
|