adorn-api 1.0.28 → 1.0.29
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/dist/adapter/express/coercion.d.ts +12 -0
- package/dist/adapter/express/coercion.js +247 -0
- package/dist/adapter/express/controllers.d.ts +10 -0
- package/dist/adapter/express/controllers.js +88 -0
- package/dist/adapter/express/cors.d.ts +8 -0
- package/dist/adapter/express/cors.js +57 -0
- package/dist/adapter/express/index.d.ts +12 -0
- package/dist/adapter/express/index.js +52 -0
- package/dist/adapter/express/openapi.d.ts +10 -0
- package/dist/adapter/express/openapi.js +70 -0
- package/dist/adapter/express/types.d.ts +84 -0
- package/dist/adapter/express/types.js +2 -0
- package/dist/e2e/cors.e2e.test.d.ts +1 -0
- package/dist/e2e/cors.e2e.test.js +192 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type InputMeta } from "../../core/metadata";
|
|
2
|
+
import type { InputCoercionMode } from "./types";
|
|
3
|
+
export type InputLocation = "params" | "query";
|
|
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,247 @@
|
|
|
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);
|
|
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) {
|
|
105
|
+
const parsed = coerce_1.coerce.string(value);
|
|
106
|
+
if (parsed === undefined) {
|
|
107
|
+
return { value, ok: true, changed: false };
|
|
108
|
+
}
|
|
109
|
+
return { value: parsed, ok: true, changed: parsed !== value };
|
|
110
|
+
}
|
|
111
|
+
function coerceArrayValue(value, schema, mode) {
|
|
112
|
+
if (value === undefined || value === null) {
|
|
113
|
+
return { value, ok: true, changed: false };
|
|
114
|
+
}
|
|
115
|
+
const input = Array.isArray(value) ? value : [value];
|
|
116
|
+
let changed = !Array.isArray(value);
|
|
117
|
+
let ok = true;
|
|
118
|
+
const output = input.map((entry) => {
|
|
119
|
+
const result = coerceValue(entry, schema.items, mode);
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
ok = false;
|
|
122
|
+
}
|
|
123
|
+
if (result.changed) {
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
return result.value;
|
|
127
|
+
});
|
|
128
|
+
return { value: changed ? output : value, ok, changed };
|
|
129
|
+
}
|
|
130
|
+
function coerceObjectValue(value, schema, mode) {
|
|
131
|
+
if (value === undefined || value === null) {
|
|
132
|
+
return { value, ok: true, changed: false };
|
|
133
|
+
}
|
|
134
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
135
|
+
return { value, ok: mode === "safe", changed: false };
|
|
136
|
+
}
|
|
137
|
+
const properties = schema.properties ?? {};
|
|
138
|
+
const fields = Object.entries(properties).map(([name, fieldSchema]) => ({
|
|
139
|
+
name,
|
|
140
|
+
schema: fieldSchema
|
|
141
|
+
}));
|
|
142
|
+
if (!fields.length) {
|
|
143
|
+
return { value, ok: true, changed: false };
|
|
144
|
+
}
|
|
145
|
+
const result = coerceRecord(value, fields, mode);
|
|
146
|
+
return {
|
|
147
|
+
value: result.value,
|
|
148
|
+
ok: result.invalidFields.length === 0,
|
|
149
|
+
changed: result.value !== value
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function coerceRecordValue(value, schema, mode) {
|
|
153
|
+
if (value === undefined || value === null) {
|
|
154
|
+
return { value, ok: true, changed: false };
|
|
155
|
+
}
|
|
156
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
157
|
+
return { value, ok: mode === "safe", changed: false };
|
|
158
|
+
}
|
|
159
|
+
const input = value;
|
|
160
|
+
let changed = false;
|
|
161
|
+
let ok = true;
|
|
162
|
+
const output = { ...input };
|
|
163
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
164
|
+
const result = coerceValue(entry, schema.values, mode);
|
|
165
|
+
if (!result.ok) {
|
|
166
|
+
ok = false;
|
|
167
|
+
}
|
|
168
|
+
if (result.changed) {
|
|
169
|
+
output[key] = result.value;
|
|
170
|
+
changed = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { value: changed ? output : value, ok, changed };
|
|
174
|
+
}
|
|
175
|
+
function coerceRefValue(value, schema, mode) {
|
|
176
|
+
if (value === undefined || value === null) {
|
|
177
|
+
return { value, ok: true, changed: false };
|
|
178
|
+
}
|
|
179
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
180
|
+
return { value, ok: mode === "safe", changed: false };
|
|
181
|
+
}
|
|
182
|
+
const meta = getDtoMetaSafe(schema.dto);
|
|
183
|
+
const fields = Object.entries(meta.fields).map(([name, field]) => ({
|
|
184
|
+
name,
|
|
185
|
+
schema: field.schema
|
|
186
|
+
}));
|
|
187
|
+
if (!fields.length) {
|
|
188
|
+
return { value, ok: true, changed: false };
|
|
189
|
+
}
|
|
190
|
+
const result = coerceRecord(value, fields, mode);
|
|
191
|
+
return {
|
|
192
|
+
value: result.value,
|
|
193
|
+
ok: result.invalidFields.length === 0,
|
|
194
|
+
changed: result.value !== value
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function coerceUnionValue(value, schema, mode) {
|
|
198
|
+
let fallback;
|
|
199
|
+
for (const option of schema.anyOf) {
|
|
200
|
+
const result = coerceValue(value, option, mode);
|
|
201
|
+
if (!result.ok) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (result.changed) {
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
fallback ??= result;
|
|
208
|
+
}
|
|
209
|
+
if (fallback) {
|
|
210
|
+
return fallback;
|
|
211
|
+
}
|
|
212
|
+
return { value, ok: mode === "safe", changed: false };
|
|
213
|
+
}
|
|
214
|
+
function extractFields(schema) {
|
|
215
|
+
if (isSchemaNode(schema)) {
|
|
216
|
+
if (schema.kind === "object" && schema.properties) {
|
|
217
|
+
return Object.entries(schema.properties).map(([name, fieldSchema]) => ({
|
|
218
|
+
name,
|
|
219
|
+
schema: fieldSchema
|
|
220
|
+
}));
|
|
221
|
+
}
|
|
222
|
+
return [];
|
|
223
|
+
}
|
|
224
|
+
const meta = getDtoMetaSafe(schema);
|
|
225
|
+
return Object.entries(meta.fields).map(([name, field]) => ({
|
|
226
|
+
name,
|
|
227
|
+
schema: field.schema
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
function getDtoMetaSafe(dto) {
|
|
231
|
+
const meta = (0, metadata_1.getDtoMeta)(dto);
|
|
232
|
+
if (!meta) {
|
|
233
|
+
throw new Error(`DTO "${dto.name}" is missing @Dto decorator.`);
|
|
234
|
+
}
|
|
235
|
+
return meta;
|
|
236
|
+
}
|
|
237
|
+
function isSchemaNode(value) {
|
|
238
|
+
return !!value && typeof value === "object" && "kind" in value;
|
|
239
|
+
}
|
|
240
|
+
function isPresent(value) {
|
|
241
|
+
return coerce_1.coerce.string(value) !== undefined;
|
|
242
|
+
}
|
|
243
|
+
function buildInvalidMessage(location, fields) {
|
|
244
|
+
const label = location === "params" ? "path parameter" : "query parameter";
|
|
245
|
+
const suffix = fields.length > 1 ? "s" : "";
|
|
246
|
+
return `Invalid ${label}${suffix}: ${fields.join(", ")}.`;
|
|
247
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { Constructor } from "../../core/types";
|
|
3
|
+
import type { InputCoercionSetting } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* Attaches controllers to an Express application.
|
|
6
|
+
* @param app - Express application instance
|
|
7
|
+
* @param controllers - Array of controller classes
|
|
8
|
+
* @param inputCoercion - Input coercion setting
|
|
9
|
+
*/
|
|
10
|
+
export declare function attachControllers(app: Express, controllers: Constructor[], inputCoercion?: InputCoercionSetting): void;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachControllers = attachControllers;
|
|
4
|
+
const metadata_1 = require("../../core/metadata");
|
|
5
|
+
const errors_1 = require("../../core/errors");
|
|
6
|
+
const coercion_1 = require("./coercion");
|
|
7
|
+
/**
|
|
8
|
+
* Attaches controllers to an Express application.
|
|
9
|
+
* @param app - Express application instance
|
|
10
|
+
* @param controllers - Array of controller classes
|
|
11
|
+
* @param inputCoercion - Input coercion setting
|
|
12
|
+
*/
|
|
13
|
+
function attachControllers(app, controllers, inputCoercion = "safe") {
|
|
14
|
+
for (const controller of controllers) {
|
|
15
|
+
const meta = (0, metadata_1.getControllerMeta)(controller);
|
|
16
|
+
if (!meta) {
|
|
17
|
+
throw new Error(`Controller "${controller.name}" is missing @Controller decorator.`);
|
|
18
|
+
}
|
|
19
|
+
const instance = new controller();
|
|
20
|
+
for (const route of meta.routes) {
|
|
21
|
+
const path = joinPaths(meta.basePath, route.path);
|
|
22
|
+
const handler = instance[route.handlerName];
|
|
23
|
+
if (typeof handler !== "function") {
|
|
24
|
+
throw new Error(`Handler "${String(route.handlerName)}" is not a function on ${controller.name}.`);
|
|
25
|
+
}
|
|
26
|
+
const coerceParams = inputCoercion === false
|
|
27
|
+
? undefined
|
|
28
|
+
: (0, coercion_1.createInputCoercer)(route.params, { mode: inputCoercion, location: "params" });
|
|
29
|
+
const coerceQuery = inputCoercion === false
|
|
30
|
+
? undefined
|
|
31
|
+
: (0, coercion_1.createInputCoercer)(route.query, { mode: inputCoercion, location: "query" });
|
|
32
|
+
app[route.httpMethod](path, async (req, res, next) => {
|
|
33
|
+
try {
|
|
34
|
+
const ctx = {
|
|
35
|
+
req,
|
|
36
|
+
res,
|
|
37
|
+
body: req.body,
|
|
38
|
+
query: coerceQuery ? coerceQuery(req.query) : req.query,
|
|
39
|
+
params: coerceParams ? coerceParams(req.params) : req.params,
|
|
40
|
+
headers: req.headers
|
|
41
|
+
};
|
|
42
|
+
const result = await handler.call(instance, ctx);
|
|
43
|
+
if (res.headersSent) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (result === undefined) {
|
|
47
|
+
res.status(defaultStatus(route)).end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
res.status(defaultStatus(route)).json(result);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if ((0, errors_1.isHttpError)(error)) {
|
|
54
|
+
sendHttpError(res, error);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
next(error);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function defaultStatus(route) {
|
|
64
|
+
const responses = route.responses ?? [];
|
|
65
|
+
const success = responses.find((response) => !response.error && response.status < 400);
|
|
66
|
+
return success?.status ?? 200;
|
|
67
|
+
}
|
|
68
|
+
function sendHttpError(res, error) {
|
|
69
|
+
if (res.headersSent) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (error.headers) {
|
|
73
|
+
for (const [key, value] of Object.entries(error.headers)) {
|
|
74
|
+
res.setHeader(key, value);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const body = error.body ?? { message: error.message };
|
|
78
|
+
if (body === undefined) {
|
|
79
|
+
res.status(error.status).end();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
res.status(error.status).json(body);
|
|
83
|
+
}
|
|
84
|
+
function joinPaths(base, path) {
|
|
85
|
+
const normalizedBase = base.replace(/\/+$/, "");
|
|
86
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
87
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { CorsOptions } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Attaches CORS middleware to an Express application.
|
|
5
|
+
* @param app - Express application instance
|
|
6
|
+
* @param options - CORS options
|
|
7
|
+
*/
|
|
8
|
+
export declare function attachCors(app: Express, options?: CorsOptions): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachCors = attachCors;
|
|
4
|
+
/**
|
|
5
|
+
* Attaches CORS middleware to an Express application.
|
|
6
|
+
* @param app - Express application instance
|
|
7
|
+
* @param options - CORS options
|
|
8
|
+
*/
|
|
9
|
+
function attachCors(app, options = {}) {
|
|
10
|
+
const methods = options.methods ?? ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"];
|
|
11
|
+
const allowedHeaders = options.allowedHeaders ?? ["Content-Type", "Authorization"];
|
|
12
|
+
const maxAge = options.maxAge ?? 86400;
|
|
13
|
+
const resolveOrigin = (requestOrigin) => {
|
|
14
|
+
const origin = options.origin ?? "*";
|
|
15
|
+
if (origin === "*") {
|
|
16
|
+
return options.credentials ? (requestOrigin ?? "*") : "*";
|
|
17
|
+
}
|
|
18
|
+
if (typeof origin === "string") {
|
|
19
|
+
return origin === requestOrigin ? origin : false;
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(origin)) {
|
|
22
|
+
return requestOrigin && origin.includes(requestOrigin) ? requestOrigin : false;
|
|
23
|
+
}
|
|
24
|
+
if (typeof origin === "function") {
|
|
25
|
+
const result = origin(requestOrigin);
|
|
26
|
+
if (typeof result === "string") {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
return result && requestOrigin ? requestOrigin : false;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
};
|
|
33
|
+
app.use((req, res, next) => {
|
|
34
|
+
const requestOrigin = req.headers.origin;
|
|
35
|
+
const allowedOrigin = resolveOrigin(requestOrigin);
|
|
36
|
+
if (allowedOrigin) {
|
|
37
|
+
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
38
|
+
}
|
|
39
|
+
if (options.credentials) {
|
|
40
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
41
|
+
}
|
|
42
|
+
if (options.exposedHeaders?.length) {
|
|
43
|
+
res.setHeader("Access-Control-Expose-Headers", options.exposedHeaders.join(", "));
|
|
44
|
+
}
|
|
45
|
+
if (allowedOrigin !== "*" && requestOrigin) {
|
|
46
|
+
res.setHeader("Vary", "Origin");
|
|
47
|
+
}
|
|
48
|
+
if (req.method === "OPTIONS") {
|
|
49
|
+
res.setHeader("Access-Control-Allow-Methods", methods.join(", "));
|
|
50
|
+
res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(", "));
|
|
51
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
52
|
+
res.status(204).end();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
next();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import type { ExpressAdapterOptions } from "./types";
|
|
3
|
+
export * from "./types";
|
|
4
|
+
export { attachCors } from "./cors";
|
|
5
|
+
export { attachControllers } from "./controllers";
|
|
6
|
+
export { attachOpenApi } from "./openapi";
|
|
7
|
+
/**
|
|
8
|
+
* Creates an Express application with Adorn controllers.
|
|
9
|
+
* @param options - Express adapter options
|
|
10
|
+
* @returns Configured Express application
|
|
11
|
+
*/
|
|
12
|
+
export declare function createExpressApp(options: ExpressAdapterOptions): express.Express;
|
|
@@ -0,0 +1,52 @@
|
|
|
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 = exports.attachCors = void 0;
|
|
21
|
+
exports.createExpressApp = createExpressApp;
|
|
22
|
+
const express_1 = __importDefault(require("express"));
|
|
23
|
+
const cors_1 = require("./cors");
|
|
24
|
+
const controllers_1 = require("./controllers");
|
|
25
|
+
const openapi_1 = require("./openapi");
|
|
26
|
+
__exportStar(require("./types"), exports);
|
|
27
|
+
var cors_2 = require("./cors");
|
|
28
|
+
Object.defineProperty(exports, "attachCors", { enumerable: true, get: function () { return cors_2.attachCors; } });
|
|
29
|
+
var controllers_2 = require("./controllers");
|
|
30
|
+
Object.defineProperty(exports, "attachControllers", { enumerable: true, get: function () { return controllers_2.attachControllers; } });
|
|
31
|
+
var openapi_2 = require("./openapi");
|
|
32
|
+
Object.defineProperty(exports, "attachOpenApi", { enumerable: true, get: function () { return openapi_2.attachOpenApi; } });
|
|
33
|
+
/**
|
|
34
|
+
* Creates an Express application with Adorn controllers.
|
|
35
|
+
* @param options - Express adapter options
|
|
36
|
+
* @returns Configured Express application
|
|
37
|
+
*/
|
|
38
|
+
function createExpressApp(options) {
|
|
39
|
+
const app = (0, express_1.default)();
|
|
40
|
+
if (options.cors) {
|
|
41
|
+
(0, cors_1.attachCors)(app, options.cors === true ? {} : options.cors);
|
|
42
|
+
}
|
|
43
|
+
if (options.jsonBody ?? true) {
|
|
44
|
+
app.use(express_1.default.json());
|
|
45
|
+
}
|
|
46
|
+
const inputCoercion = options.inputCoercion ?? "safe";
|
|
47
|
+
(0, controllers_1.attachControllers)(app, options.controllers, inputCoercion);
|
|
48
|
+
if (options.openApi) {
|
|
49
|
+
(0, openapi_1.attachOpenApi)(app, options.controllers, options.openApi);
|
|
50
|
+
}
|
|
51
|
+
return app;
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { Constructor } from "../../core/types";
|
|
3
|
+
import type { OpenApiExpressOptions } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* Attaches OpenAPI endpoints to an Express application.
|
|
6
|
+
* @param app - Express application instance
|
|
7
|
+
* @param controllers - Array of controller classes
|
|
8
|
+
* @param options - OpenAPI options
|
|
9
|
+
*/
|
|
10
|
+
export declare function attachOpenApi(app: Express, controllers: Constructor[], options: OpenApiExpressOptions): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachOpenApi = attachOpenApi;
|
|
4
|
+
const openapi_1 = require("../../core/openapi");
|
|
5
|
+
/**
|
|
6
|
+
* Attaches OpenAPI endpoints to an Express application.
|
|
7
|
+
* @param app - Express application instance
|
|
8
|
+
* @param controllers - Array of controller classes
|
|
9
|
+
* @param options - OpenAPI options
|
|
10
|
+
*/
|
|
11
|
+
function attachOpenApi(app, controllers, options) {
|
|
12
|
+
const openApiPath = normalizePath(options.path, "/openapi.json");
|
|
13
|
+
const document = (0, openapi_1.buildOpenApi)({
|
|
14
|
+
info: options.info,
|
|
15
|
+
servers: options.servers,
|
|
16
|
+
controllers
|
|
17
|
+
});
|
|
18
|
+
app.get(openApiPath, (_req, res) => {
|
|
19
|
+
res.json(document);
|
|
20
|
+
});
|
|
21
|
+
if (!options.docs) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const docsOptions = typeof options.docs === "object" ? options.docs : {};
|
|
25
|
+
const docsPath = normalizePath(docsOptions.path, "/docs");
|
|
26
|
+
const title = docsOptions.title ?? `${options.info.title} Docs`;
|
|
27
|
+
const swaggerUiUrl = (docsOptions.swaggerUiUrl ?? "https://unpkg.com/swagger-ui-dist@5").replace(/\/+$/, "");
|
|
28
|
+
const html = buildSwaggerUiHtml({ title, swaggerUiUrl, openApiPath });
|
|
29
|
+
app.get(docsPath, (_req, res) => {
|
|
30
|
+
res.type("html").send(html);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function normalizePath(path, fallback) {
|
|
34
|
+
if (!path) {
|
|
35
|
+
return fallback;
|
|
36
|
+
}
|
|
37
|
+
return path.startsWith("/") ? path : `/${path}`;
|
|
38
|
+
}
|
|
39
|
+
function buildSwaggerUiHtml(options) {
|
|
40
|
+
return `<!doctype html>
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8" />
|
|
44
|
+
<title>${options.title}</title>
|
|
45
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
46
|
+
<link rel="stylesheet" href="${options.swaggerUiUrl}/swagger-ui.css" />
|
|
47
|
+
<style>
|
|
48
|
+
body {
|
|
49
|
+
margin: 0;
|
|
50
|
+
background: #f6f6f6;
|
|
51
|
+
}
|
|
52
|
+
</style>
|
|
53
|
+
</head>
|
|
54
|
+
<body>
|
|
55
|
+
<div id="swagger-ui"></div>
|
|
56
|
+
<script src="${options.swaggerUiUrl}/swagger-ui-bundle.js"></script>
|
|
57
|
+
<script>
|
|
58
|
+
window.onload = () => {
|
|
59
|
+
window.ui = SwaggerUIBundle({
|
|
60
|
+
url: "${options.openApiPath}",
|
|
61
|
+
dom_id: "#swagger-ui",
|
|
62
|
+
deepLinking: true,
|
|
63
|
+
presets: [SwaggerUIBundle.presets.apis],
|
|
64
|
+
layout: "BaseLayout"
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
</script>
|
|
68
|
+
</body>
|
|
69
|
+
</html>`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import type { Constructor } from "../../core/types";
|
|
3
|
+
import type { OpenApiInfo, OpenApiServer } from "../../core/openapi";
|
|
4
|
+
/**
|
|
5
|
+
* Request context provided to route handlers.
|
|
6
|
+
*/
|
|
7
|
+
export interface RequestContext<TBody = unknown, TQuery extends object | undefined = Record<string, unknown>, TParams extends object | undefined = Record<string, string | number | boolean | undefined>, THeaders extends object | undefined = Record<string, string | string[] | undefined>> {
|
|
8
|
+
/** Express request object */
|
|
9
|
+
req: Request;
|
|
10
|
+
/** Express response object */
|
|
11
|
+
res: Response;
|
|
12
|
+
/** Parsed request body */
|
|
13
|
+
body: TBody;
|
|
14
|
+
/** Parsed query parameters */
|
|
15
|
+
query: TQuery;
|
|
16
|
+
/** Parsed path parameters */
|
|
17
|
+
params: TParams;
|
|
18
|
+
/** Request headers */
|
|
19
|
+
headers: THeaders;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Input coercion modes.
|
|
23
|
+
*/
|
|
24
|
+
export type InputCoercionMode = "safe" | "strict";
|
|
25
|
+
/**
|
|
26
|
+
* Input coercion setting - can be a mode or disabled.
|
|
27
|
+
*/
|
|
28
|
+
export type InputCoercionSetting = InputCoercionMode | false;
|
|
29
|
+
/**
|
|
30
|
+
* CORS configuration options.
|
|
31
|
+
*/
|
|
32
|
+
export interface CorsOptions {
|
|
33
|
+
/** Allowed origins. Use "*" for all, a string, array of strings, or a function for dynamic matching. */
|
|
34
|
+
origin?: string | string[] | ((origin: string | undefined) => boolean | string);
|
|
35
|
+
/** Allowed HTTP methods. Defaults to ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"]. */
|
|
36
|
+
methods?: string[];
|
|
37
|
+
/** Allowed headers. Defaults to ["Content-Type", "Authorization"]. */
|
|
38
|
+
allowedHeaders?: string[];
|
|
39
|
+
/** Headers exposed to the client. */
|
|
40
|
+
exposedHeaders?: string[];
|
|
41
|
+
/** Whether to include credentials (cookies, authorization headers). Defaults to false. */
|
|
42
|
+
credentials?: boolean;
|
|
43
|
+
/** Max age in seconds for preflight cache. Defaults to 86400 (24 hours). */
|
|
44
|
+
maxAge?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Options for OpenAPI documentation UI.
|
|
48
|
+
*/
|
|
49
|
+
export interface OpenApiDocsOptions {
|
|
50
|
+
/** Path for documentation UI */
|
|
51
|
+
path?: string;
|
|
52
|
+
/** Title for documentation page */
|
|
53
|
+
title?: string;
|
|
54
|
+
/** URL for Swagger UI assets */
|
|
55
|
+
swaggerUiUrl?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* OpenAPI configuration for Express adapter.
|
|
59
|
+
*/
|
|
60
|
+
export interface OpenApiExpressOptions {
|
|
61
|
+
/** OpenAPI document info */
|
|
62
|
+
info: OpenApiInfo;
|
|
63
|
+
/** Array of servers */
|
|
64
|
+
servers?: OpenApiServer[];
|
|
65
|
+
/** Path for OpenAPI JSON endpoint */
|
|
66
|
+
path?: string;
|
|
67
|
+
/** Documentation UI configuration */
|
|
68
|
+
docs?: boolean | OpenApiDocsOptions;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Options for creating an Express application adapter.
|
|
72
|
+
*/
|
|
73
|
+
export interface ExpressAdapterOptions {
|
|
74
|
+
/** Array of controller classes */
|
|
75
|
+
controllers: Constructor[];
|
|
76
|
+
/** Whether to enable JSON body parsing */
|
|
77
|
+
jsonBody?: boolean;
|
|
78
|
+
/** OpenAPI configuration */
|
|
79
|
+
openApi?: OpenApiExpressOptions;
|
|
80
|
+
/** Input coercion setting */
|
|
81
|
+
inputCoercion?: InputCoercionSetting;
|
|
82
|
+
/** CORS configuration. Set to true for permissive defaults, or provide options. */
|
|
83
|
+
cors?: boolean | CorsOptions;
|
|
84
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
|
|
3
|
+
var useValue = arguments.length > 2;
|
|
4
|
+
for (var i = 0; i < initializers.length; i++) {
|
|
5
|
+
value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
6
|
+
}
|
|
7
|
+
return useValue ? value : void 0;
|
|
8
|
+
};
|
|
9
|
+
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
10
|
+
function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
|
|
11
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
12
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
13
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
14
|
+
var _, done = false;
|
|
15
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
16
|
+
var context = {};
|
|
17
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
18
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
19
|
+
context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
|
|
20
|
+
var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
|
|
21
|
+
if (kind === "accessor") {
|
|
22
|
+
if (result === void 0) continue;
|
|
23
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
24
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
25
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
26
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
27
|
+
}
|
|
28
|
+
else if (_ = accept(result)) {
|
|
29
|
+
if (kind === "field") initializers.unshift(_);
|
|
30
|
+
else descriptor[key] = _;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
34
|
+
done = true;
|
|
35
|
+
};
|
|
36
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
37
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
|
+
};
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
const vitest_1 = require("vitest");
|
|
41
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
42
|
+
const index_1 = require("../index");
|
|
43
|
+
let TestController = (() => {
|
|
44
|
+
let _classDecorators = [(0, index_1.Controller)("/test")];
|
|
45
|
+
let _classDescriptor;
|
|
46
|
+
let _classExtraInitializers = [];
|
|
47
|
+
let _classThis;
|
|
48
|
+
let _instanceExtraInitializers = [];
|
|
49
|
+
let _hello_decorators;
|
|
50
|
+
var TestController = class {
|
|
51
|
+
static { _classThis = this; }
|
|
52
|
+
static {
|
|
53
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
|
|
54
|
+
_hello_decorators = [(0, index_1.Get)("/hello")];
|
|
55
|
+
__esDecorate(this, null, _hello_decorators, { kind: "method", name: "hello", static: false, private: false, access: { has: obj => "hello" in obj, get: obj => obj.hello }, metadata: _metadata }, null, _instanceExtraInitializers);
|
|
56
|
+
__esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
|
|
57
|
+
TestController = _classThis = _classDescriptor.value;
|
|
58
|
+
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
|
|
59
|
+
__runInitializers(_classThis, _classExtraInitializers);
|
|
60
|
+
}
|
|
61
|
+
hello() {
|
|
62
|
+
return { message: "hello" };
|
|
63
|
+
}
|
|
64
|
+
constructor() {
|
|
65
|
+
__runInitializers(this, _instanceExtraInitializers);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
return TestController = _classThis;
|
|
69
|
+
})();
|
|
70
|
+
(0, vitest_1.describe)("CORS middleware", () => {
|
|
71
|
+
(0, vitest_1.describe)("cors: true (permissive defaults)", () => {
|
|
72
|
+
const app = (0, index_1.createExpressApp)({ controllers: [TestController], cors: true });
|
|
73
|
+
(0, vitest_1.it)("allows all origins with wildcard", async () => {
|
|
74
|
+
const res = await (0, supertest_1.default)(app)
|
|
75
|
+
.get("/test/hello")
|
|
76
|
+
.set("Origin", "https://example.com");
|
|
77
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-origin"]).toBe("*");
|
|
78
|
+
});
|
|
79
|
+
(0, vitest_1.it)("handles preflight OPTIONS requests", async () => {
|
|
80
|
+
const res = await (0, supertest_1.default)(app)
|
|
81
|
+
.options("/test/hello")
|
|
82
|
+
.set("Origin", "https://example.com")
|
|
83
|
+
.set("Access-Control-Request-Method", "GET");
|
|
84
|
+
(0, vitest_1.expect)(res.status).toBe(204);
|
|
85
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-methods"]).toContain("GET");
|
|
86
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-headers"]).toContain("Content-Type");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.describe)("cors with specific origin", () => {
|
|
90
|
+
const app = (0, index_1.createExpressApp)({
|
|
91
|
+
controllers: [TestController],
|
|
92
|
+
cors: { origin: "https://allowed.com" }
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.it)("allows matching origin", async () => {
|
|
95
|
+
const res = await (0, supertest_1.default)(app)
|
|
96
|
+
.get("/test/hello")
|
|
97
|
+
.set("Origin", "https://allowed.com");
|
|
98
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-origin"]).toBe("https://allowed.com");
|
|
99
|
+
});
|
|
100
|
+
(0, vitest_1.it)("does not set header for non-matching origin", async () => {
|
|
101
|
+
const res = await (0, supertest_1.default)(app)
|
|
102
|
+
.get("/test/hello")
|
|
103
|
+
.set("Origin", "https://not-allowed.com");
|
|
104
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-origin"]).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
(0, vitest_1.describe)("cors with origin array", () => {
|
|
108
|
+
const app = (0, index_1.createExpressApp)({
|
|
109
|
+
controllers: [TestController],
|
|
110
|
+
cors: { origin: ["https://a.com", "https://b.com"] }
|
|
111
|
+
});
|
|
112
|
+
(0, vitest_1.it)("allows origins in the list", async () => {
|
|
113
|
+
const res = await (0, supertest_1.default)(app)
|
|
114
|
+
.get("/test/hello")
|
|
115
|
+
.set("Origin", "https://a.com");
|
|
116
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-origin"]).toBe("https://a.com");
|
|
117
|
+
});
|
|
118
|
+
(0, vitest_1.it)("does not allow origins not in the list", async () => {
|
|
119
|
+
const res = await (0, supertest_1.default)(app)
|
|
120
|
+
.get("/test/hello")
|
|
121
|
+
.set("Origin", "https://c.com");
|
|
122
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-origin"]).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.describe)("cors with dynamic origin function", () => {
|
|
126
|
+
const app = (0, index_1.createExpressApp)({
|
|
127
|
+
controllers: [TestController],
|
|
128
|
+
cors: { origin: (origin) => origin?.endsWith(".trusted.com") ?? false }
|
|
129
|
+
});
|
|
130
|
+
(0, vitest_1.it)("allows origins matching the function", async () => {
|
|
131
|
+
const res = await (0, supertest_1.default)(app)
|
|
132
|
+
.get("/test/hello")
|
|
133
|
+
.set("Origin", "https://api.trusted.com");
|
|
134
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-origin"]).toBe("https://api.trusted.com");
|
|
135
|
+
});
|
|
136
|
+
(0, vitest_1.it)("rejects origins not matching the function", async () => {
|
|
137
|
+
const res = await (0, supertest_1.default)(app)
|
|
138
|
+
.get("/test/hello")
|
|
139
|
+
.set("Origin", "https://evil.com");
|
|
140
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-origin"]).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
(0, vitest_1.describe)("cors with credentials", () => {
|
|
144
|
+
const app = (0, index_1.createExpressApp)({
|
|
145
|
+
controllers: [TestController],
|
|
146
|
+
cors: { origin: "https://app.com", credentials: true }
|
|
147
|
+
});
|
|
148
|
+
(0, vitest_1.it)("sets credentials header", async () => {
|
|
149
|
+
const res = await (0, supertest_1.default)(app)
|
|
150
|
+
.get("/test/hello")
|
|
151
|
+
.set("Origin", "https://app.com");
|
|
152
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-credentials"]).toBe("true");
|
|
153
|
+
});
|
|
154
|
+
(0, vitest_1.it)("sets Vary header for non-wildcard origin", async () => {
|
|
155
|
+
const res = await (0, supertest_1.default)(app)
|
|
156
|
+
.get("/test/hello")
|
|
157
|
+
.set("Origin", "https://app.com");
|
|
158
|
+
(0, vitest_1.expect)(res.headers["vary"]).toBe("Origin");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
(0, vitest_1.describe)("cors with exposed headers", () => {
|
|
162
|
+
const app = (0, index_1.createExpressApp)({
|
|
163
|
+
controllers: [TestController],
|
|
164
|
+
cors: { exposedHeaders: ["X-Custom-Header", "X-Request-Id"] }
|
|
165
|
+
});
|
|
166
|
+
(0, vitest_1.it)("sets exposed headers", async () => {
|
|
167
|
+
const res = await (0, supertest_1.default)(app)
|
|
168
|
+
.get("/test/hello")
|
|
169
|
+
.set("Origin", "https://example.com");
|
|
170
|
+
(0, vitest_1.expect)(res.headers["access-control-expose-headers"]).toBe("X-Custom-Header, X-Request-Id");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
(0, vitest_1.describe)("cors with custom methods and headers", () => {
|
|
174
|
+
const app = (0, index_1.createExpressApp)({
|
|
175
|
+
controllers: [TestController],
|
|
176
|
+
cors: {
|
|
177
|
+
methods: ["GET", "POST"],
|
|
178
|
+
allowedHeaders: ["X-API-Key"],
|
|
179
|
+
maxAge: 3600
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
(0, vitest_1.it)("uses custom methods in preflight", async () => {
|
|
183
|
+
const res = await (0, supertest_1.default)(app)
|
|
184
|
+
.options("/test/hello")
|
|
185
|
+
.set("Origin", "https://example.com")
|
|
186
|
+
.set("Access-Control-Request-Method", "POST");
|
|
187
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-methods"]).toBe("GET, POST");
|
|
188
|
+
(0, vitest_1.expect)(res.headers["access-control-allow-headers"]).toBe("X-API-Key");
|
|
189
|
+
(0, vitest_1.expect)(res.headers["access-control-max-age"]).toBe("3600");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export * from "./core/schema";
|
|
|
3
3
|
export * from "./core/openapi";
|
|
4
4
|
export * from "./core/errors";
|
|
5
5
|
export * from "./core/coerce";
|
|
6
|
-
export * from "./adapter/express";
|
|
6
|
+
export * from "./adapter/express/index";
|
|
7
7
|
export * from "./adapter/metal-orm/index";
|
|
8
8
|
export * from "./core/types";
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,6 @@ __exportStar(require("./core/schema"), exports);
|
|
|
19
19
|
__exportStar(require("./core/openapi"), exports);
|
|
20
20
|
__exportStar(require("./core/errors"), exports);
|
|
21
21
|
__exportStar(require("./core/coerce"), exports);
|
|
22
|
-
__exportStar(require("./adapter/express"), exports);
|
|
22
|
+
__exportStar(require("./adapter/express/index"), exports);
|
|
23
23
|
__exportStar(require("./adapter/metal-orm/index"), exports);
|
|
24
24
|
__exportStar(require("./core/types"), exports);
|