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.
Files changed (75) hide show
  1. package/README.md +18 -0
  2. package/dist/adapter/express/types.d.ts +3 -46
  3. package/dist/adapter/fastify/coercion.d.ts +12 -0
  4. package/dist/adapter/fastify/coercion.js +289 -0
  5. package/dist/adapter/fastify/controllers.d.ts +7 -0
  6. package/dist/adapter/fastify/controllers.js +201 -0
  7. package/dist/adapter/fastify/index.d.ts +14 -0
  8. package/dist/adapter/fastify/index.js +67 -0
  9. package/dist/adapter/fastify/multipart.d.ts +26 -0
  10. package/dist/adapter/fastify/multipart.js +75 -0
  11. package/dist/adapter/fastify/openapi.d.ts +10 -0
  12. package/dist/adapter/fastify/openapi.js +76 -0
  13. package/dist/adapter/fastify/response-serializer.d.ts +2 -0
  14. package/dist/adapter/fastify/response-serializer.js +162 -0
  15. package/dist/adapter/fastify/types.d.ts +100 -0
  16. package/dist/adapter/fastify/types.js +2 -0
  17. package/dist/adapter/metal-orm/index.d.ts +1 -1
  18. package/dist/adapter/metal-orm/types.d.ts +23 -0
  19. package/dist/adapter/native/coercion.d.ts +12 -0
  20. package/dist/adapter/native/coercion.js +289 -0
  21. package/dist/adapter/native/controllers.d.ts +17 -0
  22. package/dist/adapter/native/controllers.js +215 -0
  23. package/dist/adapter/native/index.d.ts +14 -0
  24. package/dist/adapter/native/index.js +127 -0
  25. package/dist/adapter/native/openapi.d.ts +7 -0
  26. package/dist/adapter/native/openapi.js +82 -0
  27. package/dist/adapter/native/response-serializer.d.ts +5 -0
  28. package/dist/adapter/native/response-serializer.js +160 -0
  29. package/dist/adapter/native/router.d.ts +25 -0
  30. package/dist/adapter/native/router.js +68 -0
  31. package/dist/adapter/native/types.d.ts +77 -0
  32. package/dist/adapter/native/types.js +2 -0
  33. package/dist/core/auth.d.ts +11 -12
  34. package/dist/core/auth.js +2 -2
  35. package/dist/core/logger.d.ts +3 -4
  36. package/dist/core/logger.js +2 -2
  37. package/dist/core/streaming.d.ts +10 -10
  38. package/dist/core/streaming.js +31 -19
  39. package/dist/core/types.d.ts +102 -0
  40. package/dist/index.d.ts +6 -1
  41. package/dist/index.js +16 -1
  42. package/examples/fastify/app.ts +16 -0
  43. package/examples/fastify/index.ts +21 -0
  44. package/package.json +24 -18
  45. package/src/adapter/express/controllers.ts +249 -249
  46. package/src/adapter/express/types.ts +121 -160
  47. package/src/adapter/fastify/coercion.ts +369 -0
  48. package/src/adapter/fastify/controllers.ts +255 -0
  49. package/src/adapter/fastify/index.ts +53 -0
  50. package/src/adapter/fastify/multipart.ts +94 -0
  51. package/src/adapter/fastify/openapi.ts +93 -0
  52. package/src/adapter/fastify/response-serializer.ts +179 -0
  53. package/src/adapter/fastify/types.ts +119 -0
  54. package/src/adapter/metal-orm/index.ts +3 -0
  55. package/src/adapter/metal-orm/types.ts +25 -0
  56. package/src/adapter/native/coercion.ts +369 -0
  57. package/src/adapter/native/controllers.ts +271 -0
  58. package/src/adapter/native/index.ts +116 -0
  59. package/src/adapter/native/openapi.ts +109 -0
  60. package/src/adapter/native/response-serializer.ts +177 -0
  61. package/src/adapter/native/router.ts +90 -0
  62. package/src/adapter/native/types.ts +96 -0
  63. package/src/core/auth.ts +314 -315
  64. package/src/core/health.ts +234 -235
  65. package/src/core/logger.ts +245 -247
  66. package/src/core/streaming.ts +342 -330
  67. package/src/core/types.ts +115 -0
  68. package/src/index.ts +46 -16
  69. package/tests/e2e/fastify.e2e.test.ts +174 -0
  70. package/tests/native.test.ts +191 -0
  71. package/tests/typecheck/query-params.typecheck.ts +42 -0
  72. package/tests/unit/openapi-parameters.test.ts +97 -97
  73. package/tsconfig.json +14 -13
  74. package/tsconfig.typecheck.json +8 -0
  75. 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, string | number | boolean | undefined>>(
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
+ }