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
|
@@ -1,249 +1,249 @@
|
|
|
1
|
-
import type { Express, Request, Response, NextFunction } from "express";
|
|
2
|
-
import type { Constructor } from "../../core/types";
|
|
3
|
-
import type { SchemaSource } from "../../core/schema";
|
|
4
|
-
import { getControllerMeta } from "../../core/metadata";
|
|
5
|
-
import { isHttpError, type HttpError } from "../../core/errors";
|
|
6
|
-
import { isHttpResponse } from "../../core/response";
|
|
7
|
-
import type { InputCoercionSetting, MultipartOptions, RequestContext, ValidationOptions } from "./types";
|
|
8
|
-
import { createInputCoercer } from "./coercion";
|
|
9
|
-
import { serializeResponse } from "./response-serializer";
|
|
10
|
-
import {
|
|
11
|
-
createMultipartMiddleware,
|
|
12
|
-
extractFiles,
|
|
13
|
-
hasFileUploads,
|
|
14
|
-
normalizeMultipartOptions
|
|
15
|
-
} from "./multipart";
|
|
16
|
-
import { lifecycleRegistry } from "../../core/lifecycle";
|
|
17
|
-
import { createSseEmitter, createStreamWriter } from "../../core/streaming";
|
|
18
|
-
import { validate } from "../../core/validation";
|
|
19
|
-
import { ValidationErrors, isValidationErrors } from "../../core/validation-errors";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Attaches controllers to an Express application.
|
|
23
|
-
* @param app - Express application instance
|
|
24
|
-
* @param controllers - Array of controller classes
|
|
25
|
-
* @param inputCoercion - Input coercion setting
|
|
26
|
-
* @param multipart - Multipart file upload configuration
|
|
27
|
-
*/
|
|
28
|
-
export async function attachControllers(
|
|
29
|
-
app: Express,
|
|
30
|
-
controllers: Constructor[],
|
|
31
|
-
inputCoercion: InputCoercionSetting = "safe",
|
|
32
|
-
multipart?: boolean | MultipartOptions,
|
|
33
|
-
validation?: boolean | ValidationOptions
|
|
34
|
-
): Promise<void> {
|
|
35
|
-
const multipartOptions = normalizeMultipartOptions(multipart);
|
|
36
|
-
for (const controller of controllers) {
|
|
37
|
-
const meta = getControllerMeta(controller);
|
|
38
|
-
if (!meta) {
|
|
39
|
-
throw new Error(`Controller "${controller.name}" is missing @Controller decorator.`);
|
|
40
|
-
}
|
|
41
|
-
const instance = new controller();
|
|
42
|
-
lifecycleRegistry.register(instance);
|
|
43
|
-
await lifecycleRegistry.callOnModuleInit(instance);
|
|
44
|
-
for (const route of meta.routes) {
|
|
45
|
-
const path = joinPaths(meta.basePath, route.path);
|
|
46
|
-
const handler = instance[route.handlerName as keyof typeof instance];
|
|
47
|
-
if (typeof handler !== "function") {
|
|
48
|
-
throw new Error(`Handler "${String(route.handlerName)}" is not a function on ${controller.name}.`);
|
|
49
|
-
}
|
|
50
|
-
const coerceParams = inputCoercion === false
|
|
51
|
-
? undefined
|
|
52
|
-
: createInputCoercer<Record<string,
|
|
53
|
-
route.params,
|
|
54
|
-
{ mode: inputCoercion, location: "params" }
|
|
55
|
-
);
|
|
56
|
-
const coerceQuery = inputCoercion === false
|
|
57
|
-
? undefined
|
|
58
|
-
: createInputCoercer<Record<string, unknown>>(route.query, { mode: inputCoercion, location: "query" });
|
|
59
|
-
const coerceBody = inputCoercion === false
|
|
60
|
-
? undefined
|
|
61
|
-
: createInputCoercer<Record<string, unknown>>(route.body, { mode: inputCoercion, location: "body" });
|
|
62
|
-
|
|
63
|
-
// Build middleware chain
|
|
64
|
-
const middlewares: Array<(req: Request, res: Response, next: NextFunction) => void> = [];
|
|
65
|
-
|
|
66
|
-
// Add multipart middleware if route has file uploads
|
|
67
|
-
if (multipartOptions && hasFileUploads(route.files)) {
|
|
68
|
-
middlewares.push(createMultipartMiddleware(route.files!, multipartOptions));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Determine if validation is enabled for this route
|
|
72
|
-
const isValidationEnabled = validation !== false && (validation as ValidationOptions)?.enabled !== false;
|
|
73
|
-
|
|
74
|
-
// Main route handler
|
|
75
|
-
const routeHandler = async (req: Request, res: Response, next: NextFunction) => {
|
|
76
|
-
try {
|
|
77
|
-
const files = extractFiles(req);
|
|
78
|
-
|
|
79
|
-
// Create context
|
|
80
|
-
const ctx = {
|
|
81
|
-
req,
|
|
82
|
-
res,
|
|
83
|
-
body: coerceBody ? coerceBody(req.body) : req.body,
|
|
84
|
-
query: coerceQuery ? coerceQuery(req.query as Record<string, unknown>) : req.query,
|
|
85
|
-
params: coerceParams ? coerceParams(req.params) : req.params,
|
|
86
|
-
headers: req.headers,
|
|
87
|
-
files,
|
|
88
|
-
sse: route.sse ? createSseEmitter(res) : undefined,
|
|
89
|
-
stream: route.streaming || route.sse ? createStreamWriter(res) : undefined
|
|
90
|
-
} as unknown as RequestContext;
|
|
91
|
-
|
|
92
|
-
// Validate inputs if validation is enabled
|
|
93
|
-
if (isValidationEnabled) {
|
|
94
|
-
const validationErrors = [];
|
|
95
|
-
|
|
96
|
-
if (route.body) {
|
|
97
|
-
const bodyErrors = validate(ctx.body, route.body.schema);
|
|
98
|
-
validationErrors.push(...bodyErrors);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (route.query) {
|
|
102
|
-
const queryErrors = validate(ctx.query, route.query.schema);
|
|
103
|
-
validationErrors.push(...queryErrors);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (route.params) {
|
|
107
|
-
const paramsErrors = validate(ctx.params, route.params.schema);
|
|
108
|
-
validationErrors.push(...paramsErrors);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (route.headers) {
|
|
112
|
-
const headersErrors = validate(ctx.headers, route.headers.schema);
|
|
113
|
-
validationErrors.push(...headersErrors);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (validationErrors.length > 0) {
|
|
117
|
-
throw new ValidationErrors(validationErrors);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Call handler
|
|
122
|
-
const result = await handler.call(instance, ctx);
|
|
123
|
-
if (res.headersSent) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (isHttpResponse(result)) {
|
|
128
|
-
if (result.headers) {
|
|
129
|
-
for (const [key, value] of Object.entries(result.headers)) {
|
|
130
|
-
res.setHeader(key, value);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (result.body === undefined) {
|
|
134
|
-
res.status(result.status).end();
|
|
135
|
-
} else if (route.raw) {
|
|
136
|
-
if (!res.getHeader("Content-Type")) {
|
|
137
|
-
const ct = getResponseContentType(route) ?? "application/octet-stream";
|
|
138
|
-
res.type(ct);
|
|
139
|
-
}
|
|
140
|
-
res.status(result.status).send(result.body as any);
|
|
141
|
-
} else {
|
|
142
|
-
const responseSchema = getResponseSchemaForStatus(route, result.status);
|
|
143
|
-
const output = responseSchema ? serializeResponse(result.body, responseSchema) : result.body;
|
|
144
|
-
res.status(result.status).json(output);
|
|
145
|
-
}
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (result === undefined) {
|
|
150
|
-
res.status(defaultStatus(route)).end();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (route.raw) {
|
|
155
|
-
if (!res.getHeader("Content-Type")) {
|
|
156
|
-
const ct = getResponseContentType(route) ?? "application/octet-stream";
|
|
157
|
-
res.type(ct);
|
|
158
|
-
}
|
|
159
|
-
res.status(defaultStatus(route)).send(result as any);
|
|
160
|
-
} else {
|
|
161
|
-
const responseSchema = getResponseSchema(route);
|
|
162
|
-
const output = responseSchema ? serializeResponse(result, responseSchema) : result;
|
|
163
|
-
res.status(defaultStatus(route)).json(output);
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
if (isValidationErrors(error)) {
|
|
167
|
-
sendValidationError(res, error);
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
if (isHttpError(error)) {
|
|
171
|
-
sendHttpError(res, error);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
next(error);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
middlewares.push(routeHandler);
|
|
179
|
-
app[route.httpMethod](path, ...middlewares);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function defaultStatus(route: {
|
|
185
|
-
responses?: Array<{ status: number; error?: boolean }>;
|
|
186
|
-
}): number {
|
|
187
|
-
const responses = route.responses ?? [];
|
|
188
|
-
const success = responses.find(
|
|
189
|
-
(response) => !response.error && response.status < 400
|
|
190
|
-
);
|
|
191
|
-
return success?.status ?? 200;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function getResponseSchema(route: {
|
|
195
|
-
responses?: Array<{ status: number; error?: boolean; schema?: SchemaSource }>;
|
|
196
|
-
}): SchemaSource | undefined {
|
|
197
|
-
const responses = route.responses ?? [];
|
|
198
|
-
const success = responses.find((response) => !response.error && response.status < 400);
|
|
199
|
-
return success?.schema;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function getResponseContentType(route: {
|
|
203
|
-
responses?: Array<{ status: number; error?: boolean; contentType?: string }>;
|
|
204
|
-
}): string | undefined {
|
|
205
|
-
const responses = route.responses ?? [];
|
|
206
|
-
const success = responses.find((r) => !r.error && r.status < 400);
|
|
207
|
-
return success?.contentType;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function getResponseSchemaForStatus(
|
|
211
|
-
route: {
|
|
212
|
-
responses?: Array<{ status: number; error?: boolean; schema?: SchemaSource }>;
|
|
213
|
-
},
|
|
214
|
-
status: number
|
|
215
|
-
): SchemaSource | undefined {
|
|
216
|
-
const responses = route.responses ?? [];
|
|
217
|
-
const response = responses.find((r) => r.status === status);
|
|
218
|
-
return response?.schema;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function sendValidationError(res: Response, error: ValidationErrors): void {
|
|
222
|
-
if (res.headersSent) {
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
res.status(error.status).json(error.body);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function sendHttpError(res: Response, error: HttpError): void {
|
|
229
|
-
if (res.headersSent) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (error.headers) {
|
|
233
|
-
for (const [key, value] of Object.entries(error.headers)) {
|
|
234
|
-
res.setHeader(key, value);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
const body = error.body ?? { message: error.message };
|
|
238
|
-
if (body === undefined) {
|
|
239
|
-
res.status(error.status).end();
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
res.status(error.status).json(body);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function joinPaths(base: string, path: string): string {
|
|
246
|
-
const normalizedBase = base.replace(/\/+$/, "");
|
|
247
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
248
|
-
return `${normalizedBase}${normalizedPath}`;
|
|
249
|
-
}
|
|
1
|
+
import type { Express, Request, Response, NextFunction } from "express";
|
|
2
|
+
import type { Constructor } from "../../core/types";
|
|
3
|
+
import type { SchemaSource } from "../../core/schema";
|
|
4
|
+
import { getControllerMeta } from "../../core/metadata";
|
|
5
|
+
import { isHttpError, type HttpError } from "../../core/errors";
|
|
6
|
+
import { isHttpResponse } from "../../core/response";
|
|
7
|
+
import type { InputCoercionSetting, MultipartOptions, RequestContext, ValidationOptions } from "./types";
|
|
8
|
+
import { createInputCoercer } from "./coercion";
|
|
9
|
+
import { serializeResponse } from "./response-serializer";
|
|
10
|
+
import {
|
|
11
|
+
createMultipartMiddleware,
|
|
12
|
+
extractFiles,
|
|
13
|
+
hasFileUploads,
|
|
14
|
+
normalizeMultipartOptions
|
|
15
|
+
} from "./multipart";
|
|
16
|
+
import { lifecycleRegistry } from "../../core/lifecycle";
|
|
17
|
+
import { createSseEmitter, createStreamWriter } from "../../core/streaming";
|
|
18
|
+
import { validate } from "../../core/validation";
|
|
19
|
+
import { ValidationErrors, isValidationErrors } from "../../core/validation-errors";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Attaches controllers to an Express application.
|
|
23
|
+
* @param app - Express application instance
|
|
24
|
+
* @param controllers - Array of controller classes
|
|
25
|
+
* @param inputCoercion - Input coercion setting
|
|
26
|
+
* @param multipart - Multipart file upload configuration
|
|
27
|
+
*/
|
|
28
|
+
export async function attachControllers(
|
|
29
|
+
app: Express,
|
|
30
|
+
controllers: Constructor[],
|
|
31
|
+
inputCoercion: InputCoercionSetting = "safe",
|
|
32
|
+
multipart?: boolean | MultipartOptions,
|
|
33
|
+
validation?: boolean | ValidationOptions
|
|
34
|
+
): Promise<void> {
|
|
35
|
+
const multipartOptions = normalizeMultipartOptions(multipart);
|
|
36
|
+
for (const controller of controllers) {
|
|
37
|
+
const meta = getControllerMeta(controller);
|
|
38
|
+
if (!meta) {
|
|
39
|
+
throw new Error(`Controller "${controller.name}" is missing @Controller decorator.`);
|
|
40
|
+
}
|
|
41
|
+
const instance = new controller();
|
|
42
|
+
lifecycleRegistry.register(instance);
|
|
43
|
+
await lifecycleRegistry.callOnModuleInit(instance);
|
|
44
|
+
for (const route of meta.routes) {
|
|
45
|
+
const path = joinPaths(meta.basePath, route.path);
|
|
46
|
+
const handler = instance[route.handlerName as keyof typeof instance];
|
|
47
|
+
if (typeof handler !== "function") {
|
|
48
|
+
throw new Error(`Handler "${String(route.handlerName)}" is not a function on ${controller.name}.`);
|
|
49
|
+
}
|
|
50
|
+
const coerceParams = inputCoercion === false
|
|
51
|
+
? undefined
|
|
52
|
+
: createInputCoercer<Record<string, unknown>>(
|
|
53
|
+
route.params,
|
|
54
|
+
{ mode: inputCoercion, location: "params" }
|
|
55
|
+
);
|
|
56
|
+
const coerceQuery = inputCoercion === false
|
|
57
|
+
? undefined
|
|
58
|
+
: createInputCoercer<Record<string, unknown>>(route.query, { mode: inputCoercion, location: "query" });
|
|
59
|
+
const coerceBody = inputCoercion === false
|
|
60
|
+
? undefined
|
|
61
|
+
: createInputCoercer<Record<string, unknown>>(route.body, { mode: inputCoercion, location: "body" });
|
|
62
|
+
|
|
63
|
+
// Build middleware chain
|
|
64
|
+
const middlewares: Array<(req: Request, res: Response, next: NextFunction) => void> = [];
|
|
65
|
+
|
|
66
|
+
// Add multipart middleware if route has file uploads
|
|
67
|
+
if (multipartOptions && hasFileUploads(route.files)) {
|
|
68
|
+
middlewares.push(createMultipartMiddleware(route.files!, multipartOptions));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Determine if validation is enabled for this route
|
|
72
|
+
const isValidationEnabled = validation !== false && (validation as ValidationOptions)?.enabled !== false;
|
|
73
|
+
|
|
74
|
+
// Main route handler
|
|
75
|
+
const routeHandler = async (req: Request, res: Response, next: NextFunction) => {
|
|
76
|
+
try {
|
|
77
|
+
const files = extractFiles(req);
|
|
78
|
+
|
|
79
|
+
// Create context
|
|
80
|
+
const ctx = {
|
|
81
|
+
req,
|
|
82
|
+
res,
|
|
83
|
+
body: coerceBody ? coerceBody(req.body) : req.body,
|
|
84
|
+
query: coerceQuery ? coerceQuery(req.query as Record<string, unknown>) : req.query,
|
|
85
|
+
params: coerceParams ? coerceParams(req.params as Record<string, unknown>) : req.params,
|
|
86
|
+
headers: req.headers,
|
|
87
|
+
files,
|
|
88
|
+
sse: route.sse ? createSseEmitter(res) : undefined,
|
|
89
|
+
stream: route.streaming || route.sse ? createStreamWriter(res) : undefined
|
|
90
|
+
} as unknown as RequestContext;
|
|
91
|
+
|
|
92
|
+
// Validate inputs if validation is enabled
|
|
93
|
+
if (isValidationEnabled) {
|
|
94
|
+
const validationErrors = [];
|
|
95
|
+
|
|
96
|
+
if (route.body) {
|
|
97
|
+
const bodyErrors = validate(ctx.body, route.body.schema);
|
|
98
|
+
validationErrors.push(...bodyErrors);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (route.query) {
|
|
102
|
+
const queryErrors = validate(ctx.query, route.query.schema);
|
|
103
|
+
validationErrors.push(...queryErrors);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (route.params) {
|
|
107
|
+
const paramsErrors = validate(ctx.params, route.params.schema);
|
|
108
|
+
validationErrors.push(...paramsErrors);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (route.headers) {
|
|
112
|
+
const headersErrors = validate(ctx.headers, route.headers.schema);
|
|
113
|
+
validationErrors.push(...headersErrors);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (validationErrors.length > 0) {
|
|
117
|
+
throw new ValidationErrors(validationErrors);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Call handler
|
|
122
|
+
const result = await handler.call(instance, ctx);
|
|
123
|
+
if (res.headersSent) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (isHttpResponse(result)) {
|
|
128
|
+
if (result.headers) {
|
|
129
|
+
for (const [key, value] of Object.entries(result.headers)) {
|
|
130
|
+
res.setHeader(key, value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (result.body === undefined) {
|
|
134
|
+
res.status(result.status).end();
|
|
135
|
+
} else if (route.raw) {
|
|
136
|
+
if (!res.getHeader("Content-Type")) {
|
|
137
|
+
const ct = getResponseContentType(route) ?? "application/octet-stream";
|
|
138
|
+
res.type(ct);
|
|
139
|
+
}
|
|
140
|
+
res.status(result.status).send(result.body as any);
|
|
141
|
+
} else {
|
|
142
|
+
const responseSchema = getResponseSchemaForStatus(route, result.status);
|
|
143
|
+
const output = responseSchema ? serializeResponse(result.body, responseSchema) : result.body;
|
|
144
|
+
res.status(result.status).json(output);
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (result === undefined) {
|
|
150
|
+
res.status(defaultStatus(route)).end();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (route.raw) {
|
|
155
|
+
if (!res.getHeader("Content-Type")) {
|
|
156
|
+
const ct = getResponseContentType(route) ?? "application/octet-stream";
|
|
157
|
+
res.type(ct);
|
|
158
|
+
}
|
|
159
|
+
res.status(defaultStatus(route)).send(result as any);
|
|
160
|
+
} else {
|
|
161
|
+
const responseSchema = getResponseSchema(route);
|
|
162
|
+
const output = responseSchema ? serializeResponse(result, responseSchema) : result;
|
|
163
|
+
res.status(defaultStatus(route)).json(output);
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
if (isValidationErrors(error)) {
|
|
167
|
+
sendValidationError(res, error);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (isHttpError(error)) {
|
|
171
|
+
sendHttpError(res, error);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
next(error);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
middlewares.push(routeHandler);
|
|
179
|
+
app[route.httpMethod](path, ...middlewares);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function defaultStatus(route: {
|
|
185
|
+
responses?: Array<{ status: number; error?: boolean }>;
|
|
186
|
+
}): number {
|
|
187
|
+
const responses = route.responses ?? [];
|
|
188
|
+
const success = responses.find(
|
|
189
|
+
(response) => !response.error && response.status < 400
|
|
190
|
+
);
|
|
191
|
+
return success?.status ?? 200;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getResponseSchema(route: {
|
|
195
|
+
responses?: Array<{ status: number; error?: boolean; schema?: SchemaSource }>;
|
|
196
|
+
}): SchemaSource | undefined {
|
|
197
|
+
const responses = route.responses ?? [];
|
|
198
|
+
const success = responses.find((response) => !response.error && response.status < 400);
|
|
199
|
+
return success?.schema;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getResponseContentType(route: {
|
|
203
|
+
responses?: Array<{ status: number; error?: boolean; contentType?: string }>;
|
|
204
|
+
}): string | undefined {
|
|
205
|
+
const responses = route.responses ?? [];
|
|
206
|
+
const success = responses.find((r) => !r.error && r.status < 400);
|
|
207
|
+
return success?.contentType;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getResponseSchemaForStatus(
|
|
211
|
+
route: {
|
|
212
|
+
responses?: Array<{ status: number; error?: boolean; schema?: SchemaSource }>;
|
|
213
|
+
},
|
|
214
|
+
status: number
|
|
215
|
+
): SchemaSource | undefined {
|
|
216
|
+
const responses = route.responses ?? [];
|
|
217
|
+
const response = responses.find((r) => r.status === status);
|
|
218
|
+
return response?.schema;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sendValidationError(res: Response, error: ValidationErrors): void {
|
|
222
|
+
if (res.headersSent) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
res.status(error.status).json(error.body);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function sendHttpError(res: Response, error: HttpError): void {
|
|
229
|
+
if (res.headersSent) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (error.headers) {
|
|
233
|
+
for (const [key, value] of Object.entries(error.headers)) {
|
|
234
|
+
res.setHeader(key, value);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const body = error.body ?? { message: error.message };
|
|
238
|
+
if (body === undefined) {
|
|
239
|
+
res.status(error.status).end();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
res.status(error.status).json(body);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function joinPaths(base: string, path: string): string {
|
|
246
|
+
const normalizedBase = base.replace(/\/+$/, "");
|
|
247
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
248
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
249
|
+
}
|