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
package/README.md
CHANGED
|
@@ -639,6 +639,24 @@ export class UserController {
|
|
|
639
639
|
|
|
640
640
|
The `listConfig` object contains: `filterMappings`, `sortableColumns`, `defaultSortBy`, `defaultSortDirection`, `defaultPageSize`, `maxPageSize`, `sortByKey`, and `sortDirectionKey`.
|
|
641
641
|
|
|
642
|
+
### Type-Only Query Interfaces
|
|
643
|
+
|
|
644
|
+
For consumer projects that need pure TypeScript interfaces (without creating extra DTO classes), Adorn exports:
|
|
645
|
+
- `PaginationQueryParams`
|
|
646
|
+
- `SortingQueryParams`
|
|
647
|
+
- `PagedQueryParams`
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
import type { PagedQueryParams } from "adorn-api";
|
|
651
|
+
|
|
652
|
+
interface CaixaEntradaQueryDto extends PagedQueryParams {
|
|
653
|
+
usuarioId?: number;
|
|
654
|
+
lido?: boolean;
|
|
655
|
+
}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
`sortDirection` is the official typed sort field (`"asc" | "desc"`). `sortOrder` remains parser compatibility for legacy/external clients.
|
|
659
|
+
|
|
642
660
|
### `BaseService.list` Before/After (Boilerplate Reduction)
|
|
643
661
|
|
|
644
662
|
Before:
|
|
@@ -1,53 +1,10 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { Constructor } from "../../core/types";
|
|
1
|
+
import type { Constructor, RequestContext as CoreRequestContext, UploadedFileInfo } from "../../core/types";
|
|
3
2
|
import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Uploaded file information from multipart form data.
|
|
7
|
-
*/
|
|
8
|
-
export interface UploadedFileInfo {
|
|
9
|
-
/** Original filename as provided by the client */
|
|
10
|
-
originalName: string;
|
|
11
|
-
/** MIME type of the file */
|
|
12
|
-
mimeType: string;
|
|
13
|
-
/** Size of the file in bytes */
|
|
14
|
-
size: number;
|
|
15
|
-
/** File buffer (when using memory storage) */
|
|
16
|
-
buffer?: Buffer;
|
|
17
|
-
/** Path to the file on disk (when using disk storage) */
|
|
18
|
-
path?: string;
|
|
19
|
-
/** Field name from the form */
|
|
20
|
-
fieldName: string;
|
|
21
|
-
}
|
|
3
|
+
export { UploadedFileInfo };
|
|
22
4
|
/**
|
|
23
5
|
* Request context provided to route handlers.
|
|
24
6
|
*/
|
|
25
|
-
export
|
|
26
|
-
/** Express request object */
|
|
27
|
-
req: Request;
|
|
28
|
-
/** Express response object */
|
|
29
|
-
res: Response;
|
|
30
|
-
/** Parsed request body */
|
|
31
|
-
body: TBody;
|
|
32
|
-
/** Parsed query parameters */
|
|
33
|
-
query: TQuery;
|
|
34
|
-
/** Parsed path parameters */
|
|
35
|
-
params: TParams;
|
|
36
|
-
/** Request headers */
|
|
37
|
-
headers: THeaders;
|
|
38
|
-
/** Uploaded files (when using multipart handling) */
|
|
39
|
-
files: TFiles;
|
|
40
|
-
/**
|
|
41
|
-
* Server-Sent Events emitter for streaming events to client.
|
|
42
|
-
* Only available on routes marked with @Sse decorator.
|
|
43
|
-
*/
|
|
44
|
-
sse?: SseEmitter;
|
|
45
|
-
/**
|
|
46
|
-
* Stream writer for streaming responses.
|
|
47
|
-
* Available on routes marked with @Streaming or @Sse decorator.
|
|
48
|
-
*/
|
|
49
|
-
stream?: StreamWriter;
|
|
50
|
-
}
|
|
7
|
+
export type RequestContext<TBody = any, TQuery extends object | undefined = Record<string, any>, TParams extends object | undefined = Record<string, any>, THeaders extends object | undefined = Record<string, any>, TFiles extends Record<string, UploadedFileInfo | UploadedFileInfo[]> | undefined = any> = CoreRequestContext<TBody, TQuery, TParams, THeaders, TFiles>;
|
|
51
8
|
/**
|
|
52
9
|
* Input coercion modes.
|
|
53
10
|
*/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type InputMeta } from "../../core/metadata";
|
|
2
|
+
import type { InputCoercionMode } from "./types";
|
|
3
|
+
export type InputLocation = "params" | "query" | "body";
|
|
4
|
+
interface CoerceInputOptions {
|
|
5
|
+
mode: InputCoercionMode;
|
|
6
|
+
location: InputLocation;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Creates an input coercer function for the given input metadata.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createInputCoercer<T extends Record<string, unknown> = Record<string, unknown>>(input: InputMeta | undefined, options: CoerceInputOptions): ((value: T) => T) | undefined;
|
|
12
|
+
export {};
|
|
@@ -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,7 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { Constructor } from "../../core/types";
|
|
3
|
+
import type { InputCoercionSetting, MultipartOptions, ValidationOptions } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* Attaches controllers to a Fastify application.
|
|
6
|
+
*/
|
|
7
|
+
export declare function attachControllers(app: FastifyInstance, controllers: Constructor[], inputCoercion?: InputCoercionSetting, multipart?: boolean | MultipartOptions, validation?: boolean | ValidationOptions): Promise<void>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachControllers = attachControllers;
|
|
4
|
+
const metadata_1 = require("../../core/metadata");
|
|
5
|
+
const auth_1 = require("../../core/auth");
|
|
6
|
+
const errors_1 = require("../../core/errors");
|
|
7
|
+
const response_1 = require("../../core/response");
|
|
8
|
+
const coercion_1 = require("./coercion");
|
|
9
|
+
const response_serializer_1 = require("./response-serializer");
|
|
10
|
+
const multipart_1 = require("./multipart");
|
|
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
|
+
* Attaches controllers to a Fastify application.
|
|
17
|
+
*/
|
|
18
|
+
async function attachControllers(app, controllers, inputCoercion = "safe", multipart, validation) {
|
|
19
|
+
const multipartOptions = (0, multipart_1.normalizeMultipartOptions)(multipart);
|
|
20
|
+
for (const controller of controllers) {
|
|
21
|
+
const meta = (0, metadata_1.getControllerMeta)(controller);
|
|
22
|
+
if (!meta) {
|
|
23
|
+
throw new Error(`Controller "${controller.name}" is missing @Controller decorator.`);
|
|
24
|
+
}
|
|
25
|
+
const instance = new controller();
|
|
26
|
+
lifecycle_1.lifecycleRegistry.register(instance);
|
|
27
|
+
await lifecycle_1.lifecycleRegistry.callOnModuleInit(instance);
|
|
28
|
+
for (const route of meta.routes) {
|
|
29
|
+
const path = joinPaths(meta.basePath, route.path);
|
|
30
|
+
const handler = instance[route.handlerName];
|
|
31
|
+
if (typeof handler !== "function") {
|
|
32
|
+
throw new Error(`Handler "${String(route.handlerName)}" is not a function on ${controller.name}.`);
|
|
33
|
+
}
|
|
34
|
+
const coerceParams = inputCoercion === false
|
|
35
|
+
? undefined
|
|
36
|
+
: (0, coercion_1.createInputCoercer)(route.params, { mode: inputCoercion, location: "params" });
|
|
37
|
+
const coerceQuery = inputCoercion === false
|
|
38
|
+
? undefined
|
|
39
|
+
: (0, coercion_1.createInputCoercer)(route.query, { mode: inputCoercion, location: "query" });
|
|
40
|
+
const coerceBody = inputCoercion === false
|
|
41
|
+
? undefined
|
|
42
|
+
: (0, coercion_1.createInputCoercer)(route.body, { mode: inputCoercion, location: "body" });
|
|
43
|
+
const isValidationEnabled = validation !== false && validation?.enabled !== false;
|
|
44
|
+
const authMeta = (0, auth_1.getRouteAuthMeta)(controller, route.handlerName);
|
|
45
|
+
app.route({
|
|
46
|
+
method: route.httpMethod.toUpperCase(),
|
|
47
|
+
url: path,
|
|
48
|
+
handler: async (req, reply) => {
|
|
49
|
+
try {
|
|
50
|
+
// Apply auth guard if metadata exists
|
|
51
|
+
if (authMeta && authMeta.requiresAuth && !authMeta.isPublic) {
|
|
52
|
+
const user = req.user || req.raw.user;
|
|
53
|
+
if (!user) {
|
|
54
|
+
throw new errors_1.HttpError(401, "Unauthorized");
|
|
55
|
+
}
|
|
56
|
+
if (authMeta.roles?.length) {
|
|
57
|
+
const hasRole = authMeta.roles.some((role) => user.roles?.includes(role));
|
|
58
|
+
if (!hasRole) {
|
|
59
|
+
throw new errors_1.HttpError(403, "Insufficient permissions");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (authMeta.allRoles?.length) {
|
|
63
|
+
const hasAllRoles = authMeta.allRoles.every((role) => user.roles?.includes(role));
|
|
64
|
+
if (!hasAllRoles) {
|
|
65
|
+
throw new errors_1.HttpError(403, "Insufficient permissions");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (authMeta.guard) {
|
|
69
|
+
const allowed = await authMeta.guard(user, req);
|
|
70
|
+
if (!allowed) {
|
|
71
|
+
throw new errors_1.HttpError(403, "Access denied by guard");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let files = undefined;
|
|
76
|
+
if (multipartOptions && (0, multipart_1.hasFileUploads)(route.files)) {
|
|
77
|
+
files = await (0, multipart_1.extractFiles)(req);
|
|
78
|
+
}
|
|
79
|
+
const body = req.body;
|
|
80
|
+
const query = (coerceQuery && req.query && Object.keys(req.query).length > 0) ? coerceQuery(req.query) : req.query;
|
|
81
|
+
const params = (coerceParams && req.params && Object.keys(req.params).length > 0) ? coerceParams(req.params) : req.params;
|
|
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: reply.raw,
|
|
107
|
+
body,
|
|
108
|
+
query,
|
|
109
|
+
params,
|
|
110
|
+
headers: req.headers,
|
|
111
|
+
files,
|
|
112
|
+
sse: route.sse ? (0, streaming_1.createSseEmitter)(reply.raw) : undefined,
|
|
113
|
+
stream: route.streaming || route.sse ? (0, streaming_1.createStreamWriter)(reply.raw) : undefined
|
|
114
|
+
};
|
|
115
|
+
const result = await handler.call(instance, ctx);
|
|
116
|
+
if (reply.sent || route.sse || route.streaming) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if ((0, response_1.isHttpResponse)(result)) {
|
|
120
|
+
if (result.headers) {
|
|
121
|
+
reply.headers(result.headers);
|
|
122
|
+
}
|
|
123
|
+
if (result.body === undefined) {
|
|
124
|
+
reply.status(result.status).send();
|
|
125
|
+
}
|
|
126
|
+
else if (route.raw) {
|
|
127
|
+
if (!reply.getHeader("Content-Type")) {
|
|
128
|
+
const ct = getResponseContentType(route) ?? "application/octet-stream";
|
|
129
|
+
reply.type(ct);
|
|
130
|
+
}
|
|
131
|
+
reply.status(result.status).send(result.body);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const responseSchema = getResponseSchemaForStatus(route, result.status);
|
|
135
|
+
const output = responseSchema ? (0, response_serializer_1.serializeResponse)(result.body, responseSchema) : result.body;
|
|
136
|
+
reply.status(result.status).send(output);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (result === undefined) {
|
|
141
|
+
reply.status(defaultStatus(route)).send();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (route.raw) {
|
|
145
|
+
if (!reply.getHeader("Content-Type")) {
|
|
146
|
+
const ct = getResponseContentType(route) ?? "application/octet-stream";
|
|
147
|
+
reply.type(ct);
|
|
148
|
+
}
|
|
149
|
+
reply.status(defaultStatus(route)).send(result);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const responseSchema = getResponseSchema(route);
|
|
153
|
+
const output = responseSchema ? (0, response_serializer_1.serializeResponse)(result, responseSchema) : result;
|
|
154
|
+
reply.status(defaultStatus(route)).send(output);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if ((0, validation_errors_1.isValidationErrors)(error)) {
|
|
159
|
+
reply.status(error.status).send(error.body);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if ((0, errors_1.isHttpError)(error)) {
|
|
163
|
+
if (error.headers) {
|
|
164
|
+
reply.headers(error.headers);
|
|
165
|
+
}
|
|
166
|
+
const body = error.body ?? { message: error.message };
|
|
167
|
+
reply.status(error.status).send(body);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function defaultStatus(route) {
|
|
178
|
+
const responses = route.responses ?? [];
|
|
179
|
+
const success = responses.find((response) => !response.error && response.status < 400);
|
|
180
|
+
return success?.status ?? 200;
|
|
181
|
+
}
|
|
182
|
+
function getResponseSchema(route) {
|
|
183
|
+
const responses = route.responses ?? [];
|
|
184
|
+
const success = responses.find((response) => !response.error && response.status < 400);
|
|
185
|
+
return success?.schema;
|
|
186
|
+
}
|
|
187
|
+
function getResponseContentType(route) {
|
|
188
|
+
const responses = route.responses ?? [];
|
|
189
|
+
const success = responses.find((r) => !r.error && r.status < 400);
|
|
190
|
+
return success?.contentType;
|
|
191
|
+
}
|
|
192
|
+
function getResponseSchemaForStatus(route, status) {
|
|
193
|
+
const responses = route.responses ?? [];
|
|
194
|
+
const response = responses.find((r) => r.status === status);
|
|
195
|
+
return response?.schema;
|
|
196
|
+
}
|
|
197
|
+
function joinPaths(base, path) {
|
|
198
|
+
const normalizedBase = base.replace(/\/+$/, "");
|
|
199
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
200
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
201
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FastifyAdapterOptions } from "./types";
|
|
2
|
+
export * from "./types";
|
|
3
|
+
export { attachControllers } from "./controllers";
|
|
4
|
+
export { attachOpenApi } from "./openapi";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a Fastify application with Adorn controllers.
|
|
7
|
+
* @param options - Fastify adapter options
|
|
8
|
+
* @returns Configured Fastify application
|
|
9
|
+
*/
|
|
10
|
+
export declare function createFastifyApp(options: FastifyAdapterOptions): Promise<any>;
|
|
11
|
+
/**
|
|
12
|
+
* Trigger shutdown hooks for graceful application shutdown.
|
|
13
|
+
*/
|
|
14
|
+
export declare function shutdownApp(signal?: string): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.attachOpenApi = exports.attachControllers = void 0;
|
|
21
|
+
exports.createFastifyApp = createFastifyApp;
|
|
22
|
+
exports.shutdownApp = shutdownApp;
|
|
23
|
+
const fastify_1 = __importDefault(require("fastify"));
|
|
24
|
+
const controllers_1 = require("./controllers");
|
|
25
|
+
const openapi_1 = require("./openapi");
|
|
26
|
+
const lifecycle_1 = require("../../core/lifecycle");
|
|
27
|
+
const cors_1 = __importDefault(require("@fastify/cors"));
|
|
28
|
+
const multipart_1 = __importDefault(require("@fastify/multipart"));
|
|
29
|
+
__exportStar(require("./types"), exports);
|
|
30
|
+
var controllers_2 = require("./controllers");
|
|
31
|
+
Object.defineProperty(exports, "attachControllers", { enumerable: true, get: function () { return controllers_2.attachControllers; } });
|
|
32
|
+
var openapi_2 = require("./openapi");
|
|
33
|
+
Object.defineProperty(exports, "attachOpenApi", { enumerable: true, get: function () { return openapi_2.attachOpenApi; } });
|
|
34
|
+
/**
|
|
35
|
+
* Creates a Fastify application with Adorn controllers.
|
|
36
|
+
* @param options - Fastify adapter options
|
|
37
|
+
* @returns Configured Fastify application
|
|
38
|
+
*/
|
|
39
|
+
async function createFastifyApp(options) {
|
|
40
|
+
const app = (0, fastify_1.default)({
|
|
41
|
+
bodyLimit: options.bodyLimit
|
|
42
|
+
});
|
|
43
|
+
if (options.cors) {
|
|
44
|
+
app.register(cors_1.default, options.cors === true ? {} : options.cors);
|
|
45
|
+
}
|
|
46
|
+
if (options.multipart) {
|
|
47
|
+
app.register(multipart_1.default, {
|
|
48
|
+
limits: {
|
|
49
|
+
fileSize: typeof options.multipart === "object" ? options.multipart.maxFileSize : undefined
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const inputCoercion = options.inputCoercion ?? "safe";
|
|
54
|
+
await (0, controllers_1.attachControllers)(app, options.controllers, inputCoercion, options.multipart, options.validation);
|
|
55
|
+
if (options.openApi) {
|
|
56
|
+
(0, openapi_1.attachOpenApi)(app, options.controllers, options.openApi);
|
|
57
|
+
}
|
|
58
|
+
await lifecycle_1.lifecycleRegistry.callOnApplicationBootstrap();
|
|
59
|
+
return app;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Trigger shutdown hooks for graceful application shutdown.
|
|
63
|
+
*/
|
|
64
|
+
async function shutdownApp(signal) {
|
|
65
|
+
await lifecycle_1.lifecycleRegistry.callShutdownHooks(signal);
|
|
66
|
+
lifecycle_1.lifecycleRegistry.clear();
|
|
67
|
+
}
|