@ailura/nestjs-hono-adapter 1.0.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/LICENSE +21 -0
- package/README.md +411 -0
- package/dist/body.d.ts +35 -0
- package/dist/body.d.ts.map +1 -0
- package/dist/body.js +180 -0
- package/dist/body.js.map +1 -0
- package/dist/bridge.d.ts +64 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +168 -0
- package/dist/bridge.js.map +1 -0
- package/dist/closing.d.ts +13 -0
- package/dist/closing.d.ts.map +1 -0
- package/dist/closing.js +30 -0
- package/dist/closing.js.map +1 -0
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +2 -0
- package/dist/context.js.map +1 -0
- package/dist/cors-middleware.d.ts +64 -0
- package/dist/cors-middleware.d.ts.map +1 -0
- package/dist/cors-middleware.js +211 -0
- package/dist/cors-middleware.js.map +1 -0
- package/dist/handler-bridge.d.ts +51 -0
- package/dist/handler-bridge.d.ts.map +1 -0
- package/dist/handler-bridge.js +122 -0
- package/dist/handler-bridge.js.map +1 -0
- package/dist/hono-lifecycle.d.ts +90 -0
- package/dist/hono-lifecycle.d.ts.map +1 -0
- package/dist/hono-lifecycle.js +169 -0
- package/dist/hono-lifecycle.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/path.d.ts +3 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +144 -0
- package/dist/path.js.map +1 -0
- package/dist/query.d.ts +19 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +238 -0
- package/dist/query.js.map +1 -0
- package/dist/response-helpers.d.ts +16 -0
- package/dist/response-helpers.d.ts.map +1 -0
- package/dist/response-helpers.js +45 -0
- package/dist/response-helpers.js.map +1 -0
- package/dist/response-writer.d.ts +28 -0
- package/dist/response-writer.d.ts.map +1 -0
- package/dist/response-writer.js +52 -0
- package/dist/response-writer.js.map +1 -0
- package/dist/route-adapter.d.ts +53 -0
- package/dist/route-adapter.d.ts.map +1 -0
- package/dist/route-adapter.js +141 -0
- package/dist/route-adapter.js.map +1 -0
- package/dist/server-adapter.d.ts +106 -0
- package/dist/server-adapter.d.ts.map +1 -0
- package/dist/server-adapter.js +153 -0
- package/dist/server-adapter.js.map +1 -0
- package/dist/sse.d.ts +67 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +211 -0
- package/dist/sse.js.map +1 -0
- package/dist/static-assets.d.ts +39 -0
- package/dist/static-assets.d.ts.map +1 -0
- package/dist/static-assets.js +155 -0
- package/dist/static-assets.js.map +1 -0
- package/dist/version-filter.d.ts +24 -0
- package/dist/version-filter.d.ts.map +1 -0
- package/dist/version-filter.js +107 -0
- package/dist/version-filter.js.map +1 -0
- package/dist/versioned-route.d.ts +21 -0
- package/dist/versioned-route.d.ts.map +1 -0
- package/dist/versioned-route.js +15 -0
- package/dist/versioned-route.js.map +1 -0
- package/dist/views.d.ts +42 -0
- package/dist/views.d.ts.map +1 -0
- package/dist/views.js +110 -0
- package/dist/views.js.map +1 -0
- package/dist/ws-adapter.d.ts +81 -0
- package/dist/ws-adapter.d.ts.map +1 -0
- package/dist/ws-adapter.js +214 -0
- package/dist/ws-adapter.js.map +1 -0
- package/dist/ws-client.d.ts +68 -0
- package/dist/ws-client.d.ts.map +1 -0
- package/dist/ws-client.js +135 -0
- package/dist/ws-client.js.map +1 -0
- package/dist/ws-server.d.ts +23 -0
- package/dist/ws-server.d.ts.map +1 -0
- package/dist/ws-server.js +37 -0
- package/dist/ws-server.js.map +1 -0
- package/dist/ws.d.ts +14 -0
- package/dist/ws.d.ts.map +1 -0
- package/dist/ws.js +12 -0
- package/dist/ws.js.map +1 -0
- package/package.json +99 -0
- package/src/body.ts +251 -0
- package/src/bridge.ts +308 -0
- package/src/closing.ts +38 -0
- package/src/context.ts +25 -0
- package/src/cors-middleware.ts +347 -0
- package/src/handler-bridge.ts +226 -0
- package/src/hono-lifecycle.ts +259 -0
- package/src/index.ts +27 -0
- package/src/path.ts +169 -0
- package/src/query.ts +304 -0
- package/src/response-helpers.ts +60 -0
- package/src/response-writer.ts +100 -0
- package/src/route-adapter.ts +261 -0
- package/src/server-adapter.ts +294 -0
- package/src/sse.ts +274 -0
- package/src/static-assets.ts +247 -0
- package/src/version-filter.ts +170 -0
- package/src/versioned-route.ts +30 -0
- package/src/views.ts +188 -0
- package/src/ws-adapter.ts +329 -0
- package/src/ws-client.ts +190 -0
- package/src/ws-server.ts +40 -0
- package/src/ws.ts +13 -0
package/src/query.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A query string parsed into the shape a controller receives:
|
|
3
|
+
* every leaf is a string, and a name that appears more than
|
|
4
|
+
* once or uses brackets becomes a list or a nested object.
|
|
5
|
+
*/
|
|
6
|
+
type ParsedQuery = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Names that reach the prototype chain. A parser that writes
|
|
10
|
+
* them hands a caller control over every object in the process,
|
|
11
|
+
* so they are dropped instead.
|
|
12
|
+
*/
|
|
13
|
+
const FORBIDDEN_NAMES = new Set([
|
|
14
|
+
'__proto__',
|
|
15
|
+
'constructor',
|
|
16
|
+
'prototype',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const INDEX = /^\d+$/u;
|
|
20
|
+
const BRACKETS = /\[(?<name>[^\]]*)\]/gu;
|
|
21
|
+
const MISSING = -1;
|
|
22
|
+
|
|
23
|
+
/** One assignment: the names still to walk and the value. */
|
|
24
|
+
interface Target {
|
|
25
|
+
readonly names: readonly string[];
|
|
26
|
+
readonly value: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isRecord(
|
|
30
|
+
value: unknown,
|
|
31
|
+
): value is Record<string, unknown> {
|
|
32
|
+
return (
|
|
33
|
+
typeof value === 'object' &&
|
|
34
|
+
value !== null &&
|
|
35
|
+
!Array.isArray(value)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
40
|
+
if (isRecord(value)) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function asList(value: unknown): unknown[] {
|
|
47
|
+
if (Array.isArray(value)) {
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Says whether the next segment builds a list: `[]` appends,
|
|
55
|
+
* and a number indexes.
|
|
56
|
+
*/
|
|
57
|
+
function isListStep(name: string): boolean {
|
|
58
|
+
return name === '' || INDEX.test(name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Decodes one side of a pair. A sequence that is not valid
|
|
63
|
+
* percent-encoding is kept as it arrived, which is what the
|
|
64
|
+
* platform parsers do rather than refusing the request.
|
|
65
|
+
*/
|
|
66
|
+
function decode(value: string): string {
|
|
67
|
+
const spaced = value.replaceAll('+', ' ');
|
|
68
|
+
try {
|
|
69
|
+
return decodeURIComponent(spaced);
|
|
70
|
+
} catch {
|
|
71
|
+
return spaced;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Splits `filter[name][]` into `['filter', 'name', '']`. A name
|
|
77
|
+
* without brackets is its own single segment.
|
|
78
|
+
*/
|
|
79
|
+
function toNames(name: string): string[] {
|
|
80
|
+
const first = name.indexOf('[');
|
|
81
|
+
if (first === MISSING) {
|
|
82
|
+
return [name];
|
|
83
|
+
}
|
|
84
|
+
const names = [name.slice(0, first)];
|
|
85
|
+
for (const match of name.slice(first).matchAll(BRACKETS)) {
|
|
86
|
+
const groups = match.groups ?? {};
|
|
87
|
+
names.push(groups.name ?? '');
|
|
88
|
+
}
|
|
89
|
+
return names;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Adds a value to a name that may already hold one. The second
|
|
94
|
+
* value turns the entry into a list, which is how a repeated
|
|
95
|
+
* name is read.
|
|
96
|
+
*/
|
|
97
|
+
function repeated(current: unknown, value: string): unknown {
|
|
98
|
+
if (current === undefined) {
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(current)) {
|
|
102
|
+
current.push(value);
|
|
103
|
+
return current;
|
|
104
|
+
}
|
|
105
|
+
return [current, value];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function sidesOf(pair: string): readonly [string, string] {
|
|
109
|
+
const separator = pair.indexOf('=');
|
|
110
|
+
if (separator === MISSING) {
|
|
111
|
+
return [pair, ''];
|
|
112
|
+
}
|
|
113
|
+
return [pair.slice(0, separator), pair.slice(separator + 1)];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function withoutQuestionMark(source: string): string {
|
|
117
|
+
if (source.startsWith('?')) {
|
|
118
|
+
return source.slice(1);
|
|
119
|
+
}
|
|
120
|
+
return source;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function accepts(names: readonly string[]): boolean {
|
|
124
|
+
if (names.length === 0) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
return !names.some((segment) => FORBIDDEN_NAMES.has(segment));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Builds the tree one pair at a time.
|
|
132
|
+
*
|
|
133
|
+
* A step whose next name is `[]` or a number builds a list, and
|
|
134
|
+
* every other step builds an object; the methods below are
|
|
135
|
+
* split along that decision so each one stays readable on its
|
|
136
|
+
* own.
|
|
137
|
+
*/
|
|
138
|
+
class QueryBuilder {
|
|
139
|
+
private readonly query: ParsedQuery = {};
|
|
140
|
+
|
|
141
|
+
public parse(source: string): ParsedQuery {
|
|
142
|
+
for (const pair of withoutQuestionMark(source).split('&')) {
|
|
143
|
+
this.addPair(pair);
|
|
144
|
+
}
|
|
145
|
+
return this.query;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private addPair(pair: string): void {
|
|
149
|
+
if (pair === '') {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const [name, value] = sidesOf(pair);
|
|
153
|
+
const names = toNames(decode(name));
|
|
154
|
+
if (!accepts(names)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.assign(this.query, { names, value: decode(value) });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private assign(
|
|
161
|
+
container: Record<string, unknown>,
|
|
162
|
+
target: Target,
|
|
163
|
+
): void {
|
|
164
|
+
const [name, ...rest] = target.names;
|
|
165
|
+
if (name === undefined) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (rest.length === 0) {
|
|
169
|
+
container[name] = repeated(container[name], target.value);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.assignDeeper(container, name, {
|
|
173
|
+
names: rest,
|
|
174
|
+
value: target.value,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private assignDeeper(
|
|
179
|
+
container: Record<string, unknown>,
|
|
180
|
+
name: string,
|
|
181
|
+
target: Target,
|
|
182
|
+
): void {
|
|
183
|
+
const [next] = target.names;
|
|
184
|
+
if (isListStep(next ?? '')) {
|
|
185
|
+
this.assignList(container, name, target);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.assignRecord(container, name, target);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private assignList(
|
|
192
|
+
container: Record<string, unknown>,
|
|
193
|
+
name: string,
|
|
194
|
+
target: Target,
|
|
195
|
+
): void {
|
|
196
|
+
const list = asList(container[name]);
|
|
197
|
+
container[name] = list;
|
|
198
|
+
this.intoList(list, target);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private assignRecord(
|
|
202
|
+
container: Record<string, unknown>,
|
|
203
|
+
name: string,
|
|
204
|
+
target: Target,
|
|
205
|
+
): void {
|
|
206
|
+
const existing = container[name];
|
|
207
|
+
if (Array.isArray(existing)) {
|
|
208
|
+
const nested: Record<string, unknown> = {};
|
|
209
|
+
existing.push(nested);
|
|
210
|
+
this.assign(nested, target);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
container[name] = this.toRecord(existing, target);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private intoList(list: unknown[], target: Target): void {
|
|
217
|
+
const [name, ...rest] = target.names;
|
|
218
|
+
if (name === undefined) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const nested = { names: rest, value: target.value };
|
|
222
|
+
if (name === '') {
|
|
223
|
+
this.addElement(list, nested);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
this.addIndex(list, name, nested);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private addIndex(
|
|
230
|
+
list: unknown[],
|
|
231
|
+
name: string,
|
|
232
|
+
target: Target,
|
|
233
|
+
): void {
|
|
234
|
+
const index = Number(name);
|
|
235
|
+
if (!Number.isSafeInteger(index) || index < 0) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.addAt(list, index, target);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private addElement(list: unknown[], target: Target): void {
|
|
242
|
+
const [next] = target.names;
|
|
243
|
+
if (next === undefined) {
|
|
244
|
+
list.push(target.value);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (isListStep(next)) {
|
|
248
|
+
const nested: unknown[] = [];
|
|
249
|
+
list.push(nested);
|
|
250
|
+
this.intoList(nested, target);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
list.push(this.toRecord(undefined, target));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private addAt(
|
|
257
|
+
list: unknown[],
|
|
258
|
+
index: number,
|
|
259
|
+
target: Target,
|
|
260
|
+
): void {
|
|
261
|
+
const existing = list[index];
|
|
262
|
+
if (target.names.length === 0) {
|
|
263
|
+
list[index] = repeated(existing, target.value);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const [next] = target.names;
|
|
267
|
+
if (isListStep(next ?? '')) {
|
|
268
|
+
list[index] = this.toList(existing, target);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
list[index] = this.toRecord(existing, target);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private toList(existing: unknown, target: Target): unknown[] {
|
|
275
|
+
const nested = asList(existing);
|
|
276
|
+
this.intoList(nested, target);
|
|
277
|
+
return nested;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private toRecord(
|
|
281
|
+
existing: unknown,
|
|
282
|
+
target: Target,
|
|
283
|
+
): Record<string, unknown> {
|
|
284
|
+
const nested = asRecord(existing);
|
|
285
|
+
this.assign(nested, target);
|
|
286
|
+
return nested;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Parses a query string, with or without its leading `?`.
|
|
292
|
+
*
|
|
293
|
+
* Repeated names become lists, `name[]` appends, `name[0]`
|
|
294
|
+
* indexes and `name[child]` nests; anything else is a string.
|
|
295
|
+
* The grammar is the one the platform parsers accept, which is
|
|
296
|
+
* what a controller written against Express or Fastify expects
|
|
297
|
+
* to read.
|
|
298
|
+
*/
|
|
299
|
+
function parseQuery(source: string): ParsedQuery {
|
|
300
|
+
return new QueryBuilder().parse(source);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export { parseQuery };
|
|
304
|
+
export type { ParsedQuery };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { NestContext } from './context.ts';
|
|
2
|
+
|
|
3
|
+
/** The Hono helpers that write the answer for a request. */
|
|
4
|
+
const RESPONSE_HELPERS = [
|
|
5
|
+
'body',
|
|
6
|
+
'html',
|
|
7
|
+
'json',
|
|
8
|
+
'newResponse',
|
|
9
|
+
'notFound',
|
|
10
|
+
'redirect',
|
|
11
|
+
'text',
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type ResponseHelper = (...args: never[]) => Response;
|
|
15
|
+
|
|
16
|
+
type ResponseHelperCarrier = Record<
|
|
17
|
+
(typeof RESPONSE_HELPERS)[number],
|
|
18
|
+
ResponseHelper
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Wraps one helper so that the response it built becomes the
|
|
23
|
+
* response of the context.
|
|
24
|
+
*/
|
|
25
|
+
function wrapHelper(
|
|
26
|
+
carrier: ResponseHelperCarrier,
|
|
27
|
+
name: (typeof RESPONSE_HELPERS)[number],
|
|
28
|
+
context: NestContext,
|
|
29
|
+
): void {
|
|
30
|
+
const helper = carrier[name];
|
|
31
|
+
if (typeof helper !== 'function') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
carrier[name] = (...args: never[]): Response => {
|
|
35
|
+
const response = helper.apply(context, args);
|
|
36
|
+
context.res = response;
|
|
37
|
+
return response;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Makes the Hono response helpers finalize the context.
|
|
43
|
+
*
|
|
44
|
+
* A handler that answers through `@Res()` calls one of these
|
|
45
|
+
* helpers instead of returning a value, and Nest then skips its
|
|
46
|
+
* own reply path. Hono only finalizes a context when `res` is
|
|
47
|
+
* assigned, and the helpers do not assign it, so the answer
|
|
48
|
+
* would be dropped and the client would read an empty response.
|
|
49
|
+
* Wrapping them on the instance — Hono's helpers are own
|
|
50
|
+
* properties, not prototype methods — keeps the imperative
|
|
51
|
+
* style working without taking the response away from Hono.
|
|
52
|
+
*/
|
|
53
|
+
function finalizeOnResponse(context: NestContext): void {
|
|
54
|
+
const carrier = context as unknown as ResponseHelperCarrier;
|
|
55
|
+
for (const name of RESPONSE_HELPERS) {
|
|
56
|
+
wrapHelper(carrier, name, context);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { finalizeOnResponse };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { HttpStatus } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
import { buildResponse } from './bridge.ts';
|
|
4
|
+
import type { NestContext } from './context.ts';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Writes what Nest answers onto the context of the request.
|
|
8
|
+
*
|
|
9
|
+
* A Hono context doubles as the response object, so a value is
|
|
10
|
+
* turned into a Web response and stored on it. A status set
|
|
11
|
+
* through `@HttpCode()` or `@Res()` is remembered per request,
|
|
12
|
+
* because the adapter is asked for it after the handler ran.
|
|
13
|
+
*/
|
|
14
|
+
class ResponseWriter {
|
|
15
|
+
private readonly pendingStatus = new WeakMap<
|
|
16
|
+
NestContext,
|
|
17
|
+
number
|
|
18
|
+
>();
|
|
19
|
+
|
|
20
|
+
public status(
|
|
21
|
+
response: NestContext,
|
|
22
|
+
statusCode: number,
|
|
23
|
+
): void {
|
|
24
|
+
this.pendingStatus.set(response, statusCode);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The status Nest asked for, before any body was written. An
|
|
29
|
+
* event stream needs it while the handler still runs.
|
|
30
|
+
*/
|
|
31
|
+
public statusOf(response: NestContext): number | undefined {
|
|
32
|
+
return this.pendingStatus.get(response);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public reply(
|
|
36
|
+
response: NestContext,
|
|
37
|
+
body: unknown,
|
|
38
|
+
statusCode?: number,
|
|
39
|
+
): void {
|
|
40
|
+
response.res = buildResponse(
|
|
41
|
+
response,
|
|
42
|
+
body,
|
|
43
|
+
statusCode ?? this.resolve(response),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public end(response: NestContext, message?: string): void {
|
|
48
|
+
response.res = buildResponse(
|
|
49
|
+
response,
|
|
50
|
+
message,
|
|
51
|
+
this.resolve(response),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public redirect(
|
|
56
|
+
response: NestContext,
|
|
57
|
+
statusCode: number,
|
|
58
|
+
url: string,
|
|
59
|
+
): void {
|
|
60
|
+
response.header('Location', url);
|
|
61
|
+
response.res = buildResponse(
|
|
62
|
+
response,
|
|
63
|
+
undefined,
|
|
64
|
+
statusCode,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public setHeader(
|
|
69
|
+
response: NestContext,
|
|
70
|
+
name: string,
|
|
71
|
+
value: string,
|
|
72
|
+
): void {
|
|
73
|
+
response.header(name, value);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public getHeader(
|
|
77
|
+
response: NestContext,
|
|
78
|
+
name: string,
|
|
79
|
+
): string | undefined {
|
|
80
|
+
return response.res.headers.get(name) ?? undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public appendHeader(
|
|
84
|
+
response: NestContext,
|
|
85
|
+
name: string,
|
|
86
|
+
value: string,
|
|
87
|
+
): void {
|
|
88
|
+
response.header(name, value, { append: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public isHeadersSent(response: NestContext): boolean {
|
|
92
|
+
return response.finalized;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private resolve(response: NestContext): number {
|
|
96
|
+
return this.pendingStatus.get(response) ?? HttpStatus.OK;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { ResponseWriter };
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { ServerType } from '@hono/node-server';
|
|
2
|
+
import { RequestMethod } from '@nestjs/common';
|
|
3
|
+
import type { VersioningOptions } from '@nestjs/common';
|
|
4
|
+
import { AbstractHttpAdapter } from '@nestjs/core';
|
|
5
|
+
|
|
6
|
+
import type { NestHandler, NestRequest } from './bridge.ts';
|
|
7
|
+
import type { NestContext } from './context.ts';
|
|
8
|
+
import { createVersionFilter } from './version-filter.ts';
|
|
9
|
+
import type { VersionValue } from './version-filter.ts';
|
|
10
|
+
import { asVersionedRoute } from './versioned-route.ts';
|
|
11
|
+
import type { VersionedRoute } from './versioned-route.ts';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Nest's verb enum mapped onto HTTP method names, for the one
|
|
15
|
+
* place that receives the enum rather than the name.
|
|
16
|
+
*/
|
|
17
|
+
const METHOD_BY_REQUEST_METHOD: Record<number, string> = {
|
|
18
|
+
[RequestMethod.GET]: 'GET',
|
|
19
|
+
[RequestMethod.POST]: 'POST',
|
|
20
|
+
[RequestMethod.PUT]: 'PUT',
|
|
21
|
+
[RequestMethod.DELETE]: 'DELETE',
|
|
22
|
+
[RequestMethod.PATCH]: 'PATCH',
|
|
23
|
+
[RequestMethod.ALL]: 'ALL',
|
|
24
|
+
[RequestMethod.OPTIONS]: 'OPTIONS',
|
|
25
|
+
[RequestMethod.HEAD]: 'HEAD',
|
|
26
|
+
[RequestMethod.SEARCH]: 'SEARCH',
|
|
27
|
+
[RequestMethod.PROPFIND]: 'PROPFIND',
|
|
28
|
+
[RequestMethod.PROPPATCH]: 'PROPPATCH',
|
|
29
|
+
[RequestMethod.MKCOL]: 'MKCOL',
|
|
30
|
+
[RequestMethod.COPY]: 'COPY',
|
|
31
|
+
[RequestMethod.MOVE]: 'MOVE',
|
|
32
|
+
[RequestMethod.LOCK]: 'LOCK',
|
|
33
|
+
[RequestMethod.UNLOCK]: 'UNLOCK',
|
|
34
|
+
[RequestMethod.QUERY]: 'QUERY',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Method registered for a route that answers every verb, and
|
|
39
|
+
* for middleware, which Nest mounts without one.
|
|
40
|
+
*/
|
|
41
|
+
const ALL_METHOD = 'ALL';
|
|
42
|
+
|
|
43
|
+
/** Path a handler is mounted at when Nest passes it without one. */
|
|
44
|
+
const ROOT_PATH = '/';
|
|
45
|
+
|
|
46
|
+
/** Either a route path, or the handler to mount at the root. */
|
|
47
|
+
type RouteTarget = string | NestHandler;
|
|
48
|
+
|
|
49
|
+
function isHandler(value: unknown): value is NestHandler {
|
|
50
|
+
return typeof value === 'function';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function resolveRoute(
|
|
54
|
+
target: RouteTarget,
|
|
55
|
+
handler: NestHandler | undefined,
|
|
56
|
+
): [string, NestHandler] {
|
|
57
|
+
if (typeof target === 'function') {
|
|
58
|
+
return [ROOT_PATH, target];
|
|
59
|
+
}
|
|
60
|
+
if (handler === undefined) {
|
|
61
|
+
throw new TypeError(
|
|
62
|
+
'A route registered with a path also needs a handler.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return [target, handler];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The routing half of the Nest adapter contract: every verb a
|
|
70
|
+
* route can be mapped to, the middleware factory and version
|
|
71
|
+
* filtering. The concrete adapter supplies the registration, so
|
|
72
|
+
* this half never touches Hono itself.
|
|
73
|
+
*/
|
|
74
|
+
abstract class RouteAdapter extends AbstractHttpAdapter<
|
|
75
|
+
ServerType,
|
|
76
|
+
NestRequest,
|
|
77
|
+
NestContext
|
|
78
|
+
> {
|
|
79
|
+
/** Registers one route with the underlying framework. */
|
|
80
|
+
protected abstract register(
|
|
81
|
+
method: string,
|
|
82
|
+
path: string,
|
|
83
|
+
handler: NestHandler,
|
|
84
|
+
): void;
|
|
85
|
+
|
|
86
|
+
public override get(
|
|
87
|
+
target: RouteTarget,
|
|
88
|
+
handler?: NestHandler,
|
|
89
|
+
): void {
|
|
90
|
+
this.route('GET', target, handler);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public override post(
|
|
94
|
+
target: RouteTarget,
|
|
95
|
+
handler?: NestHandler,
|
|
96
|
+
): void {
|
|
97
|
+
this.route('POST', target, handler);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public override put(
|
|
101
|
+
target: RouteTarget,
|
|
102
|
+
handler?: NestHandler,
|
|
103
|
+
): void {
|
|
104
|
+
this.route('PUT', target, handler);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public override patch(
|
|
108
|
+
target: RouteTarget,
|
|
109
|
+
handler?: NestHandler,
|
|
110
|
+
): void {
|
|
111
|
+
this.route('PATCH', target, handler);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public override delete(
|
|
115
|
+
target: RouteTarget,
|
|
116
|
+
handler?: NestHandler,
|
|
117
|
+
): void {
|
|
118
|
+
this.route('DELETE', target, handler);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public override options(
|
|
122
|
+
target: RouteTarget,
|
|
123
|
+
handler?: NestHandler,
|
|
124
|
+
): void {
|
|
125
|
+
this.route('OPTIONS', target, handler);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public override head(
|
|
129
|
+
target: RouteTarget,
|
|
130
|
+
handler?: NestHandler,
|
|
131
|
+
): void {
|
|
132
|
+
this.route('HEAD', target, handler);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public override all(
|
|
136
|
+
target: RouteTarget,
|
|
137
|
+
handler?: NestHandler,
|
|
138
|
+
): void {
|
|
139
|
+
this.route(ALL_METHOD, target, handler);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public override search(
|
|
143
|
+
target: RouteTarget,
|
|
144
|
+
handler?: NestHandler,
|
|
145
|
+
): void {
|
|
146
|
+
this.route('SEARCH', target, handler);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public override query(
|
|
150
|
+
target: RouteTarget,
|
|
151
|
+
handler?: NestHandler,
|
|
152
|
+
): void {
|
|
153
|
+
this.route('QUERY', target, handler);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public override propfind(
|
|
157
|
+
target: RouteTarget,
|
|
158
|
+
handler?: NestHandler,
|
|
159
|
+
): void {
|
|
160
|
+
this.route('PROPFIND', target, handler);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public override proppatch(
|
|
164
|
+
target: RouteTarget,
|
|
165
|
+
handler?: NestHandler,
|
|
166
|
+
): void {
|
|
167
|
+
this.route('PROPPATCH', target, handler);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public override mkcol(
|
|
171
|
+
target: RouteTarget,
|
|
172
|
+
handler?: NestHandler,
|
|
173
|
+
): void {
|
|
174
|
+
this.route('MKCOL', target, handler);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public override copy(
|
|
178
|
+
target: RouteTarget,
|
|
179
|
+
handler?: NestHandler,
|
|
180
|
+
): void {
|
|
181
|
+
this.route('COPY', target, handler);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public override move(
|
|
185
|
+
target: RouteTarget,
|
|
186
|
+
handler?: NestHandler,
|
|
187
|
+
): void {
|
|
188
|
+
this.route('MOVE', target, handler);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
public override lock(
|
|
192
|
+
target: RouteTarget,
|
|
193
|
+
handler?: NestHandler,
|
|
194
|
+
): void {
|
|
195
|
+
this.route('LOCK', target, handler);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public override unlock(
|
|
199
|
+
target: RouteTarget,
|
|
200
|
+
handler?: NestHandler,
|
|
201
|
+
): void {
|
|
202
|
+
this.route('UNLOCK', target, handler);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Mounts global middleware. Nest calls this with a handler,
|
|
207
|
+
* or with a path and a handler, and both answer every
|
|
208
|
+
* method.
|
|
209
|
+
*/
|
|
210
|
+
public override use(...args: unknown[]): void {
|
|
211
|
+
const [first, second] = args;
|
|
212
|
+
if (isHandler(first) && second === undefined) {
|
|
213
|
+
this.route(ALL_METHOD, first);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (typeof first === 'string' && isHandler(second)) {
|
|
217
|
+
this.route(ALL_METHOD, first, second);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
throw new TypeError(
|
|
221
|
+
'Middleware is mounted as a handler, or as a path and a ' +
|
|
222
|
+
'handler.',
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
public override createMiddlewareFactory(
|
|
227
|
+
requestMethod: RequestMethod,
|
|
228
|
+
): (path: string, callback: unknown) => void {
|
|
229
|
+
const method =
|
|
230
|
+
METHOD_BY_REQUEST_METHOD[requestMethod] ?? ALL_METHOD;
|
|
231
|
+
return (path, callback) => {
|
|
232
|
+
if (!isHandler(callback)) {
|
|
233
|
+
throw new TypeError(
|
|
234
|
+
'Nest middleware must be registered as a function.',
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
this.route(method, path, callback);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public override applyVersionFilter(
|
|
242
|
+
handler: NestHandler,
|
|
243
|
+
version: VersionValue,
|
|
244
|
+
versioningOptions: VersioningOptions,
|
|
245
|
+
): VersionedRoute {
|
|
246
|
+
return asVersionedRoute(
|
|
247
|
+
createVersionFilter(handler, version, versioningOptions),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private route(
|
|
252
|
+
method: string,
|
|
253
|
+
target: RouteTarget,
|
|
254
|
+
handler?: NestHandler,
|
|
255
|
+
): void {
|
|
256
|
+
const [path, routeHandler] = resolveRoute(target, handler);
|
|
257
|
+
this.register(method, path, routeHandler);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export { ALL_METHOD, RouteAdapter };
|