adorn-api 1.1.11 → 1.1.13
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 +18 -0
- package/dist/adapter/express/types.d.ts +3 -46
- package/dist/adapter/fastify/coercion.d.ts +12 -0
- package/dist/adapter/fastify/coercion.js +289 -0
- package/dist/adapter/fastify/controllers.d.ts +7 -0
- package/dist/adapter/fastify/controllers.js +201 -0
- package/dist/adapter/fastify/index.d.ts +14 -0
- package/dist/adapter/fastify/index.js +67 -0
- package/dist/adapter/fastify/multipart.d.ts +26 -0
- package/dist/adapter/fastify/multipart.js +75 -0
- package/dist/adapter/fastify/openapi.d.ts +10 -0
- package/dist/adapter/fastify/openapi.js +76 -0
- package/dist/adapter/fastify/response-serializer.d.ts +2 -0
- package/dist/adapter/fastify/response-serializer.js +162 -0
- package/dist/adapter/fastify/types.d.ts +100 -0
- package/dist/adapter/fastify/types.js +2 -0
- package/dist/adapter/metal-orm/index.d.ts +1 -1
- package/dist/adapter/metal-orm/types.d.ts +23 -0
- package/dist/adapter/native/coercion.d.ts +12 -0
- package/dist/adapter/native/coercion.js +289 -0
- package/dist/adapter/native/controllers.d.ts +17 -0
- package/dist/adapter/native/controllers.js +215 -0
- package/dist/adapter/native/index.d.ts +14 -0
- package/dist/adapter/native/index.js +127 -0
- package/dist/adapter/native/openapi.d.ts +7 -0
- package/dist/adapter/native/openapi.js +82 -0
- package/dist/adapter/native/response-serializer.d.ts +5 -0
- package/dist/adapter/native/response-serializer.js +160 -0
- package/dist/adapter/native/router.d.ts +25 -0
- package/dist/adapter/native/router.js +68 -0
- package/dist/adapter/native/types.d.ts +77 -0
- package/dist/adapter/native/types.js +2 -0
- package/dist/core/auth.d.ts +11 -12
- package/dist/core/auth.js +2 -2
- package/dist/core/logger.d.ts +3 -4
- package/dist/core/logger.js +2 -2
- package/dist/core/streaming.d.ts +10 -10
- package/dist/core/streaming.js +31 -19
- package/dist/core/types.d.ts +102 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +16 -1
- package/examples/fastify/app.ts +16 -0
- package/examples/fastify/index.ts +21 -0
- package/package.json +24 -18
- package/src/adapter/express/controllers.ts +249 -249
- package/src/adapter/express/types.ts +121 -160
- package/src/adapter/fastify/coercion.ts +369 -0
- package/src/adapter/fastify/controllers.ts +255 -0
- package/src/adapter/fastify/index.ts +53 -0
- package/src/adapter/fastify/multipart.ts +94 -0
- package/src/adapter/fastify/openapi.ts +93 -0
- package/src/adapter/fastify/response-serializer.ts +179 -0
- package/src/adapter/fastify/types.ts +119 -0
- package/src/adapter/metal-orm/index.ts +3 -0
- package/src/adapter/metal-orm/types.ts +25 -0
- package/src/adapter/native/coercion.ts +369 -0
- package/src/adapter/native/controllers.ts +271 -0
- package/src/adapter/native/index.ts +116 -0
- package/src/adapter/native/openapi.ts +109 -0
- package/src/adapter/native/response-serializer.ts +177 -0
- package/src/adapter/native/router.ts +90 -0
- package/src/adapter/native/types.ts +96 -0
- package/src/core/auth.ts +314 -315
- package/src/core/health.ts +234 -235
- package/src/core/logger.ts +245 -247
- package/src/core/streaming.ts +342 -330
- package/src/core/types.ts +115 -0
- package/src/index.ts +46 -16
- package/tests/e2e/fastify.e2e.test.ts +174 -0
- package/tests/native.test.ts +191 -0
- package/tests/typecheck/query-params.typecheck.ts +42 -0
- package/tests/unit/openapi-parameters.test.ts +97 -97
- package/tsconfig.json +14 -13
- package/tsconfig.typecheck.json +8 -0
- package/vitest.config.ts +47 -7
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createInputCoercer = createInputCoercer;
|
|
4
|
+
const metadata_1 = require("../../core/metadata");
|
|
5
|
+
const coerce_1 = require("../../core/coerce");
|
|
6
|
+
const errors_1 = require("../../core/errors");
|
|
7
|
+
/**
|
|
8
|
+
* Creates an input coercer function for the given input metadata.
|
|
9
|
+
*/
|
|
10
|
+
function createInputCoercer(input, options) {
|
|
11
|
+
if (!input) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const fields = extractFields(input.schema);
|
|
15
|
+
if (!fields.length) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return (value) => {
|
|
19
|
+
const result = coerceRecord(value, fields, options.mode);
|
|
20
|
+
if (options.mode === "strict" && result.invalidFields.length) {
|
|
21
|
+
throw new errors_1.HttpError(400, buildInvalidMessage(options.location, result.invalidFields));
|
|
22
|
+
}
|
|
23
|
+
return result.value;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function coerceRecord(value, fields, mode) {
|
|
27
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
28
|
+
return { value, invalidFields: [] };
|
|
29
|
+
}
|
|
30
|
+
const input = value;
|
|
31
|
+
let changed = false;
|
|
32
|
+
const output = { ...input };
|
|
33
|
+
const invalidFields = [];
|
|
34
|
+
for (const field of fields) {
|
|
35
|
+
if (!(field.name in input)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const original = input[field.name];
|
|
39
|
+
const result = coerceValue(original, field.schema, mode);
|
|
40
|
+
if (!result.ok && mode === "strict") {
|
|
41
|
+
invalidFields.push(field.name);
|
|
42
|
+
}
|
|
43
|
+
if (result.changed) {
|
|
44
|
+
output[field.name] = result.value;
|
|
45
|
+
changed = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { value: changed ? output : value, invalidFields };
|
|
49
|
+
}
|
|
50
|
+
function coerceValue(value, schema, mode) {
|
|
51
|
+
switch (schema.kind) {
|
|
52
|
+
case "integer":
|
|
53
|
+
return coerceNumber(value, schema, true);
|
|
54
|
+
case "number":
|
|
55
|
+
return coerceNumber(value, schema, false);
|
|
56
|
+
case "boolean": {
|
|
57
|
+
return coerceBoolean(value);
|
|
58
|
+
}
|
|
59
|
+
case "string": {
|
|
60
|
+
return coerceString(value, schema);
|
|
61
|
+
}
|
|
62
|
+
case "array":
|
|
63
|
+
return coerceArrayValue(value, schema, mode);
|
|
64
|
+
case "object":
|
|
65
|
+
return coerceObjectValue(value, schema, mode);
|
|
66
|
+
case "record":
|
|
67
|
+
return coerceRecordValue(value, schema, mode);
|
|
68
|
+
case "ref":
|
|
69
|
+
return coerceRefValue(value, schema, mode);
|
|
70
|
+
case "union":
|
|
71
|
+
return coerceUnionValue(value, schema, mode);
|
|
72
|
+
default:
|
|
73
|
+
return { value, ok: true, changed: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function coerceNumber(value, schema, integer) {
|
|
77
|
+
if (!isPresent(value)) {
|
|
78
|
+
return { value, ok: true, changed: false };
|
|
79
|
+
}
|
|
80
|
+
const parsed = integer
|
|
81
|
+
? coerce_1.coerce.integer(value, { min: schema.minimum, max: schema.maximum })
|
|
82
|
+
: coerce_1.coerce.number(value, { min: schema.minimum, max: schema.maximum });
|
|
83
|
+
if (parsed === undefined) {
|
|
84
|
+
return { value, ok: false, changed: false };
|
|
85
|
+
}
|
|
86
|
+
if (schema.exclusiveMinimum !== undefined && parsed <= schema.exclusiveMinimum) {
|
|
87
|
+
return { value, ok: false, changed: false };
|
|
88
|
+
}
|
|
89
|
+
if (schema.exclusiveMaximum !== undefined && parsed >= schema.exclusiveMaximum) {
|
|
90
|
+
return { value, ok: false, changed: false };
|
|
91
|
+
}
|
|
92
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
93
|
+
}
|
|
94
|
+
function coerceBoolean(value) {
|
|
95
|
+
if (!isPresent(value)) {
|
|
96
|
+
return { value, ok: true, changed: false };
|
|
97
|
+
}
|
|
98
|
+
const parsed = coerce_1.coerce.boolean(value);
|
|
99
|
+
if (parsed === undefined) {
|
|
100
|
+
return { value, ok: false, changed: false };
|
|
101
|
+
}
|
|
102
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
103
|
+
}
|
|
104
|
+
function coerceString(value, schema) {
|
|
105
|
+
if (!isPresent(value)) {
|
|
106
|
+
return { value, ok: true, changed: false };
|
|
107
|
+
}
|
|
108
|
+
if (schema.format === "date" || schema.format === "date-time") {
|
|
109
|
+
const parsed = parseDateValue(value);
|
|
110
|
+
if (!parsed) {
|
|
111
|
+
return { value, ok: false, changed: false };
|
|
112
|
+
}
|
|
113
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
114
|
+
}
|
|
115
|
+
const parsed = coerce_1.coerce.string(value);
|
|
116
|
+
if (parsed === undefined) {
|
|
117
|
+
return { value, ok: true, changed: false };
|
|
118
|
+
}
|
|
119
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
120
|
+
}
|
|
121
|
+
function parseDateValue(value) {
|
|
122
|
+
if (value instanceof Date) {
|
|
123
|
+
return Number.isNaN(value.getTime()) ? undefined : value;
|
|
124
|
+
}
|
|
125
|
+
const text = coerce_1.coerce.string(value);
|
|
126
|
+
if (text === undefined) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const parsed = new Date(text);
|
|
130
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return parsed;
|
|
134
|
+
}
|
|
135
|
+
function coerceArrayValue(value, schema, mode) {
|
|
136
|
+
if (value === undefined || value === null) {
|
|
137
|
+
return { value, ok: true, changed: false };
|
|
138
|
+
}
|
|
139
|
+
let input;
|
|
140
|
+
let changed;
|
|
141
|
+
if (Array.isArray(value)) {
|
|
142
|
+
input = value;
|
|
143
|
+
changed = false;
|
|
144
|
+
}
|
|
145
|
+
else if (typeof value === "string" && value.includes(",")) {
|
|
146
|
+
input = value.split(",").map((s) => s.trim());
|
|
147
|
+
changed = true;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
input = [value];
|
|
151
|
+
changed = true;
|
|
152
|
+
}
|
|
153
|
+
let ok = true;
|
|
154
|
+
const output = input.map((entry) => {
|
|
155
|
+
const result = coerceValue(entry, schema.items, mode);
|
|
156
|
+
if (!result.ok) {
|
|
157
|
+
ok = false;
|
|
158
|
+
}
|
|
159
|
+
if (result.changed) {
|
|
160
|
+
changed = true;
|
|
161
|
+
}
|
|
162
|
+
return result.value;
|
|
163
|
+
});
|
|
164
|
+
return { value: changed ? output : value, ok, changed };
|
|
165
|
+
}
|
|
166
|
+
function coerceObjectValue(value, schema, mode) {
|
|
167
|
+
if (value === undefined || value === null) {
|
|
168
|
+
return { value, ok: true, changed: false };
|
|
169
|
+
}
|
|
170
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
171
|
+
return { value, ok: mode === "safe", changed: false };
|
|
172
|
+
}
|
|
173
|
+
const properties = schema.properties ?? {};
|
|
174
|
+
const fields = Object.entries(properties).map(([name, fieldSchema]) => ({
|
|
175
|
+
name,
|
|
176
|
+
schema: fieldSchema
|
|
177
|
+
}));
|
|
178
|
+
if (!fields.length) {
|
|
179
|
+
return { value, ok: true, changed: false };
|
|
180
|
+
}
|
|
181
|
+
const result = coerceRecord(value, fields, mode);
|
|
182
|
+
return {
|
|
183
|
+
value: result.value,
|
|
184
|
+
ok: result.invalidFields.length === 0,
|
|
185
|
+
changed: result.value !== value
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function coerceRecordValue(value, schema, mode) {
|
|
189
|
+
if (value === undefined || value === null) {
|
|
190
|
+
return { value, ok: true, changed: false };
|
|
191
|
+
}
|
|
192
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
193
|
+
return { value, ok: mode === "safe", changed: false };
|
|
194
|
+
}
|
|
195
|
+
const input = value;
|
|
196
|
+
let changed = false;
|
|
197
|
+
let ok = true;
|
|
198
|
+
const output = { ...input };
|
|
199
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
200
|
+
const result = coerceValue(entry, schema.values, mode);
|
|
201
|
+
if (!result.ok) {
|
|
202
|
+
ok = false;
|
|
203
|
+
}
|
|
204
|
+
if (result.changed) {
|
|
205
|
+
output[key] = result.value;
|
|
206
|
+
changed = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { value: changed ? output : value, ok, changed };
|
|
210
|
+
}
|
|
211
|
+
function coerceRefValue(value, schema, mode) {
|
|
212
|
+
if (value === undefined || value === null) {
|
|
213
|
+
return { value, ok: true, changed: false };
|
|
214
|
+
}
|
|
215
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
216
|
+
return { value, ok: mode === "safe", changed: false };
|
|
217
|
+
}
|
|
218
|
+
const meta = getDtoMetaSafe(schema.dto);
|
|
219
|
+
const fields = Object.entries(meta.fields).map(([name, field]) => ({
|
|
220
|
+
name,
|
|
221
|
+
schema: field.schema
|
|
222
|
+
}));
|
|
223
|
+
if (!fields.length) {
|
|
224
|
+
return { value, ok: true, changed: false };
|
|
225
|
+
}
|
|
226
|
+
const result = coerceRecord(value, fields, mode);
|
|
227
|
+
return {
|
|
228
|
+
value: result.value,
|
|
229
|
+
ok: result.invalidFields.length === 0,
|
|
230
|
+
changed: result.value !== value
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function coerceUnionValue(value, schema, mode) {
|
|
234
|
+
let fallback;
|
|
235
|
+
for (const option of schema.anyOf) {
|
|
236
|
+
const result = coerceValue(value, option, mode);
|
|
237
|
+
if (!result.ok) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (result.changed) {
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
fallback ??= result;
|
|
244
|
+
}
|
|
245
|
+
if (fallback) {
|
|
246
|
+
return fallback;
|
|
247
|
+
}
|
|
248
|
+
return { value, ok: mode === "safe", changed: false };
|
|
249
|
+
}
|
|
250
|
+
function extractFields(schema) {
|
|
251
|
+
if (isSchemaNode(schema)) {
|
|
252
|
+
if (schema.kind === "object" && schema.properties) {
|
|
253
|
+
return Object.entries(schema.properties).map(([name, fieldSchema]) => ({
|
|
254
|
+
name,
|
|
255
|
+
schema: fieldSchema
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
const meta = getDtoMetaSafe(schema);
|
|
261
|
+
return Object.entries(meta.fields).map(([name, field]) => ({
|
|
262
|
+
name,
|
|
263
|
+
schema: field.schema
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
function getDtoMetaSafe(dto) {
|
|
267
|
+
const meta = (0, metadata_1.getDtoMeta)(dto);
|
|
268
|
+
if (!meta) {
|
|
269
|
+
throw new Error(`DTO "${dto.name}" is missing @Dto decorator.`);
|
|
270
|
+
}
|
|
271
|
+
return meta;
|
|
272
|
+
}
|
|
273
|
+
function isSchemaNode(value) {
|
|
274
|
+
return !!value && typeof value === "object" && "kind" in value;
|
|
275
|
+
}
|
|
276
|
+
function isPresent(value) {
|
|
277
|
+
return coerce_1.coerce.string(value) !== undefined;
|
|
278
|
+
}
|
|
279
|
+
function buildInvalidMessage(location, fields) {
|
|
280
|
+
let label = "query parameter";
|
|
281
|
+
if (location === "params") {
|
|
282
|
+
label = "path parameter";
|
|
283
|
+
}
|
|
284
|
+
else if (location === "body") {
|
|
285
|
+
label = "request body field";
|
|
286
|
+
}
|
|
287
|
+
const suffix = fields.length > 1 ? "s" : "";
|
|
288
|
+
return `Invalid ${label}${suffix}: ${fields.join(", ")}.`;
|
|
289
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { Constructor } from "../../core/types";
|
|
3
|
+
import type { InputCoercionSetting, ValidationOptions } from "./types";
|
|
4
|
+
import { Router } from "./router";
|
|
5
|
+
/**
|
|
6
|
+
* Registers controllers with a native application router.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerControllers(router: Router, controllers: Constructor[]): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Dispatches a native request to the appropriate controller handler.
|
|
11
|
+
*/
|
|
12
|
+
export declare function dispatchRequest(req: IncomingMessage, res: ServerResponse, match: any, options: {
|
|
13
|
+
inputCoercion: InputCoercionSetting;
|
|
14
|
+
validation?: boolean | ValidationOptions;
|
|
15
|
+
body?: any;
|
|
16
|
+
query?: Record<string, any>;
|
|
17
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerControllers = registerControllers;
|
|
4
|
+
exports.dispatchRequest = dispatchRequest;
|
|
5
|
+
const metadata_1 = require("../../core/metadata");
|
|
6
|
+
const auth_1 = require("../../core/auth");
|
|
7
|
+
const errors_1 = require("../../core/errors");
|
|
8
|
+
const response_1 = require("../../core/response");
|
|
9
|
+
const coercion_1 = require("./coercion");
|
|
10
|
+
const response_serializer_1 = require("./response-serializer");
|
|
11
|
+
const lifecycle_1 = require("../../core/lifecycle");
|
|
12
|
+
const streaming_1 = require("../../core/streaming");
|
|
13
|
+
const validation_1 = require("../../core/validation");
|
|
14
|
+
const validation_errors_1 = require("../../core/validation-errors");
|
|
15
|
+
/**
|
|
16
|
+
* Registers controllers with a native application router.
|
|
17
|
+
*/
|
|
18
|
+
async function registerControllers(router, controllers) {
|
|
19
|
+
for (const controller of controllers) {
|
|
20
|
+
const meta = (0, metadata_1.getControllerMeta)(controller);
|
|
21
|
+
if (!meta) {
|
|
22
|
+
throw new Error(`Controller "${controller.name}" is missing @Controller decorator.`);
|
|
23
|
+
}
|
|
24
|
+
const instance = new controller();
|
|
25
|
+
lifecycle_1.lifecycleRegistry.register(instance);
|
|
26
|
+
await lifecycle_1.lifecycleRegistry.callOnModuleInit(instance);
|
|
27
|
+
for (const route of meta.routes) {
|
|
28
|
+
router.add(instance, route, meta.basePath);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Dispatches a native request to the appropriate controller handler.
|
|
34
|
+
*/
|
|
35
|
+
async function dispatchRequest(req, res, match, options) {
|
|
36
|
+
const { controller: instance, route, params: rawParams } = match;
|
|
37
|
+
const { inputCoercion, validation, body: rawBody, query: rawQuery } = options;
|
|
38
|
+
const handler = instance[route.handlerName];
|
|
39
|
+
if (typeof handler !== "function") {
|
|
40
|
+
throw new Error(`Handler "${String(route.handlerName)}" is not a function.`);
|
|
41
|
+
}
|
|
42
|
+
const coerceParams = inputCoercion === false
|
|
43
|
+
? undefined
|
|
44
|
+
: (0, coercion_1.createInputCoercer)(route.params, { mode: inputCoercion, location: "params" });
|
|
45
|
+
const coerceQuery = inputCoercion === false
|
|
46
|
+
? undefined
|
|
47
|
+
: (0, coercion_1.createInputCoercer)(route.query, { mode: inputCoercion, location: "query" });
|
|
48
|
+
const coerceBody = inputCoercion === false
|
|
49
|
+
? undefined
|
|
50
|
+
: (0, coercion_1.createInputCoercer)(route.body, { mode: inputCoercion, location: "body" });
|
|
51
|
+
const isValidationEnabled = validation !== false && validation?.enabled !== false;
|
|
52
|
+
const authMeta = (0, auth_1.getRouteAuthMeta)(instance.constructor, route.handlerName);
|
|
53
|
+
try {
|
|
54
|
+
// Apply auth guard if metadata exists
|
|
55
|
+
if (authMeta && authMeta.requiresAuth && !authMeta.isPublic) {
|
|
56
|
+
const user = req.user;
|
|
57
|
+
if (!user) {
|
|
58
|
+
throw new errors_1.HttpError(401, "Unauthorized");
|
|
59
|
+
}
|
|
60
|
+
if (authMeta.roles?.length) {
|
|
61
|
+
const hasRole = authMeta.roles.some((role) => user.roles?.includes(role));
|
|
62
|
+
if (!hasRole) {
|
|
63
|
+
throw new errors_1.HttpError(403, "Insufficient permissions");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (authMeta.allRoles?.length) {
|
|
67
|
+
const hasAllRoles = authMeta.allRoles.every((role) => user.roles?.includes(role));
|
|
68
|
+
if (!hasAllRoles) {
|
|
69
|
+
throw new errors_1.HttpError(403, "Insufficient permissions");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (authMeta.guard) {
|
|
73
|
+
const allowed = await authMeta.guard(user, req);
|
|
74
|
+
if (!allowed) {
|
|
75
|
+
throw new errors_1.HttpError(403, "Access denied by guard");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const body = (coerceBody && rawBody) ? coerceBody(rawBody) : rawBody;
|
|
80
|
+
const query = (coerceQuery && rawQuery) ? coerceQuery(rawQuery) : rawQuery;
|
|
81
|
+
const params = (coerceParams && rawParams) ? coerceParams(rawParams) : rawParams;
|
|
82
|
+
if (isValidationEnabled) {
|
|
83
|
+
const validationErrors = [];
|
|
84
|
+
if (route.body) {
|
|
85
|
+
const bodyErrors = (0, validation_1.validate)(body, route.body.schema);
|
|
86
|
+
validationErrors.push(...bodyErrors);
|
|
87
|
+
}
|
|
88
|
+
if (route.query) {
|
|
89
|
+
const queryErrors = (0, validation_1.validate)(query, route.query.schema);
|
|
90
|
+
validationErrors.push(...queryErrors);
|
|
91
|
+
}
|
|
92
|
+
if (route.params) {
|
|
93
|
+
const paramsErrors = (0, validation_1.validate)(params, route.params.schema);
|
|
94
|
+
validationErrors.push(...paramsErrors);
|
|
95
|
+
}
|
|
96
|
+
if (route.headers) {
|
|
97
|
+
const headersErrors = (0, validation_1.validate)(req.headers, route.headers.schema);
|
|
98
|
+
validationErrors.push(...headersErrors);
|
|
99
|
+
}
|
|
100
|
+
if (validationErrors.length > 0) {
|
|
101
|
+
throw new validation_errors_1.ValidationErrors(validationErrors);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const ctx = {
|
|
105
|
+
req,
|
|
106
|
+
res,
|
|
107
|
+
body,
|
|
108
|
+
query,
|
|
109
|
+
params,
|
|
110
|
+
headers: req.headers,
|
|
111
|
+
files: undefined, // Native adapter doesn't support multipart yet
|
|
112
|
+
sse: route.sse ? (0, streaming_1.createSseEmitter)(res) : undefined,
|
|
113
|
+
stream: route.streaming || route.sse ? (0, streaming_1.createStreamWriter)(res) : undefined
|
|
114
|
+
};
|
|
115
|
+
const result = await handler.call(instance, ctx);
|
|
116
|
+
if (res.writableEnded || route.sse || route.streaming) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if ((0, response_1.isHttpResponse)(result)) {
|
|
120
|
+
if (result.headers) {
|
|
121
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
122
|
+
if (value !== undefined) {
|
|
123
|
+
res.setHeader(key, value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (result.body === undefined) {
|
|
128
|
+
res.statusCode = result.status;
|
|
129
|
+
res.end();
|
|
130
|
+
}
|
|
131
|
+
else if (route.raw) {
|
|
132
|
+
if (!res.getHeader("Content-Type")) {
|
|
133
|
+
const ct = getResponseContentType(route) ?? "application/octet-stream";
|
|
134
|
+
res.setHeader("Content-Type", ct);
|
|
135
|
+
}
|
|
136
|
+
res.statusCode = result.status;
|
|
137
|
+
res.end(result.body);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const responseSchema = getResponseSchemaForStatus(route, result.status);
|
|
141
|
+
const output = responseSchema ? (0, response_serializer_1.serializeResponse)(result.body, responseSchema) : result.body;
|
|
142
|
+
res.statusCode = result.status;
|
|
143
|
+
res.setHeader("Content-Type", "application/json");
|
|
144
|
+
res.end(JSON.stringify(output));
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (result === undefined) {
|
|
149
|
+
res.statusCode = defaultStatus(route);
|
|
150
|
+
res.end();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (route.raw) {
|
|
154
|
+
if (!res.getHeader("Content-Type")) {
|
|
155
|
+
const ct = getResponseContentType(route) ?? "application/octet-stream";
|
|
156
|
+
res.setHeader("Content-Type", ct);
|
|
157
|
+
}
|
|
158
|
+
res.statusCode = defaultStatus(route);
|
|
159
|
+
res.end(result);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const responseSchema = getResponseSchema(route);
|
|
163
|
+
const output = responseSchema ? (0, response_serializer_1.serializeResponse)(result, responseSchema) : result;
|
|
164
|
+
res.statusCode = defaultStatus(route);
|
|
165
|
+
res.setHeader("Content-Type", "application/json");
|
|
166
|
+
res.end(JSON.stringify(output));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if ((0, validation_errors_1.isValidationErrors)(error)) {
|
|
171
|
+
res.statusCode = error.status;
|
|
172
|
+
res.setHeader("Content-Type", "application/json");
|
|
173
|
+
res.end(JSON.stringify(error.body));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if ((0, errors_1.isHttpError)(error)) {
|
|
177
|
+
if (error.headers) {
|
|
178
|
+
for (const [key, value] of Object.entries(error.headers)) {
|
|
179
|
+
if (value !== undefined) {
|
|
180
|
+
res.setHeader(key, value);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const body = error.body ?? { message: error.message };
|
|
185
|
+
res.statusCode = error.status;
|
|
186
|
+
res.setHeader("Content-Type", "application/json");
|
|
187
|
+
res.end(JSON.stringify(body));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
console.error("Unhandled error:", error);
|
|
191
|
+
res.statusCode = 500;
|
|
192
|
+
res.setHeader("Content-Type", "application/json");
|
|
193
|
+
res.end(JSON.stringify({ message: "Internal server error" }));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function defaultStatus(route) {
|
|
197
|
+
const responses = route.responses ?? [];
|
|
198
|
+
const success = responses.find((response) => !response.error && response.status < 400);
|
|
199
|
+
return success?.status ?? 200;
|
|
200
|
+
}
|
|
201
|
+
function getResponseSchema(route) {
|
|
202
|
+
const responses = route.responses ?? [];
|
|
203
|
+
const success = responses.find((response) => !response.error && response.status < 400);
|
|
204
|
+
return success?.schema;
|
|
205
|
+
}
|
|
206
|
+
function getResponseContentType(route) {
|
|
207
|
+
const responses = route.responses ?? [];
|
|
208
|
+
const success = responses.find((r) => !r.error && r.status < 400);
|
|
209
|
+
return success?.contentType;
|
|
210
|
+
}
|
|
211
|
+
function getResponseSchemaForStatus(route, status) {
|
|
212
|
+
const responses = route.responses ?? [];
|
|
213
|
+
const response = responses.find((r) => r.status === status);
|
|
214
|
+
return response?.schema;
|
|
215
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NativeAdapterOptions, NativeApp } from "./types";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export { registerControllers as attachControllers } from "./controllers";
|
|
4
|
+
export { registerOpenApi as attachOpenApi } from "./openapi";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a native Node.js application with Adorn controllers.
|
|
7
|
+
* @param options - Native adapter options
|
|
8
|
+
* @returns Native application object
|
|
9
|
+
*/
|
|
10
|
+
export declare function createNativeApp(options: NativeAdapterOptions): Promise<NativeApp>;
|
|
11
|
+
/**
|
|
12
|
+
* Trigger shutdown hooks for graceful application shutdown.
|
|
13
|
+
*/
|
|
14
|
+
export declare function shutdownApp(signal?: string): Promise<void>;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.attachOpenApi = exports.attachControllers = void 0;
|
|
18
|
+
exports.createNativeApp = createNativeApp;
|
|
19
|
+
exports.shutdownApp = shutdownApp;
|
|
20
|
+
const node_http_1 = require("node:http");
|
|
21
|
+
const controllers_1 = require("./controllers");
|
|
22
|
+
const openapi_1 = require("./openapi");
|
|
23
|
+
const lifecycle_1 = require("../../core/lifecycle");
|
|
24
|
+
const router_1 = require("./router");
|
|
25
|
+
__exportStar(require("./types"), exports);
|
|
26
|
+
var controllers_2 = require("./controllers");
|
|
27
|
+
Object.defineProperty(exports, "attachControllers", { enumerable: true, get: function () { return controllers_2.registerControllers; } });
|
|
28
|
+
var openapi_2 = require("./openapi");
|
|
29
|
+
Object.defineProperty(exports, "attachOpenApi", { enumerable: true, get: function () { return openapi_2.registerOpenApi; } });
|
|
30
|
+
/**
|
|
31
|
+
* Creates a native Node.js application with Adorn controllers.
|
|
32
|
+
* @param options - Native adapter options
|
|
33
|
+
* @returns Native application object
|
|
34
|
+
*/
|
|
35
|
+
async function createNativeApp(options) {
|
|
36
|
+
const router = new router_1.Router();
|
|
37
|
+
const inputCoercion = options.inputCoercion ?? "safe";
|
|
38
|
+
await (0, controllers_1.registerControllers)(router, options.controllers);
|
|
39
|
+
if (options.openApi) {
|
|
40
|
+
(0, openapi_1.registerOpenApi)(router, options.controllers, options.openApi);
|
|
41
|
+
}
|
|
42
|
+
const handle = async (req, res) => {
|
|
43
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
44
|
+
const match = router.match(req.method || "GET", url.pathname);
|
|
45
|
+
if (!match) {
|
|
46
|
+
res.statusCode = 404;
|
|
47
|
+
res.setHeader("Content-Type", "application/json");
|
|
48
|
+
res.end(JSON.stringify({ message: `Not Found: ${req.method} ${url.pathname}` }));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const query = {};
|
|
52
|
+
url.searchParams.forEach((value, key) => {
|
|
53
|
+
if (query[key]) {
|
|
54
|
+
if (Array.isArray(query[key])) {
|
|
55
|
+
query[key].push(value);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
query[key] = [query[key], value];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
query[key] = value;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
let body = undefined;
|
|
66
|
+
if (options.jsonBody !== false && (req.method === "POST" || req.method === "PUT" || req.method === "PATCH")) {
|
|
67
|
+
try {
|
|
68
|
+
body = await parseJsonBody(req, options.bodyLimit);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
res.statusCode = 400;
|
|
72
|
+
res.setHeader("Content-Type", "application/json");
|
|
73
|
+
res.end(JSON.stringify({ message: "Invalid JSON body" }));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
await (0, controllers_1.dispatchRequest)(req, res, match, {
|
|
78
|
+
inputCoercion,
|
|
79
|
+
validation: options.validation,
|
|
80
|
+
body,
|
|
81
|
+
query
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
await lifecycle_1.lifecycleRegistry.callOnApplicationBootstrap();
|
|
85
|
+
return {
|
|
86
|
+
handle,
|
|
87
|
+
listen: (port, callback) => {
|
|
88
|
+
const server = (0, node_http_1.createServer)(handle);
|
|
89
|
+
return server.listen(port, callback);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Trigger shutdown hooks for graceful application shutdown.
|
|
95
|
+
*/
|
|
96
|
+
async function shutdownApp(signal) {
|
|
97
|
+
await lifecycle_1.lifecycleRegistry.callShutdownHooks(signal);
|
|
98
|
+
lifecycle_1.lifecycleRegistry.clear();
|
|
99
|
+
}
|
|
100
|
+
async function parseJsonBody(req, limit) {
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
let data = "";
|
|
103
|
+
let size = 0;
|
|
104
|
+
req.on("data", (chunk) => {
|
|
105
|
+
data += chunk;
|
|
106
|
+
size += chunk.length;
|
|
107
|
+
if (limit && size > limit) {
|
|
108
|
+
reject(new Error("Body too large"));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
req.on("end", () => {
|
|
112
|
+
if (!data) {
|
|
113
|
+
resolve(undefined);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
resolve(JSON.parse(data));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
reject(error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
req.on("error", (err) => {
|
|
124
|
+
reject(err);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Constructor } from "../../core/types";
|
|
2
|
+
import type { OpenApiNativeOptions } from "./types";
|
|
3
|
+
import type { Router } from "./router";
|
|
4
|
+
/**
|
|
5
|
+
* Registers OpenAPI endpoints with a native application router.
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerOpenApi(router: Router, controllers: Constructor[], options: OpenApiNativeOptions): void;
|