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.
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adorn-api",
3
- "version": "1.0.28",
3
+ "version": "1.0.29",
4
4
  "description": "Decorator-first web framework with OpenAPI 3.1 schema generation.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",