@fluojs/http 1.0.0-beta.1
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.ko.md +142 -0
- package/README.md +144 -0
- package/dist/adapter.d.ts +58 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +42 -0
- package/dist/adapters/binding.d.ts +11 -0
- package/dist/adapters/binding.d.ts.map +1 -0
- package/dist/adapters/binding.js +185 -0
- package/dist/adapters/dto-validation-adapter.d.ts +10 -0
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -0
- package/dist/adapters/dto-validation-adapter.js +46 -0
- package/dist/client-identity.d.ts +21 -0
- package/dist/client-identity.d.ts.map +1 -0
- package/dist/client-identity.js +108 -0
- package/dist/context/request-context.d.ts +53 -0
- package/dist/context/request-context.d.ts.map +1 -0
- package/dist/context/request-context.js +89 -0
- package/dist/context/sse.d.ts +21 -0
- package/dist/context/sse.d.ts.map +1 -0
- package/dist/context/sse.js +106 -0
- package/dist/decorators.d.ts +188 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +378 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts +9 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-content-negotiation.js +164 -0
- package/dist/dispatch/dispatch-error-policy.d.ts +3 -0
- package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-policy.js +24 -0
- package/dist/dispatch/dispatch-handler-policy.d.ts +3 -0
- package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-handler-policy.js +21 -0
- package/dist/dispatch/dispatch-response-policy.d.ts +7 -0
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-response-policy.js +45 -0
- package/dist/dispatch/dispatch-routing-policy.d.ts +4 -0
- package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-routing-policy.js +14 -0
- package/dist/dispatch/dispatcher.d.ts +36 -0
- package/dist/dispatch/dispatcher.d.ts.map +1 -0
- package/dist/dispatch/dispatcher.js +196 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/exceptions.d.ts +174 -0
- package/dist/exceptions.d.ts.map +1 -0
- package/dist/exceptions.js +222 -0
- package/dist/guards.d.ts +3 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +19 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/input-error-detail.d.ts +10 -0
- package/dist/input-error-detail.d.ts.map +1 -0
- package/dist/input-error-detail.js +8 -0
- package/dist/interceptors.d.ts +3 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +22 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +2 -0
- package/dist/mapping.d.ts +7 -0
- package/dist/mapping.d.ts.map +1 -0
- package/dist/mapping.js +244 -0
- package/dist/middleware/correlation.d.ts +3 -0
- package/dist/middleware/correlation.d.ts.map +1 -0
- package/dist/middleware/correlation.js +19 -0
- package/dist/middleware/cors.d.ts +11 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +57 -0
- package/dist/middleware/middleware.d.ts +8 -0
- package/dist/middleware/middleware.d.ts.map +1 -0
- package/dist/middleware/middleware.js +64 -0
- package/dist/middleware/rate-limit.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +106 -0
- package/dist/middleware/security-headers.d.ts +12 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +47 -0
- package/dist/route-path.d.ts +15 -0
- package/dist/route-path.d.ts.map +1 -0
- package/dist/route-path.js +69 -0
- package/dist/types.d.ts +274 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +114 -0
- package/package.json +58 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { metadataSymbol } from '@fluojs/core/internal';
|
|
2
|
+
import { validateRoutePath } from './route-path.js';
|
|
3
|
+
const standardControllerMetadataKey = Symbol.for('fluo.standard.controller');
|
|
4
|
+
const standardRouteMetadataKey = Symbol.for('fluo.standard.route');
|
|
5
|
+
const standardDtoBindingMetadataKey = Symbol.for('fluo.standard.dto-binding');
|
|
6
|
+
function normalizeProducesMediaTypes(mediaTypes) {
|
|
7
|
+
const normalized = [];
|
|
8
|
+
for (const mediaType of mediaTypes) {
|
|
9
|
+
const value = mediaType.trim();
|
|
10
|
+
if (!value || normalized.includes(value)) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
normalized.push(value);
|
|
14
|
+
}
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
function mergeUnique(existing, values) {
|
|
18
|
+
const merged = [...(existing ?? [])];
|
|
19
|
+
for (const value of values) {
|
|
20
|
+
if (!merged.includes(value)) {
|
|
21
|
+
merged.push(value);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return merged;
|
|
25
|
+
}
|
|
26
|
+
function getStandardMetadataBag(metadata) {
|
|
27
|
+
void metadataSymbol;
|
|
28
|
+
return metadata;
|
|
29
|
+
}
|
|
30
|
+
function getStandardControllerRecord(metadata) {
|
|
31
|
+
const bag = getStandardMetadataBag(metadata);
|
|
32
|
+
const current = bag[standardControllerMetadataKey];
|
|
33
|
+
if (current) {
|
|
34
|
+
return current;
|
|
35
|
+
}
|
|
36
|
+
const created = {};
|
|
37
|
+
bag[standardControllerMetadataKey] = created;
|
|
38
|
+
return created;
|
|
39
|
+
}
|
|
40
|
+
function getStandardRouteMap(metadata) {
|
|
41
|
+
const bag = getStandardMetadataBag(metadata);
|
|
42
|
+
const current = bag[standardRouteMetadataKey];
|
|
43
|
+
if (current) {
|
|
44
|
+
return current;
|
|
45
|
+
}
|
|
46
|
+
const created = new Map();
|
|
47
|
+
bag[standardRouteMetadataKey] = created;
|
|
48
|
+
return created;
|
|
49
|
+
}
|
|
50
|
+
function getStandardRouteRecord(metadata, propertyKey) {
|
|
51
|
+
const routeMap = getStandardRouteMap(metadata);
|
|
52
|
+
const current = routeMap.get(propertyKey);
|
|
53
|
+
if (current) {
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
56
|
+
const created = {};
|
|
57
|
+
routeMap.set(propertyKey, created);
|
|
58
|
+
return created;
|
|
59
|
+
}
|
|
60
|
+
function getStandardDtoBindingMap(metadata) {
|
|
61
|
+
const bag = getStandardMetadataBag(metadata);
|
|
62
|
+
const current = bag[standardDtoBindingMetadataKey];
|
|
63
|
+
if (current) {
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
const created = new Map();
|
|
67
|
+
bag[standardDtoBindingMetadataKey] = created;
|
|
68
|
+
return created;
|
|
69
|
+
}
|
|
70
|
+
function mergeStandardDtoBinding(metadata, propertyKey, partial) {
|
|
71
|
+
const map = getStandardDtoBindingMap(metadata);
|
|
72
|
+
map.set(propertyKey, {
|
|
73
|
+
...map.get(propertyKey),
|
|
74
|
+
...partial
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function createRouteDecorator(method) {
|
|
78
|
+
return path => {
|
|
79
|
+
validateRoutePath(path, `@${method}() path`);
|
|
80
|
+
const decorator = (_value, context) => {
|
|
81
|
+
const route = getStandardRouteRecord(context.metadata, context.name);
|
|
82
|
+
route.method = method;
|
|
83
|
+
route.path = path;
|
|
84
|
+
};
|
|
85
|
+
return decorator;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function createRouteValueDecorator(apply) {
|
|
89
|
+
return value => {
|
|
90
|
+
const decorator = (_target, context) => {
|
|
91
|
+
apply(getStandardRouteRecord(context.metadata, context.name), value);
|
|
92
|
+
};
|
|
93
|
+
return decorator;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function createDtoFieldDecorator(source) {
|
|
97
|
+
return key => {
|
|
98
|
+
const decorator = (_value, context) => {
|
|
99
|
+
mergeStandardDtoBinding(context.metadata, context.name, {
|
|
100
|
+
key,
|
|
101
|
+
source
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
return decorator;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Marks a class as an HTTP controller and defines its base route path.
|
|
110
|
+
*
|
|
111
|
+
* @param basePath Controller base path prefixed to every route declared on the class.
|
|
112
|
+
* @returns A class decorator that writes controller metadata for route mapping.
|
|
113
|
+
*/
|
|
114
|
+
export function Controller(basePath = '') {
|
|
115
|
+
validateRoutePath(basePath, '@Controller() base path');
|
|
116
|
+
const decorator = (_target, context) => {
|
|
117
|
+
getStandardControllerRecord(context.metadata).basePath = basePath;
|
|
118
|
+
};
|
|
119
|
+
return decorator;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Sets API version metadata on a controller or route handler.
|
|
124
|
+
*
|
|
125
|
+
* @param version Version label interpreted by runtime versioning strategy (for example `"1"`).
|
|
126
|
+
* @returns A decorator that applies version metadata at class or method scope.
|
|
127
|
+
*/
|
|
128
|
+
export function Version(version) {
|
|
129
|
+
const decorator = (_target, context) => {
|
|
130
|
+
if (context.kind === 'class') {
|
|
131
|
+
getStandardControllerRecord(context.metadata).version = version;
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
getStandardRouteRecord(context.metadata, context.name).version = version;
|
|
135
|
+
};
|
|
136
|
+
return decorator;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Registers a `GET` route handler.
|
|
141
|
+
*
|
|
142
|
+
* @param path Route path relative to the controller base path.
|
|
143
|
+
* @returns A method decorator that registers a `GET` handler mapping.
|
|
144
|
+
*/
|
|
145
|
+
export const Get = createRouteDecorator('GET');
|
|
146
|
+
/**
|
|
147
|
+
* Registers a `POST` route handler.
|
|
148
|
+
*
|
|
149
|
+
* @param path Route path relative to the controller base path.
|
|
150
|
+
* @returns A method decorator that registers a `POST` handler mapping.
|
|
151
|
+
*/
|
|
152
|
+
export const Post = createRouteDecorator('POST');
|
|
153
|
+
/**
|
|
154
|
+
* Registers a `PUT` route handler.
|
|
155
|
+
*
|
|
156
|
+
* @param path Route path relative to the controller base path.
|
|
157
|
+
* @returns A method decorator that registers a `PUT` handler mapping.
|
|
158
|
+
*/
|
|
159
|
+
export const Put = createRouteDecorator('PUT');
|
|
160
|
+
/**
|
|
161
|
+
* Registers a `PATCH` route handler.
|
|
162
|
+
*
|
|
163
|
+
* @param path Route path relative to the controller base path.
|
|
164
|
+
* @returns A method decorator that registers a `PATCH` handler mapping.
|
|
165
|
+
*/
|
|
166
|
+
export const Patch = createRouteDecorator('PATCH');
|
|
167
|
+
/**
|
|
168
|
+
* Registers a `DELETE` route handler.
|
|
169
|
+
*
|
|
170
|
+
* @param path Route path relative to the controller base path.
|
|
171
|
+
* @returns A method decorator that registers a `DELETE` handler mapping.
|
|
172
|
+
*/
|
|
173
|
+
export const Delete = createRouteDecorator('DELETE');
|
|
174
|
+
/**
|
|
175
|
+
* Registers an `OPTIONS` route handler.
|
|
176
|
+
*
|
|
177
|
+
* @param path Route path relative to the controller base path.
|
|
178
|
+
* @returns A method decorator that registers an `OPTIONS` handler mapping.
|
|
179
|
+
*/
|
|
180
|
+
export const Options = createRouteDecorator('OPTIONS');
|
|
181
|
+
/**
|
|
182
|
+
* Registers a `HEAD` route handler.
|
|
183
|
+
*
|
|
184
|
+
* @param path Route path relative to the controller base path.
|
|
185
|
+
* @returns A method decorator that registers a `HEAD` handler mapping.
|
|
186
|
+
*/
|
|
187
|
+
export const Head = createRouteDecorator('HEAD');
|
|
188
|
+
/**
|
|
189
|
+
* Registers a route handler that matches all HTTP methods.
|
|
190
|
+
*
|
|
191
|
+
* @param path Route path relative to the controller base path.
|
|
192
|
+
* @returns A method decorator that registers an all-method handler mapping.
|
|
193
|
+
*/
|
|
194
|
+
export const All = createRouteDecorator('ALL');
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Associates a DTO class used for request binding and validation.
|
|
198
|
+
*
|
|
199
|
+
* @param dto DTO class consumed by request binding and validation.
|
|
200
|
+
* @returns A method decorator that stores request DTO metadata for the route.
|
|
201
|
+
*/
|
|
202
|
+
export const RequestDto = createRouteValueDecorator((record, dto) => {
|
|
203
|
+
record.request = dto;
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Declares response media types produced by a route handler.
|
|
208
|
+
*
|
|
209
|
+
* @param mediaTypes One or more media type strings written into route metadata.
|
|
210
|
+
* @returns A method decorator that stores normalized `produces` metadata.
|
|
211
|
+
*/
|
|
212
|
+
export function Produces(...mediaTypes) {
|
|
213
|
+
return createRouteValueDecorator((record, value) => {
|
|
214
|
+
record.produces = normalizeProducesMediaTypes(value);
|
|
215
|
+
})(mediaTypes);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Overrides the default success status code for a route handler.
|
|
220
|
+
*
|
|
221
|
+
* @param status HTTP status code used when the route completes successfully.
|
|
222
|
+
* @returns A method decorator that stores the route-level success status override.
|
|
223
|
+
*/
|
|
224
|
+
export const HttpCode = createRouteValueDecorator((record, status) => {
|
|
225
|
+
record.successStatus = status;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Reads route-level `@Produces(...)` metadata from a controller method.
|
|
230
|
+
*
|
|
231
|
+
* @param controllerToken Controller class containing route metadata.
|
|
232
|
+
* @param propertyKey Controller method key to read.
|
|
233
|
+
* @returns A defensive copy of declared media types, or `undefined` when not configured.
|
|
234
|
+
*/
|
|
235
|
+
export function getRouteProducesMetadata(controllerToken, propertyKey) {
|
|
236
|
+
const bag = controllerToken[metadataSymbol];
|
|
237
|
+
const routeMap = bag?.[standardRouteMetadataKey];
|
|
238
|
+
const produces = routeMap?.get(propertyKey)?.produces;
|
|
239
|
+
return produces ? [...produces] : undefined;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Binds a DTO field from a path parameter.
|
|
244
|
+
*
|
|
245
|
+
* @param key Optional source key override. Defaults to the DTO field name.
|
|
246
|
+
* @returns A field decorator that marks the binding source as `path`.
|
|
247
|
+
*/
|
|
248
|
+
export const FromPath = createDtoFieldDecorator('path');
|
|
249
|
+
/**
|
|
250
|
+
* Binds a DTO field from query parameters.
|
|
251
|
+
*
|
|
252
|
+
* @param key Optional source key override. Defaults to the DTO field name.
|
|
253
|
+
* @returns A field decorator that marks the binding source as `query`.
|
|
254
|
+
*/
|
|
255
|
+
export const FromQuery = createDtoFieldDecorator('query');
|
|
256
|
+
/**
|
|
257
|
+
* Binds a DTO field from a request header.
|
|
258
|
+
*
|
|
259
|
+
* @param key Optional source key override. Defaults to the DTO field name.
|
|
260
|
+
* @returns A field decorator that marks the binding source as `header`.
|
|
261
|
+
*/
|
|
262
|
+
export const FromHeader = createDtoFieldDecorator('header');
|
|
263
|
+
/**
|
|
264
|
+
* Binds a DTO field from a cookie.
|
|
265
|
+
*
|
|
266
|
+
* @param key Optional source key override. Defaults to the DTO field name.
|
|
267
|
+
* @returns A field decorator that marks the binding source as `cookie`.
|
|
268
|
+
*/
|
|
269
|
+
export const FromCookie = createDtoFieldDecorator('cookie');
|
|
270
|
+
/**
|
|
271
|
+
* Binds a DTO field from the request body.
|
|
272
|
+
*
|
|
273
|
+
* @param key Optional source key override. Defaults to the DTO field name.
|
|
274
|
+
* @returns A field decorator that marks the binding source as `body`.
|
|
275
|
+
*/
|
|
276
|
+
export const FromBody = createDtoFieldDecorator('body');
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Marks a DTO field binding as optional.
|
|
280
|
+
*
|
|
281
|
+
* @returns A field decorator that marks the DTO binding as optional.
|
|
282
|
+
*/
|
|
283
|
+
export function Optional() {
|
|
284
|
+
const decorator = (_value, context) => {
|
|
285
|
+
mergeStandardDtoBinding(context.metadata, context.name, {
|
|
286
|
+
optional: true
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
return decorator;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Applies a field-level converter to a DTO binding.
|
|
294
|
+
*
|
|
295
|
+
* @param converter Converter instance or token resolved during request binding.
|
|
296
|
+
* @returns A field decorator that stores converter metadata for the DTO field.
|
|
297
|
+
*/
|
|
298
|
+
export function Convert(converter) {
|
|
299
|
+
const decorator = (_value, context) => {
|
|
300
|
+
mergeStandardDtoBinding(context.metadata, context.name, {
|
|
301
|
+
converter
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
return decorator;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Adds a static response header to the route metadata.
|
|
309
|
+
*
|
|
310
|
+
* @param name Response header name.
|
|
311
|
+
* @param value Static response header value applied by the dispatcher.
|
|
312
|
+
* @returns A method decorator that appends route-level response-header metadata.
|
|
313
|
+
*/
|
|
314
|
+
export function Header(name, value) {
|
|
315
|
+
const decorator = (_target, context) => {
|
|
316
|
+
const route = getStandardRouteRecord(context.metadata, context.name);
|
|
317
|
+
route.headers = [...(route.headers ?? []), {
|
|
318
|
+
name,
|
|
319
|
+
value
|
|
320
|
+
}];
|
|
321
|
+
};
|
|
322
|
+
return decorator;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Marks a route as a redirect with an optional status code.
|
|
327
|
+
*
|
|
328
|
+
* @param url Redirect target URL.
|
|
329
|
+
* @param statusCode Optional explicit redirect status code.
|
|
330
|
+
* @returns A method decorator that writes redirect metadata for the route.
|
|
331
|
+
*/
|
|
332
|
+
export function Redirect(url, statusCode) {
|
|
333
|
+
const decorator = (_target, context) => {
|
|
334
|
+
getStandardRouteRecord(context.metadata, context.name).redirect = {
|
|
335
|
+
url,
|
|
336
|
+
statusCode
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
return decorator;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Attaches guards to a controller or route handler.
|
|
344
|
+
*
|
|
345
|
+
* @param guards One or more guards merged into existing class- or route-level guard metadata.
|
|
346
|
+
* @returns A decorator applicable to classes and methods.
|
|
347
|
+
*/
|
|
348
|
+
export function UseGuards(...guards) {
|
|
349
|
+
const decorator = (_target, context) => {
|
|
350
|
+
if (context.kind === 'class') {
|
|
351
|
+
const controller = getStandardControllerRecord(context.metadata);
|
|
352
|
+
controller.guards = mergeUnique(controller.guards, guards);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const route = getStandardRouteRecord(context.metadata, context.name);
|
|
356
|
+
route.guards = mergeUnique(route.guards, guards);
|
|
357
|
+
};
|
|
358
|
+
return decorator;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Attaches interceptors to a controller or route handler.
|
|
363
|
+
*
|
|
364
|
+
* @param interceptors One or more interceptors merged into existing class- or route-level metadata.
|
|
365
|
+
* @returns A decorator applicable to classes and methods.
|
|
366
|
+
*/
|
|
367
|
+
export function UseInterceptors(...interceptors) {
|
|
368
|
+
const decorator = (_target, context) => {
|
|
369
|
+
if (context.kind === 'class') {
|
|
370
|
+
const controller = getStandardControllerRecord(context.metadata);
|
|
371
|
+
controller.interceptors = mergeUnique(controller.interceptors, interceptors);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const route = getStandardRouteRecord(context.metadata, context.name);
|
|
375
|
+
route.interceptors = mergeUnique(route.interceptors, interceptors);
|
|
376
|
+
};
|
|
377
|
+
return decorator;
|
|
378
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ContentNegotiationOptions, FrameworkRequest, HandlerDescriptor, ResponseFormatter } from '../types.js';
|
|
2
|
+
export interface ResolvedContentNegotiation {
|
|
3
|
+
defaultFormatter: ResponseFormatter;
|
|
4
|
+
formatters: ResponseFormatter[];
|
|
5
|
+
normalizedMediaTypes: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function resolveContentNegotiation(options: ContentNegotiationOptions | undefined): ResolvedContentNegotiation | undefined;
|
|
8
|
+
export declare function selectResponseFormatter(handler: HandlerDescriptor, request: FrameworkRequest, contentNegotiation: ResolvedContentNegotiation): ResponseFormatter;
|
|
9
|
+
//# sourceMappingURL=dispatch-content-negotiation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-content-negotiation.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-content-negotiation.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,yBAAyB,EACzB,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAQrB,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,iBAAiB,CAAC;IACpC,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAChC,oBAAoB,EAAE,MAAM,EAAE,CAAC;CAChC;AA2GD,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,yBAAyB,GAAG,SAAS,GAAG,0BAA0B,GAAG,SAAS,CA+BhI;AAqCD,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,gBAAgB,EACzB,kBAAkB,EAAE,0BAA0B,GAC7C,iBAAiB,CA2CnB"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { NotAcceptableException } from '../exceptions.js';
|
|
2
|
+
const NO_ACCEPTABLE_REPRESENTATION_MESSAGE = 'No acceptable response representation found.';
|
|
3
|
+
function normalizeMediaType(value) {
|
|
4
|
+
return value.split(';')[0]?.trim().toLowerCase() ?? '';
|
|
5
|
+
}
|
|
6
|
+
function readAcceptHeader(request) {
|
|
7
|
+
const raw = request.headers.accept ?? request.headers.Accept;
|
|
8
|
+
const value = Array.isArray(raw) ? raw.join(',') : raw;
|
|
9
|
+
const normalized = value?.trim();
|
|
10
|
+
return normalized ? normalized : undefined;
|
|
11
|
+
}
|
|
12
|
+
function parseQuality(value) {
|
|
13
|
+
if (!value) {
|
|
14
|
+
return 1;
|
|
15
|
+
}
|
|
16
|
+
const parsed = Number(value);
|
|
17
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
if (parsed > 1) {
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
function getMediaRangeSpecificity(mediaRange) {
|
|
26
|
+
if (mediaRange === '*/*') {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
if (mediaRange.endsWith('/*')) {
|
|
30
|
+
return 1;
|
|
31
|
+
}
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
34
|
+
function parseAcceptHeader(acceptHeader) {
|
|
35
|
+
const tokens = [];
|
|
36
|
+
for (const token of acceptHeader.split(',')) {
|
|
37
|
+
const [rawMediaRange, ...parameterParts] = token.trim().split(';');
|
|
38
|
+
const mediaRange = normalizeMediaType(rawMediaRange ?? '');
|
|
39
|
+
if (!mediaRange || !mediaRange.includes('/')) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
let quality = 1;
|
|
43
|
+
for (const parameterPart of parameterParts) {
|
|
44
|
+
const [name, value] = parameterPart.trim().split('=');
|
|
45
|
+
if (name?.toLowerCase() === 'q') {
|
|
46
|
+
quality = parseQuality(value?.trim());
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (quality <= 0) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
tokens.push({
|
|
54
|
+
mediaRange,
|
|
55
|
+
quality,
|
|
56
|
+
specificity: getMediaRangeSpecificity(mediaRange)
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return tokens.sort((left, right) => {
|
|
60
|
+
if (right.quality !== left.quality) {
|
|
61
|
+
return right.quality - left.quality;
|
|
62
|
+
}
|
|
63
|
+
return right.specificity - left.specificity;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function matchesMediaRange(mediaRange, mediaType) {
|
|
67
|
+
if (mediaRange === '*/*') {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
const [rangeType, rangeSubtype] = mediaRange.split('/');
|
|
71
|
+
const [mediaTypeType, mediaTypeSubtype] = mediaType.split('/');
|
|
72
|
+
if (!rangeType || !rangeSubtype || !mediaTypeType || !mediaTypeSubtype) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
if (rangeType !== '*' && rangeType !== mediaTypeType) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return rangeSubtype === '*' || rangeSubtype === mediaTypeSubtype;
|
|
79
|
+
}
|
|
80
|
+
export function resolveContentNegotiation(options) {
|
|
81
|
+
if (!options?.formatters?.length) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const formatters = options.formatters.filter(formatter => {
|
|
86
|
+
const mediaType = normalizeMediaType(formatter.mediaType);
|
|
87
|
+
if (!mediaType || seen.has(mediaType)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
seen.add(mediaType);
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
if (!formatters.length) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const defaultMediaType = normalizeMediaType(options.defaultMediaType ?? '');
|
|
97
|
+
const defaultFormatter = defaultMediaType ? formatters.find(formatter => normalizeMediaType(formatter.mediaType) === defaultMediaType) ?? formatters[0] : formatters[0];
|
|
98
|
+
return {
|
|
99
|
+
defaultFormatter,
|
|
100
|
+
formatters,
|
|
101
|
+
normalizedMediaTypes: formatters.map(f => normalizeMediaType(f.mediaType))
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function resolveAllowedFormatters(handler, contentNegotiation) {
|
|
105
|
+
if (!handler.route.produces?.length) {
|
|
106
|
+
return {
|
|
107
|
+
formatters: contentNegotiation.formatters,
|
|
108
|
+
normalizedMediaTypes: contentNegotiation.normalizedMediaTypes
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const allowed = new Set(handler.route.produces.map(mediaType => normalizeMediaType(mediaType)));
|
|
112
|
+
const formatters = [];
|
|
113
|
+
const normalizedMediaTypes = [];
|
|
114
|
+
for (let i = 0; i < contentNegotiation.formatters.length; i++) {
|
|
115
|
+
const normalized = contentNegotiation.normalizedMediaTypes[i];
|
|
116
|
+
if (allowed.has(normalized)) {
|
|
117
|
+
formatters.push(contentNegotiation.formatters[i]);
|
|
118
|
+
normalizedMediaTypes.push(normalized);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
formatters,
|
|
123
|
+
normalizedMediaTypes
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function resolveDefaultFormatter(allowedFormatters, allowedNormalizedMediaTypes, contentNegotiation) {
|
|
127
|
+
const defaultMediaType = normalizeMediaType(contentNegotiation.defaultFormatter.mediaType);
|
|
128
|
+
const idx = allowedNormalizedMediaTypes.indexOf(defaultMediaType);
|
|
129
|
+
return idx >= 0 ? allowedFormatters[idx] : allowedFormatters[0] ?? contentNegotiation.defaultFormatter;
|
|
130
|
+
}
|
|
131
|
+
export function selectResponseFormatter(handler, request, contentNegotiation) {
|
|
132
|
+
const {
|
|
133
|
+
formatters: allowedFormatters,
|
|
134
|
+
normalizedMediaTypes: allowedNormalizedMediaTypes
|
|
135
|
+
} = resolveAllowedFormatters(handler, contentNegotiation);
|
|
136
|
+
if (!allowedFormatters.length) {
|
|
137
|
+
throw new NotAcceptableException(NO_ACCEPTABLE_REPRESENTATION_MESSAGE);
|
|
138
|
+
}
|
|
139
|
+
const defaultFormatter = resolveDefaultFormatter(allowedFormatters, allowedNormalizedMediaTypes, contentNegotiation);
|
|
140
|
+
const acceptHeader = readAcceptHeader(request);
|
|
141
|
+
if (!acceptHeader) {
|
|
142
|
+
return defaultFormatter;
|
|
143
|
+
}
|
|
144
|
+
const acceptTokens = parseAcceptHeader(acceptHeader);
|
|
145
|
+
if (!acceptTokens.length) {
|
|
146
|
+
throw new NotAcceptableException(NO_ACCEPTABLE_REPRESENTATION_MESSAGE);
|
|
147
|
+
}
|
|
148
|
+
for (const token of acceptTokens) {
|
|
149
|
+
if (token.mediaRange === '*/*') {
|
|
150
|
+
return defaultFormatter;
|
|
151
|
+
}
|
|
152
|
+
let matchedFormatter;
|
|
153
|
+
for (let i = 0; i < allowedFormatters.length; i++) {
|
|
154
|
+
if (matchesMediaRange(token.mediaRange, allowedNormalizedMediaTypes[i])) {
|
|
155
|
+
matchedFormatter = allowedFormatters[i];
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (matchedFormatter) {
|
|
160
|
+
return matchedFormatter;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new NotAcceptableException(NO_ACCEPTABLE_REPRESENTATION_MESSAGE);
|
|
164
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-error-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-error-policy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAiBrD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,iBAAiB,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvH"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HandlerNotFoundError } from '../errors.js';
|
|
2
|
+
import { HttpException, InternalServerErrorException, NotFoundException, createErrorResponse } from '../exceptions.js';
|
|
3
|
+
function toHttpException(error) {
|
|
4
|
+
if (error instanceof HttpException) {
|
|
5
|
+
return error;
|
|
6
|
+
}
|
|
7
|
+
if (error instanceof HandlerNotFoundError) {
|
|
8
|
+
const message = error instanceof Error ? error.message : 'Resource not found.';
|
|
9
|
+
return new NotFoundException(message, {
|
|
10
|
+
cause: error
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
return new InternalServerErrorException('Internal server error.', {
|
|
14
|
+
cause: error
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function writeErrorResponse(error, response, requestId) {
|
|
18
|
+
if (response.committed) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const httpError = toHttpException(error);
|
|
22
|
+
response.setStatus(httpError.status);
|
|
23
|
+
await response.send(createErrorResponse(httpError, requestId));
|
|
24
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Binder, HandlerDescriptor, RequestContext } from '../types.js';
|
|
2
|
+
export declare function invokeControllerHandler(handler: HandlerDescriptor, requestContext: RequestContext, binder?: Binder): Promise<unknown>;
|
|
3
|
+
//# sourceMappingURL=dispatch-handler-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-handler-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-handler-policy.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAA2B,MAAM,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKtG,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,cAAc,EAAE,cAAc,EAC9B,MAAM,GAAE,MAAsB,GAC7B,OAAO,CAAC,OAAO,CAAC,CAuBlB"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { InvariantError } from '@fluojs/core';
|
|
2
|
+
import { DefaultBinder } from '../adapters/binding.js';
|
|
3
|
+
import { HttpDtoValidationAdapter } from '../adapters/dto-validation-adapter.js';
|
|
4
|
+
const defaultBinder = new DefaultBinder();
|
|
5
|
+
const defaultValidator = new HttpDtoValidationAdapter();
|
|
6
|
+
export async function invokeControllerHandler(handler, requestContext, binder = defaultBinder) {
|
|
7
|
+
const controller = await requestContext.container.resolve(handler.controllerToken);
|
|
8
|
+
const method = controller[handler.methodName];
|
|
9
|
+
if (typeof method !== 'function') {
|
|
10
|
+
throw new InvariantError(`Controller ${handler.controllerToken.name} does not expose handler method ${handler.methodName}.`);
|
|
11
|
+
}
|
|
12
|
+
const argumentResolverContext = {
|
|
13
|
+
handler,
|
|
14
|
+
requestContext
|
|
15
|
+
};
|
|
16
|
+
const input = handler.route.request ? await binder.bind(handler.route.request, argumentResolverContext) : undefined;
|
|
17
|
+
if (handler.route.request) {
|
|
18
|
+
await defaultValidator.validate(input, handler.route.request);
|
|
19
|
+
}
|
|
20
|
+
return method.call(controller, input, requestContext);
|
|
21
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { resolveContentNegotiation, type ResolvedContentNegotiation } from './dispatch-content-negotiation.js';
|
|
2
|
+
import { writeErrorResponse } from './dispatch-error-policy.js';
|
|
3
|
+
import type { FrameworkRequest, FrameworkResponse, HandlerDescriptor } from '../types.js';
|
|
4
|
+
export declare function writeSuccessResponse(handler: HandlerDescriptor, request: FrameworkRequest, response: FrameworkResponse, value: unknown, contentNegotiation: ResolvedContentNegotiation | undefined): Promise<void>;
|
|
5
|
+
export { resolveContentNegotiation, writeErrorResponse };
|
|
6
|
+
export type { ResolvedContentNegotiation };
|
|
7
|
+
//# sourceMappingURL=dispatch-response-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-response-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-response-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,yBAAyB,EAEzB,KAAK,0BAA0B,EAChC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAcrB,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,gBAAgB,EACzB,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,OAAO,EACd,kBAAkB,EAAE,0BAA0B,GAAG,SAAS,GACzD,OAAO,CAAC,IAAI,CAAC,CAoCf;AAED,OAAO,EAAE,yBAAyB,EAAE,kBAAkB,EAAE,CAAC;AACzD,YAAY,EAAE,0BAA0B,EAAE,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { resolveContentNegotiation, selectResponseFormatter } from './dispatch-content-negotiation.js';
|
|
2
|
+
import { writeErrorResponse } from './dispatch-error-policy.js';
|
|
3
|
+
function resolveDefaultSuccessStatus(handler, value) {
|
|
4
|
+
switch (handler.route.method) {
|
|
5
|
+
case 'POST':
|
|
6
|
+
return 201;
|
|
7
|
+
case 'DELETE':
|
|
8
|
+
case 'OPTIONS':
|
|
9
|
+
return value === undefined ? 204 : 200;
|
|
10
|
+
default:
|
|
11
|
+
return 200;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function writeSuccessResponse(handler, request, response, value, contentNegotiation) {
|
|
15
|
+
if (response.committed) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (handler.route.redirect) {
|
|
19
|
+
const {
|
|
20
|
+
url,
|
|
21
|
+
statusCode = 302
|
|
22
|
+
} = handler.route.redirect;
|
|
23
|
+
response.redirect(statusCode, url);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const formatter = contentNegotiation ? selectResponseFormatter(handler, request, contentNegotiation) : undefined;
|
|
27
|
+
|
|
28
|
+
// Write route-level headers only after successful formatter negotiation so
|
|
29
|
+
// that a negotiation failure does not leak success-only headers onto the
|
|
30
|
+
// error response.
|
|
31
|
+
for (const header of handler.route.headers ?? []) {
|
|
32
|
+
response.setHeader(header.name, header.value);
|
|
33
|
+
}
|
|
34
|
+
if (formatter) {
|
|
35
|
+
response.setHeader('Content-Type', formatter.mediaType);
|
|
36
|
+
}
|
|
37
|
+
if (handler.route.successStatus !== undefined) {
|
|
38
|
+
response.setStatus(handler.route.successStatus);
|
|
39
|
+
} else if (response.statusSet !== true) {
|
|
40
|
+
response.setStatus(resolveDefaultSuccessStatus(handler, value));
|
|
41
|
+
}
|
|
42
|
+
const responseBody = formatter ? formatter.format(value) : value;
|
|
43
|
+
await response.send(responseBody);
|
|
44
|
+
}
|
|
45
|
+
export { resolveContentNegotiation, writeErrorResponse };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FrameworkRequest, HandlerMapping, HandlerMatch, RequestContext } from '../types.js';
|
|
2
|
+
export declare function matchHandlerOrThrow(handlerMapping: HandlerMapping, request: FrameworkRequest): HandlerMatch;
|
|
3
|
+
export declare function updateRequestParams(requestContext: RequestContext, params: Readonly<Record<string, string>>): void;
|
|
4
|
+
//# sourceMappingURL=dispatch-routing-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch-routing-policy.d.ts","sourceRoot":"","sources":["../../src/dispatch/dispatch-routing-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElG,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,OAAO,EAAE,gBAAgB,GAAG,YAAY,CAQ3G;AAED,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,CAKlH"}
|