adorn-api 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +318 -620
  2. package/dist/adapter/express/auth.d.ts +5 -0
  3. package/dist/adapter/express/auth.d.ts.map +1 -0
  4. package/dist/adapter/express/coercion.d.ts +22 -0
  5. package/dist/adapter/express/coercion.d.ts.map +1 -0
  6. package/dist/adapter/express/index.d.ts +3 -50
  7. package/dist/adapter/express/index.d.ts.map +1 -1
  8. package/dist/adapter/express/openapi.d.ts +11 -0
  9. package/dist/adapter/express/openapi.d.ts.map +1 -0
  10. package/dist/adapter/express/router.d.ts +4 -0
  11. package/dist/adapter/express/router.d.ts.map +1 -0
  12. package/dist/adapter/express/swagger.d.ts +4 -0
  13. package/dist/adapter/express/swagger.d.ts.map +1 -0
  14. package/dist/adapter/express/types.d.ts +64 -0
  15. package/dist/adapter/express/types.d.ts.map +1 -0
  16. package/dist/adapter/express/validation.d.ts +10 -0
  17. package/dist/adapter/express/validation.d.ts.map +1 -0
  18. package/dist/cli.cjs +330 -142
  19. package/dist/cli.cjs.map +1 -1
  20. package/dist/cli.js +329 -141
  21. package/dist/cli.js.map +1 -1
  22. package/dist/compiler/manifest/emit.d.ts.map +1 -1
  23. package/dist/compiler/manifest/format.d.ts +1 -0
  24. package/dist/compiler/manifest/format.d.ts.map +1 -1
  25. package/dist/compiler/schema/openapi.d.ts.map +1 -1
  26. package/dist/compiler/schema/typeToJsonSchema.d.ts +7 -1
  27. package/dist/compiler/schema/typeToJsonSchema.d.ts.map +1 -1
  28. package/dist/express.cjs +618 -586
  29. package/dist/express.cjs.map +1 -1
  30. package/dist/express.js +615 -583
  31. package/dist/express.js.map +1 -1
  32. package/dist/http.d.ts +11 -9
  33. package/dist/http.d.ts.map +1 -1
  34. package/dist/index.cjs +2 -10
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.d.ts +2 -3
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -9
  39. package/dist/index.js.map +1 -1
  40. package/dist/metal/applyListQuery.d.ts +27 -0
  41. package/dist/metal/applyListQuery.d.ts.map +1 -0
  42. package/dist/metal/index.cjs +59 -0
  43. package/dist/metal/index.cjs.map +1 -1
  44. package/dist/metal/index.d.ts +4 -0
  45. package/dist/metal/index.d.ts.map +1 -1
  46. package/dist/metal/index.js +55 -0
  47. package/dist/metal/index.js.map +1 -1
  48. package/dist/metal/listQuery.d.ts +7 -0
  49. package/dist/metal/listQuery.d.ts.map +1 -0
  50. package/dist/metal/queryOptions.d.ts +8 -0
  51. package/dist/metal/queryOptions.d.ts.map +1 -0
  52. package/package.json +2 -2
  53. package/dist/compiler/analyze/extractQueryStyle.d.ts +0 -8
  54. package/dist/compiler/analyze/extractQueryStyle.d.ts.map +0 -1
package/dist/express.js CHANGED
@@ -8,10 +8,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
  // src/runtime/polyfill.ts
9
9
  Symbol.metadata ??= /* @__PURE__ */ Symbol("Symbol.metadata");
10
10
 
11
- // src/adapter/express/index.ts
11
+ // src/adapter/express/router.ts
12
12
  import { Router } from "express";
13
- import { readFileSync } from "fs";
14
- import { resolve, join, isAbsolute } from "path";
13
+ import { join } from "path";
15
14
 
16
15
  // src/runtime/metadata/key.ts
17
16
  var ADORN_META = /* @__PURE__ */ Symbol.for("adorn-api/meta");
@@ -231,101 +230,62 @@ async function loadArtifacts(options) {
231
230
  };
232
231
  }
233
232
 
234
- // src/adapter/express/index.ts
235
- import swaggerUi from "swagger-ui-express";
236
-
237
- // src/adapter/express/bootstrap.ts
238
- import express from "express";
239
- import path2 from "path";
240
- function bootstrap(options) {
241
- return new Promise(async (resolve2, reject) => {
242
- try {
243
- const {
244
- controllers,
245
- port: userPort,
246
- host: userHost,
247
- artifactsDir: userArtifactsDir = ".adorn",
248
- enableSwagger = true,
249
- swaggerPath = "/docs",
250
- swaggerJsonPath = "/docs/openapi.json",
251
- middleware,
252
- auth,
253
- coerce
254
- } = options;
255
- if (controllers.length === 0) {
256
- reject(new Error("At least one controller must be provided to bootstrap()."));
257
- return;
258
- }
259
- const envPort = process.env.PORT;
260
- const port = userPort ?? (envPort !== void 0 ? Number(envPort) : 3e3);
261
- const host = userHost ?? process.env.HOST ?? "0.0.0.0";
262
- if (isNaN(port) || port < 0 || port > 65535) {
263
- reject(new Error(`Invalid port: ${port}. Port must be between 0 and 65535.`));
264
- return;
265
- }
266
- const absoluteArtifactsDir = path2.isAbsolute(userArtifactsDir) ? userArtifactsDir : path2.resolve(process.cwd(), userArtifactsDir);
267
- const app = express();
268
- app.use(express.json());
269
- const router = await createExpressRouter({
270
- controllers,
271
- artifactsDir: absoluteArtifactsDir,
272
- middleware,
273
- auth,
274
- coerce
275
- });
276
- app.use(router);
277
- if (enableSwagger) {
278
- const displayHost = host === "0.0.0.0" ? "localhost" : host;
279
- const serverUrl = `http://${displayHost}:${port}`;
280
- app.use(
281
- setupSwagger({
282
- artifactsDir: absoluteArtifactsDir,
283
- jsonPath: swaggerJsonPath,
284
- uiPath: swaggerPath,
285
- swaggerOptions: {
286
- servers: [{ url: serverUrl }]
287
- }
288
- })
289
- );
290
- }
291
- const server = app.listen(port, host, () => {
292
- const displayHost = host === "0.0.0.0" ? "localhost" : host;
293
- const serverUrl = `http://${displayHost}:${port}`;
294
- console.log(`\u{1F680} Server running on ${serverUrl}`);
295
- if (enableSwagger) {
296
- console.log(`\u{1F4DA} Swagger UI: ${serverUrl}${swaggerPath}`);
297
- }
298
- const result = {
299
- server,
300
- app,
301
- url: serverUrl,
302
- port,
303
- host,
304
- close: () => new Promise((closeResolve) => {
305
- server.close(() => {
306
- console.log("Server closed gracefully");
307
- closeResolve();
308
- });
309
- })
310
- };
311
- resolve2(result);
312
- });
313
- server.on("error", (error) => {
314
- if (error.code === "EADDRINUSE") {
315
- reject(new Error(`Port ${port} already in use. Please choose a different port.`));
316
- } else if (error.code === "EACCES") {
317
- reject(new Error(`Permission denied for port ${port}. Ports below 1024 require root privileges.`));
318
- } else {
319
- reject(new Error(`Failed to start server: ${error.message}`));
320
- }
321
- });
322
- } catch (error) {
323
- reject(error);
233
+ // src/adapter/express/openapi.ts
234
+ function toOpenApiPath(path3) {
235
+ return path3.replace(/:([^/]+)/g, "{$1}");
236
+ }
237
+ function getOpenApiOperation(openapi, route) {
238
+ const pathKey = toOpenApiPath(route.fullPath);
239
+ const pathItem = openapi.paths?.[pathKey];
240
+ if (!pathItem) return null;
241
+ return pathItem[route.httpMethod.toLowerCase()] ?? null;
242
+ }
243
+ function getParamSchemaIndex(operation) {
244
+ const index = /* @__PURE__ */ new Map();
245
+ const params = operation?.parameters ?? [];
246
+ for (const param of params) {
247
+ if (!param?.name || !param?.in) continue;
248
+ if (param.schema) {
249
+ index.set(`${param.in}:${param.name}`, param.schema);
324
250
  }
325
- });
251
+ }
252
+ return index;
253
+ }
254
+ function getParamSchemaFromIndex(index, location, name) {
255
+ return index.get(`${location}:${name}`) ?? null;
256
+ }
257
+ function getRequestBodySchema(operation, contentType) {
258
+ const content = operation?.requestBody?.content;
259
+ if (!content) return null;
260
+ if (contentType && content[contentType]?.schema) {
261
+ return content[contentType].schema;
262
+ }
263
+ const first = Object.values(content)[0];
264
+ return first?.schema ?? null;
265
+ }
266
+ function schemaFromType(schemaType) {
267
+ if (!schemaType) return null;
268
+ return { type: schemaType };
269
+ }
270
+ function resolveSchema(schema, components, seen = /* @__PURE__ */ new Set()) {
271
+ const ref = schema.$ref;
272
+ if (typeof ref !== "string" || !ref.startsWith("#/components/schemas/")) {
273
+ return schema;
274
+ }
275
+ const name = ref.replace("#/components/schemas/", "");
276
+ if (seen.has(name)) return schema;
277
+ const next = components[name];
278
+ if (!next) return schema;
279
+ seen.add(name);
280
+ return resolveSchema(next, components, seen);
281
+ }
282
+ function getSchemaByRef(openapi, ref) {
283
+ if (!ref.startsWith("#/components/schemas/")) return null;
284
+ const schemaName = ref.replace("#/components/schemas/", "");
285
+ return openapi.components.schemas[schemaName] || null;
326
286
  }
327
287
 
328
- // src/adapter/express/index.ts
288
+ // src/adapter/express/coercion.ts
329
289
  function normalizeCoerceOptions(coerce) {
330
290
  return {
331
291
  body: coerce?.body ?? false,
@@ -337,273 +297,270 @@ function normalizeCoerceOptions(coerce) {
337
297
  date: coerce?.date ?? false
338
298
  };
339
299
  }
340
- async function createExpressRouter(options) {
341
- const { controllers, artifactsDir = ".adorn", middleware = {} } = options;
342
- let manifest;
343
- let openapi;
344
- let precompiledValidators = null;
345
- if (options.manifest && options.openapi) {
346
- manifest = options.manifest;
347
- openapi = options.openapi;
348
- if (manifest.validation.mode === "precompiled" && manifest.validation.precompiledModule) {
349
- try {
350
- const validatorPath = join(artifactsDir, manifest.validation.precompiledModule);
351
- precompiledValidators = __require(validatorPath).validators;
352
- } catch (err) {
353
- console.warn(`Failed to load precompiled validators: ${err}`);
354
- }
300
+ function getDateCoercionOptions(coerce, location) {
301
+ const enabled = coerce[location];
302
+ return {
303
+ dateTime: enabled && coerce.dateTime,
304
+ date: enabled && coerce.date
305
+ };
306
+ }
307
+ function coerceDatesWithSchema(value, schema, dateCoercion, components) {
308
+ if (!schema || !dateCoercion.date && !dateCoercion.dateTime) return value;
309
+ return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: false });
310
+ }
311
+ function coerceParamValue(value, schema, dateCoercion, components) {
312
+ if (!schema) return value;
313
+ return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: true });
314
+ }
315
+ function coerceWithSchema(value, schema, dateCoercion, components, options) {
316
+ if (value === void 0 || value === null) return value;
317
+ if (value instanceof Date) return value;
318
+ const resolved = resolveSchema(schema, components);
319
+ const override = resolved["x-adorn-coerce"];
320
+ const allowDateTime = override === true ? true : override === false ? false : dateCoercion.dateTime;
321
+ const allowDate = override === true ? true : override === false ? false : dateCoercion.date;
322
+ const byFormat = coerceDateString(value, resolved, allowDateTime, allowDate);
323
+ if (byFormat !== value) return byFormat;
324
+ const allOf = resolved.allOf;
325
+ if (Array.isArray(allOf)) {
326
+ let out = value;
327
+ for (const entry of allOf) {
328
+ out = coerceWithSchema(out, entry, { dateTime: allowDateTime, date: allowDate }, components, options);
355
329
  }
356
- } else {
357
- const artifacts = await loadArtifacts({ outDir: artifactsDir });
358
- manifest = artifacts.manifest;
359
- openapi = artifacts.openapi;
360
- precompiledValidators = artifacts.validators?.validators ?? null;
330
+ return out;
361
331
  }
362
- const routes = bindRoutes({ controllers, manifest });
363
- const validator = precompiledValidators ? null : createValidator();
364
- const router = Router();
365
- const instanceCache = /* @__PURE__ */ new Map();
366
- const coerce = normalizeCoerceOptions(options.coerce);
367
- function getInstance(Ctor) {
368
- if (!instanceCache.has(Ctor)) {
369
- instanceCache.set(Ctor, new Ctor());
332
+ const variants = resolved.oneOf ?? resolved.anyOf;
333
+ if (Array.isArray(variants)) {
334
+ for (const entry of variants) {
335
+ const out = coerceWithSchema(value, entry, { dateTime: allowDateTime, date: allowDate }, components, options);
336
+ if (out !== value) return out;
370
337
  }
371
- return instanceCache.get(Ctor);
372
338
  }
373
- function resolveMiddleware(items, named = {}) {
374
- return items.map((item) => {
375
- if (typeof item === "string") {
376
- const fn = named[item];
377
- if (!fn) {
378
- throw new Error(`Named middleware "${item}" not found in middleware registry`);
379
- }
380
- return fn;
381
- }
382
- return item;
383
- });
339
+ const schemaType = resolved.type;
340
+ const types = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
341
+ if ((types.includes("array") || resolved.items) && Array.isArray(value)) {
342
+ const itemSchema = resolved.items ?? {};
343
+ return value.map((item) => coerceWithSchema(item, itemSchema, { dateTime: allowDateTime, date: allowDate }, components, options));
384
344
  }
385
- function createAuthMiddleware(authConfig, routeAuth, globalSecurity) {
386
- return async (req, res, next) => {
387
- const isPublic = routeAuth === "public";
388
- const hasAuthDecorator = routeAuth && routeAuth !== "public";
389
- const hasGlobalSecurity = globalSecurity && globalSecurity.length > 0;
390
- if (!hasAuthDecorator && !hasGlobalSecurity) {
391
- return next();
392
- }
393
- if (isPublic) {
394
- return next();
395
- }
396
- const authMeta = routeAuth;
397
- const scheme = authMeta.scheme;
398
- const requiredScopes = authMeta.scopes || [];
399
- const isOptional = authMeta.optional ?? false;
400
- const authRuntime = authConfig.schemes[scheme];
401
- if (!authRuntime) {
402
- throw new Error(`Auth scheme "${scheme}" not found in auth configuration`);
403
- }
404
- const result = await authRuntime.authenticate(req);
405
- if (!result) {
406
- if (isOptional) {
407
- req.auth = null;
408
- return next();
345
+ if ((types.includes("object") || resolved.properties || resolved.additionalProperties) && isPlainObject(value)) {
346
+ const props = resolved.properties;
347
+ const out = { ...value };
348
+ if (props) {
349
+ for (const [key, propSchema] of Object.entries(props)) {
350
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
351
+ out[key] = coerceWithSchema(value[key], propSchema, { dateTime: allowDateTime, date: allowDate }, components, options);
409
352
  }
410
- return authRuntime.challenge(res);
411
353
  }
412
- req.auth = result.principal;
413
- if (authRuntime.authorize && requiredScopes.length > 0) {
414
- if (!authRuntime.authorize(result, requiredScopes)) {
415
- res.status(403).json({ error: "Forbidden", message: "Insufficient scopes" });
416
- return;
417
- }
354
+ }
355
+ const additional = resolved.additionalProperties;
356
+ if (additional && typeof additional === "object") {
357
+ for (const [key, entry] of Object.entries(value)) {
358
+ if (props && Object.prototype.hasOwnProperty.call(props, key)) continue;
359
+ out[key] = coerceWithSchema(entry, additional, { dateTime: allowDateTime, date: allowDate }, components, options);
418
360
  }
419
- next();
420
- };
361
+ }
362
+ return out;
421
363
  }
422
- function getSchemaByRef(ref) {
423
- if (!ref.startsWith("#/components/schemas/")) return null;
424
- const schemaName = ref.replace("#/components/schemas/", "");
425
- return openapi.components.schemas[schemaName] || null;
364
+ if (options.coercePrimitives) {
365
+ return coercePrimitiveValue(value, types);
426
366
  }
427
- for (const route of routes) {
428
- const method = route.httpMethod.toLowerCase();
429
- const openapiOperation = getOpenApiOperation(openapi, route);
430
- const paramSchemaIndex = buildParamSchemaIndex(openapiOperation);
431
- const bodySchema = getRequestBodySchema(openapiOperation, route.args.body?.contentType) ?? (route.args.body ? getSchemaByRef(route.args.body.schemaRef) : null);
432
- const coerceBodyDates = getDateCoercionOptions(coerce, "body");
433
- const coerceQueryDates = getDateCoercionOptions(coerce, "query");
434
- const coercePathDates = getDateCoercionOptions(coerce, "path");
435
- const coerceHeaderDates = getDateCoercionOptions(coerce, "header");
436
- const coerceCookieDates = getDateCoercionOptions(coerce, "cookie");
437
- const middlewareChain = [];
438
- if (middleware.global) {
439
- middlewareChain.push(...resolveMiddleware(middleware.global, middleware.named || {}));
440
- }
441
- if (route.controllerUse) {
442
- middlewareChain.push(...resolveMiddleware(route.controllerUse, middleware.named || {}));
443
- }
444
- if (route.use) {
445
- middlewareChain.push(...resolveMiddleware(route.use, middleware.named || {}));
367
+ return value;
368
+ }
369
+ function coerceDateString(value, schema, allowDateTime, allowDate) {
370
+ if (typeof value !== "string") return value;
371
+ const format = schema.format;
372
+ const schemaType = schema.type;
373
+ const types = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
374
+ const allowsString = types.length === 0 || types.includes("string");
375
+ if (format === "date-time" && allowDateTime && allowsString) {
376
+ const parsed = new Date(value);
377
+ if (Number.isNaN(parsed.getTime())) {
378
+ throw new Error(`Invalid date-time: ${value}`);
446
379
  }
447
- if (options.auth) {
448
- const authMw = createAuthMiddleware(options.auth, route.auth, openapi.security || []);
449
- middlewareChain.push(authMw);
380
+ return parsed;
381
+ }
382
+ if (format === "date" && allowDate && allowsString) {
383
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
384
+ throw new Error(`Invalid date: ${value}`);
450
385
  }
451
- router[method](route.fullPath, ...middlewareChain, async (req, res, next) => {
452
- try {
453
- const validationErrors = precompiledValidators ? validateRequestWithPrecompiled(route, req, precompiledValidators) : validateRequest(route, req, openapi, validator);
454
- if (validationErrors) {
455
- return res.status(400).json(formatValidationErrors(validationErrors));
456
- }
457
- const instance = getInstance(route.controllerCtor);
458
- const handler = instance[route.methodName];
459
- if (typeof handler !== "function") {
460
- throw new Error(`Method ${route.methodName} not found on controller`);
461
- }
462
- const args = [];
463
- if (route.args.body) {
464
- const coercedBody = (coerceBodyDates.date || coerceBodyDates.dateTime) && bodySchema ? coerceDatesWithSchema(req.body, bodySchema, coerceBodyDates, openapi.components.schemas) : req.body;
465
- args[route.args.body.index] = coercedBody;
466
- }
467
- for (const pathArg of route.args.path) {
468
- const rawValue = req.params[pathArg.name];
469
- const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", pathArg.name) ?? (pathArg.schemaRef ? getSchemaByRef(pathArg.schemaRef) : null) ?? schemaFromType(pathArg.schemaType);
470
- const coerced = coerceParamValue(rawValue, paramSchema, coercePathDates, openapi.components.schemas);
471
- args[pathArg.index] = coerced;
472
- }
473
- if (route.args.query.length > 0) {
474
- const deepObjectArgs = route.args.query.filter((q) => q.serialization?.style === "deepObject");
475
- const standardArgs = route.args.query.filter((q) => q.serialization?.style !== "deepObject");
476
- if (deepObjectArgs.length > 0) {
477
- const rawQuery = getRawQueryString(req);
478
- const names = new Set(deepObjectArgs.map((q) => q.name));
479
- const parsedDeep = parseDeepObjectParams(rawQuery, names);
480
- for (const q of deepObjectArgs) {
481
- const rawValue = parsedDeep[q.name];
482
- const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
483
- const baseValue = rawValue === void 0 ? {} : rawValue;
484
- const coerced = coerceParamValue(baseValue, paramSchema, coerceQueryDates, openapi.components.schemas);
485
- args[q.index] = coerced;
486
- }
487
- }
488
- if (standardArgs.length > 0) {
489
- const firstQueryIndex = standardArgs[0].index;
490
- const allSameIndex = standardArgs.every((q) => q.index === firstQueryIndex);
491
- if (allSameIndex) {
492
- args[firstQueryIndex] = {};
493
- for (const q of standardArgs) {
494
- const parsed = parseQueryValue(req.query[q.name], q);
495
- const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
496
- const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
497
- args[firstQueryIndex][q.name] = coerced;
498
- }
499
- } else {
500
- for (const q of standardArgs) {
501
- const parsed = parseQueryValue(req.query[q.name], q);
502
- const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
503
- const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
504
- args[q.index] = coerced;
505
- }
506
- }
507
- }
508
- }
509
- if (route.args.headers.length > 0) {
510
- const firstHeaderIndex = route.args.headers[0].index;
511
- const allSameIndex = route.args.headers.every((h) => h.index === firstHeaderIndex);
512
- if (allSameIndex) {
513
- args[firstHeaderIndex] = {};
514
- for (const h of route.args.headers) {
515
- const headerValue = req.headers[h.name.toLowerCase()];
516
- const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(h.schemaRef) : null) ?? schemaFromType(h.schemaType);
517
- const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
518
- args[firstHeaderIndex][h.name] = coerced ?? void 0;
519
- }
520
- } else {
521
- for (const h of route.args.headers) {
522
- const headerValue = req.headers[h.name.toLowerCase()];
523
- const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(h.schemaRef) : null) ?? schemaFromType(h.schemaType);
524
- const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
525
- args[h.index] = coerced ?? void 0;
526
- }
527
- }
528
- }
529
- if (route.args.cookies.length > 0) {
530
- const firstCookieIndex = route.args.cookies[0].index;
531
- const allSameIndex = route.args.cookies.every((c) => c.index === firstCookieIndex);
532
- const cookies = parseCookies(req.headers.cookie);
533
- if (allSameIndex) {
534
- args[firstCookieIndex] = {};
535
- for (const c of route.args.cookies) {
536
- const cookieValue = cookies[c.name];
537
- const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(c.schemaRef) : null) ?? schemaFromType(c.schemaType);
538
- const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
539
- args[firstCookieIndex][c.name] = coerced;
540
- }
541
- } else {
542
- for (const c of route.args.cookies) {
543
- const cookieValue = cookies[c.name];
544
- const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(c.schemaRef) : null) ?? schemaFromType(c.schemaType);
545
- const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
546
- args[c.index] = coerced;
547
- }
548
- }
549
- }
550
- if (args.length === 0) {
551
- args.push(req);
552
- }
553
- const result = await handler.apply(instance, args);
554
- const primaryResponse = route.responses[0];
555
- const status = primaryResponse?.status ?? 200;
556
- res.status(status).json(result);
557
- } catch (error) {
558
- next(error);
559
- }
560
- });
386
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
387
+ if (Number.isNaN(parsed.getTime())) {
388
+ throw new Error(`Invalid date: ${value}`);
389
+ }
390
+ return parsed;
561
391
  }
562
- return router;
392
+ return value;
563
393
  }
564
- function getDateCoercionOptions(coerce, location) {
565
- const enabled = coerce[location];
566
- return {
567
- dateTime: enabled && coerce.dateTime,
568
- date: enabled && coerce.date
569
- };
394
+ function coercePrimitiveValue(value, types) {
395
+ if (value === void 0 || value === null) return value;
396
+ if (types.includes("number") || types.includes("integer")) {
397
+ const num = Number(value);
398
+ if (Number.isNaN(num)) {
399
+ throw new Error(`Invalid number: ${value}`);
400
+ }
401
+ return num;
402
+ }
403
+ if (types.includes("boolean")) {
404
+ if (value === "true") return true;
405
+ if (value === "false") return false;
406
+ if (typeof value === "boolean") return value;
407
+ throw new Error(`Invalid boolean: ${value}`);
408
+ }
409
+ return value;
570
410
  }
571
- function toOpenApiPath(path3) {
572
- return path3.replace(/:([^/]+)/g, "{$1}");
411
+ function isPlainObject(value) {
412
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
413
+ const proto = Object.getPrototypeOf(value);
414
+ return proto === Object.prototype || proto === null;
573
415
  }
574
- function getOpenApiOperation(openapi, route) {
575
- const pathKey = toOpenApiPath(route.fullPath);
576
- const pathItem = openapi.paths?.[pathKey];
577
- if (!pathItem) return null;
578
- return pathItem[route.httpMethod.toLowerCase()] ?? null;
416
+ function parseQueryValue(value, param) {
417
+ if (value === void 0 || value === null) return value;
418
+ const isArray = Array.isArray(param.schemaType) ? param.schemaType.includes("array") : param.schemaType === "array";
419
+ if (!isArray) return value;
420
+ const style = param.serialization?.style ?? "form";
421
+ const explode = param.serialization?.explode ?? true;
422
+ if (Array.isArray(value)) {
423
+ return value;
424
+ }
425
+ if (style === "form") {
426
+ if (explode) {
427
+ return value;
428
+ }
429
+ return value.split(",");
430
+ }
431
+ if (style === "spaceDelimited") {
432
+ return value.split(" ");
433
+ }
434
+ if (style === "pipeDelimited") {
435
+ return value.split("|");
436
+ }
437
+ return value;
579
438
  }
580
- function buildParamSchemaIndex(operation) {
581
- const index = /* @__PURE__ */ new Map();
582
- const params = operation?.parameters ?? [];
583
- for (const param of params) {
584
- if (!param?.name || !param?.in) continue;
585
- if (param.schema) {
586
- index.set(`${param.in}:${param.name}`, param.schema);
439
+ function getRawQueryString(req) {
440
+ const url = req.originalUrl ?? req.url ?? "";
441
+ const index = url.indexOf("?");
442
+ if (index === -1) return "";
443
+ return url.slice(index + 1);
444
+ }
445
+ function parseDeepObjectParams(rawQuery, names) {
446
+ const out = {};
447
+ if (!rawQuery || names.size === 0) return out;
448
+ const params = new URLSearchParams(rawQuery);
449
+ for (const [key, value] of params.entries()) {
450
+ const path3 = parseBracketPath(key);
451
+ if (path3.length === 0) continue;
452
+ const root = path3[0];
453
+ if (!names.has(root)) continue;
454
+ assignDeepValue(out, path3, value);
455
+ }
456
+ return out;
457
+ }
458
+ function parseBracketPath(key) {
459
+ const parts = [];
460
+ let current = "";
461
+ for (let i = 0; i < key.length; i++) {
462
+ const ch = key[i];
463
+ if (ch === "[") {
464
+ if (current) parts.push(current);
465
+ current = "";
466
+ continue;
467
+ }
468
+ if (ch === "]") {
469
+ if (current) parts.push(current);
470
+ current = "";
471
+ continue;
587
472
  }
473
+ current += ch;
588
474
  }
589
- return index;
475
+ if (current) parts.push(current);
476
+ return parts;
590
477
  }
591
- function getParamSchemaFromIndex(index, location, name) {
592
- return index.get(`${location}:${name}`) ?? null;
478
+ function assignDeepValue(target, path3, value) {
479
+ let cursor = target;
480
+ for (let i = 0; i < path3.length; i++) {
481
+ const key = path3[i];
482
+ if (!key) continue;
483
+ const isLast = i === path3.length - 1;
484
+ if (isLast) {
485
+ const existing = cursor[key];
486
+ if (existing === void 0) {
487
+ cursor[key] = value;
488
+ } else if (Array.isArray(existing)) {
489
+ existing.push(value);
490
+ } else {
491
+ cursor[key] = [existing, value];
492
+ }
493
+ return;
494
+ }
495
+ const next = cursor[key];
496
+ if (!isPlainObject(next)) {
497
+ cursor[key] = {};
498
+ }
499
+ cursor = cursor[key];
500
+ }
593
501
  }
594
- function getRequestBodySchema(operation, contentType) {
595
- const content = operation?.requestBody?.content;
596
- if (!content) return null;
597
- if (contentType && content[contentType]?.schema) {
598
- return content[contentType].schema;
502
+ function parseCookies(cookieHeader) {
503
+ if (!cookieHeader) return {};
504
+ const cookies = {};
505
+ const pairs = cookieHeader.split(";");
506
+ for (const pair of pairs) {
507
+ const [name, ...valueParts] = pair.trim().split("=");
508
+ if (name) {
509
+ cookies[name] = valueParts.join("=");
510
+ }
599
511
  }
600
- const first = Object.values(content)[0];
601
- return first?.schema ?? null;
512
+ return cookies;
602
513
  }
603
- function schemaFromType(schemaType) {
604
- if (!schemaType) return null;
605
- return { type: schemaType };
514
+ function normalizeSort(sort) {
515
+ if (Array.isArray(sort)) {
516
+ return sort.map((s) => String(s));
517
+ }
518
+ if (typeof sort === "string") {
519
+ return sort.split(",").map((s) => s.trim()).filter(Boolean);
520
+ }
521
+ return [];
606
522
  }
523
+
524
+ // src/adapter/express/auth.ts
525
+ function createAuthMiddleware(authConfig, routeAuth, globalSecurity) {
526
+ return async (req, res, next) => {
527
+ const isPublic = routeAuth === "public";
528
+ const hasAuthDecorator = routeAuth && routeAuth !== "public";
529
+ const hasGlobalSecurity = globalSecurity && globalSecurity.length > 0;
530
+ if (!hasAuthDecorator && !hasGlobalSecurity) {
531
+ return next();
532
+ }
533
+ if (isPublic) {
534
+ return next();
535
+ }
536
+ const authMeta = routeAuth;
537
+ const scheme = authMeta.scheme;
538
+ const requiredScopes = authMeta.scopes || [];
539
+ const isOptional = authMeta.optional ?? false;
540
+ const authRuntime = authConfig.schemes[scheme];
541
+ if (!authRuntime) {
542
+ throw new Error(`Auth scheme "${scheme}" not found in auth configuration`);
543
+ }
544
+ const result = await authRuntime.authenticate(req);
545
+ if (!result) {
546
+ if (isOptional) {
547
+ req.auth = null;
548
+ return next();
549
+ }
550
+ return authRuntime.challenge(res);
551
+ }
552
+ req.auth = result.principal;
553
+ if (authRuntime.authorize && requiredScopes.length > 0) {
554
+ if (!authRuntime.authorize(result, requiredScopes)) {
555
+ res.status(403).json({ error: "Forbidden", message: "Insufficient scopes" });
556
+ return;
557
+ }
558
+ }
559
+ next();
560
+ };
561
+ }
562
+
563
+ // src/adapter/express/validation.ts
607
564
  function validateRequestWithPrecompiled(route, req, validators) {
608
565
  const errors = [];
609
566
  const deepNames = new Set(route.args.query.filter((q) => q.serialization?.style === "deepObject").map((q) => q.name));
@@ -626,48 +583,31 @@ function validateRequestWithPrecompiled(route, req, validators) {
626
583
  }
627
584
  }
628
585
  for (const q of route.args.query) {
629
- const value = q.serialization?.style === "deepObject" ? deepValues[q.name] : req.query[q.name];
630
- if (value === void 0) continue;
631
- const schema = schemaFromType(q.schemaType) ?? {};
632
- const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, {});
633
- if (Object.keys(schema).length > 0 && coerced !== void 0) {
634
- errors.push({
635
- path: `#/query/${q.name}`,
636
- message: `Schema validation not supported for query params in precompiled mode`,
637
- keyword: "notSupported",
638
- params: {}
639
- });
640
- }
641
- }
642
- for (const p of route.args.path) {
643
- const value = req.params[p.name];
644
- if (value === void 0) continue;
645
- const schema = schemaFromType(p.schemaType) ?? {};
646
- const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, {});
647
- if (Object.keys(schema).length > 0 && coerced !== void 0) {
648
- errors.push({
649
- path: `#/path/${p.name}`,
650
- message: `Schema validation not supported for path params in precompiled mode`,
651
- keyword: "notSupported",
652
- params: {}
653
- });
586
+ let value = q.serialization?.style === "deepObject" ? deepValues[q.name] : req.query[q.name];
587
+ if (q.content === "application/json" && typeof value === "string") {
588
+ try {
589
+ value = JSON.parse(value);
590
+ } catch (e) {
591
+ errors.push({
592
+ path: `#/query/${q.name}`,
593
+ message: `Invalid JSON string`,
594
+ keyword: "json",
595
+ params: {}
596
+ });
597
+ continue;
598
+ }
654
599
  }
655
600
  }
656
601
  return errors.length > 0 ? errors : null;
657
602
  }
658
603
  function validateRequest(route, req, openapi, validator) {
659
- function getSchemaByRef(ref) {
660
- if (!ref.startsWith("#/components/schemas/")) return null;
661
- const schemaName = ref.replace("#/components/schemas/", "");
662
- return openapi.components.schemas[schemaName] || null;
663
- }
664
604
  const openapiOperation = getOpenApiOperation(openapi, route);
665
- const paramSchemaIndex = buildParamSchemaIndex(openapiOperation);
605
+ const paramSchemaIndex = getParamSchemaIndex(openapiOperation);
666
606
  const deepNames = new Set(route.args.query.filter((q) => q.serialization?.style === "deepObject").map((q) => q.name));
667
607
  const deepValues = deepNames.size > 0 ? parseDeepObjectParams(getRawQueryString(req), deepNames) : {};
668
608
  const errors = [];
669
609
  if (route.args.body) {
670
- const bodySchema = getSchemaByRef(route.args.body.schemaRef);
610
+ const bodySchema = getSchemaByRef(openapi, route.args.body.schemaRef);
671
611
  if (bodySchema) {
672
612
  const validate = validator.compile(bodySchema);
673
613
  const valid = validate(req.body);
@@ -684,7 +624,20 @@ function validateRequest(route, req, openapi, validator) {
684
624
  }
685
625
  }
686
626
  for (const q of route.args.query) {
687
- const value = q.serialization?.style === "deepObject" ? deepValues[q.name] : req.query[q.name];
627
+ let value = q.serialization?.style === "deepObject" ? deepValues[q.name] : req.query[q.name];
628
+ if (q.content === "application/json" && typeof value === "string") {
629
+ try {
630
+ value = JSON.parse(value);
631
+ } catch (e) {
632
+ errors.push({
633
+ path: `#/query/${q.name}`,
634
+ message: `Invalid JSON string`,
635
+ keyword: "json",
636
+ params: {}
637
+ });
638
+ continue;
639
+ }
640
+ }
688
641
  const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name);
689
642
  let schema = {};
690
643
  if (openapiSchema) {
@@ -695,7 +648,7 @@ function validateRequest(route, req, openapi, validator) {
695
648
  schema.type = type;
696
649
  }
697
650
  if (q.schemaRef && q.schemaRef.includes("Inline")) {
698
- const inlineSchema = getSchemaByRef(q.schemaRef);
651
+ const inlineSchema = getSchemaByRef(openapi, q.schemaRef);
699
652
  if (inlineSchema) {
700
653
  Object.assign(schema, inlineSchema);
701
654
  }
@@ -729,7 +682,7 @@ function validateRequest(route, req, openapi, validator) {
729
682
  schema.type = type;
730
683
  }
731
684
  if (p.schemaRef && p.schemaRef.includes("Inline")) {
732
- const inlineSchema = getSchemaByRef(p.schemaRef);
685
+ const inlineSchema = getSchemaByRef(openapi, p.schemaRef);
733
686
  if (inlineSchema) {
734
687
  Object.assign(schema, inlineSchema);
735
688
  }
@@ -753,225 +706,213 @@ function validateRequest(route, req, openapi, validator) {
753
706
  }
754
707
  return errors.length > 0 ? errors : null;
755
708
  }
756
- function resolveSchema(schema, components, seen = /* @__PURE__ */ new Set()) {
757
- const ref = schema.$ref;
758
- if (typeof ref !== "string" || !ref.startsWith("#/components/schemas/")) {
759
- return schema;
760
- }
761
- const name = ref.replace("#/components/schemas/", "");
762
- if (seen.has(name)) return schema;
763
- const next = components[name];
764
- if (!next) return schema;
765
- seen.add(name);
766
- return resolveSchema(next, components, seen);
767
- }
768
- function coerceDatesWithSchema(value, schema, dateCoercion, components) {
769
- if (!schema || !dateCoercion.date && !dateCoercion.dateTime) return value;
770
- return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: false });
771
- }
772
- function coerceParamValue(value, schema, dateCoercion, components) {
773
- if (!schema) return value;
774
- return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: true });
775
- }
776
- function coerceWithSchema(value, schema, dateCoercion, components, options) {
777
- if (value === void 0 || value === null) return value;
778
- if (value instanceof Date) return value;
779
- const resolved = resolveSchema(schema, components);
780
- const override = resolved["x-adorn-coerce"];
781
- const allowDateTime = override === true ? true : override === false ? false : dateCoercion.dateTime;
782
- const allowDate = override === true ? true : override === false ? false : dateCoercion.date;
783
- const byFormat = coerceDateString(value, resolved, allowDateTime, allowDate);
784
- if (byFormat !== value) return byFormat;
785
- const allOf = resolved.allOf;
786
- if (Array.isArray(allOf)) {
787
- let out = value;
788
- for (const entry of allOf) {
789
- out = coerceWithSchema(out, entry, { dateTime: allowDateTime, date: allowDate }, components, options);
790
- }
791
- return out;
792
- }
793
- const variants = resolved.oneOf ?? resolved.anyOf;
794
- if (Array.isArray(variants)) {
795
- for (const entry of variants) {
796
- const out = coerceWithSchema(value, entry, { dateTime: allowDateTime, date: allowDate }, components, options);
797
- if (out !== value) return out;
798
- }
799
- }
800
- const schemaType = resolved.type;
801
- const types = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
802
- if ((types.includes("array") || resolved.items) && Array.isArray(value)) {
803
- const itemSchema = resolved.items ?? {};
804
- return value.map((item) => coerceWithSchema(item, itemSchema, { dateTime: allowDateTime, date: allowDate }, components, options));
805
- }
806
- if ((types.includes("object") || resolved.properties || resolved.additionalProperties) && isPlainObject(value)) {
807
- const props = resolved.properties;
808
- const out = { ...value };
809
- if (props) {
810
- for (const [key, propSchema] of Object.entries(props)) {
811
- if (Object.prototype.hasOwnProperty.call(value, key)) {
812
- out[key] = coerceWithSchema(value[key], propSchema, { dateTime: allowDateTime, date: allowDate }, components, options);
813
- }
814
- }
815
- }
816
- const additional = resolved.additionalProperties;
817
- if (additional && typeof additional === "object") {
818
- for (const [key, entry] of Object.entries(value)) {
819
- if (props && Object.prototype.hasOwnProperty.call(props, key)) continue;
820
- out[key] = coerceWithSchema(entry, additional, { dateTime: allowDateTime, date: allowDate }, components, options);
709
+
710
+ // src/adapter/express/router.ts
711
+ async function createExpressRouter(options) {
712
+ const { controllers, artifactsDir = ".adorn", middleware = {}, defaultPageSize = 10 } = options;
713
+ let manifest;
714
+ let openapi;
715
+ let precompiledValidators = null;
716
+ if (options.manifest && options.openapi) {
717
+ manifest = options.manifest;
718
+ openapi = options.openapi;
719
+ if (manifest.validation.mode === "precompiled" && manifest.validation.precompiledModule) {
720
+ try {
721
+ const validatorPath = join(artifactsDir, manifest.validation.precompiledModule);
722
+ precompiledValidators = __require(validatorPath).validators;
723
+ } catch (err) {
724
+ console.warn(`Failed to load precompiled validators: ${err}`);
821
725
  }
822
726
  }
823
- return out;
824
- }
825
- if (options.coercePrimitives) {
826
- return coercePrimitiveValue(value, types);
827
- }
828
- return value;
829
- }
830
- function coerceDateString(value, schema, allowDateTime, allowDate) {
831
- if (typeof value !== "string") return value;
832
- const format = schema.format;
833
- const schemaType = schema.type;
834
- const types = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
835
- const allowsString = types.length === 0 || types.includes("string");
836
- if (format === "date-time" && allowDateTime && allowsString) {
837
- const parsed = new Date(value);
838
- if (Number.isNaN(parsed.getTime())) {
839
- throw new Error(`Invalid date-time: ${value}`);
840
- }
841
- return parsed;
842
- }
843
- if (format === "date" && allowDate && allowsString) {
844
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
845
- throw new Error(`Invalid date: ${value}`);
846
- }
847
- const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
848
- if (Number.isNaN(parsed.getTime())) {
849
- throw new Error(`Invalid date: ${value}`);
850
- }
851
- return parsed;
852
- }
853
- return value;
854
- }
855
- function coercePrimitiveValue(value, types) {
856
- if (value === void 0 || value === null) return value;
857
- if (types.includes("number") || types.includes("integer")) {
858
- const num = Number(value);
859
- if (Number.isNaN(num)) {
860
- throw new Error(`Invalid number: ${value}`);
861
- }
862
- return num;
863
- }
864
- if (types.includes("boolean")) {
865
- if (value === "true") return true;
866
- if (value === "false") return false;
867
- if (typeof value === "boolean") return value;
868
- throw new Error(`Invalid boolean: ${value}`);
869
- }
870
- return value;
871
- }
872
- function isPlainObject(value) {
873
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
874
- const proto = Object.getPrototypeOf(value);
875
- return proto === Object.prototype || proto === null;
876
- }
877
- function parseQueryValue(value, param) {
878
- if (value === void 0 || value === null) return value;
879
- const isArray = Array.isArray(param.schemaType) ? param.schemaType.includes("array") : param.schemaType === "array";
880
- if (!isArray) return value;
881
- const style = param.serialization?.style ?? "form";
882
- const explode = param.serialization?.explode ?? true;
883
- if (Array.isArray(value)) {
884
- return value;
885
- }
886
- if (style === "form") {
887
- if (explode) {
888
- return value;
889
- }
890
- return value.split(",");
891
- }
892
- if (style === "spaceDelimited") {
893
- return value.split(" ");
894
- }
895
- if (style === "pipeDelimited") {
896
- return value.split("|");
897
- }
898
- return value;
899
- }
900
- function getRawQueryString(req) {
901
- const url = req.originalUrl ?? req.url ?? "";
902
- const index = url.indexOf("?");
903
- if (index === -1) return "";
904
- return url.slice(index + 1);
905
- }
906
- function parseDeepObjectParams(rawQuery, names) {
907
- const out = {};
908
- if (!rawQuery || names.size === 0) return out;
909
- const params = new URLSearchParams(rawQuery);
910
- for (const [key, value] of params.entries()) {
911
- const path3 = parseBracketPath(key);
912
- if (path3.length === 0) continue;
913
- const root = path3[0];
914
- if (!names.has(root)) continue;
915
- assignDeepValue(out, path3, value);
916
- }
917
- return out;
918
- }
919
- function parseBracketPath(key) {
920
- const parts = [];
921
- let current = "";
922
- for (let i = 0; i < key.length; i++) {
923
- const ch = key[i];
924
- if (ch === "[") {
925
- if (current) parts.push(current);
926
- current = "";
927
- continue;
928
- }
929
- if (ch === "]") {
930
- if (current) parts.push(current);
931
- current = "";
932
- continue;
727
+ } else {
728
+ const artifacts = await loadArtifacts({ outDir: artifactsDir });
729
+ manifest = artifacts.manifest;
730
+ openapi = artifacts.openapi;
731
+ precompiledValidators = artifacts.validators?.validators ?? null;
732
+ }
733
+ const routes = bindRoutes({ controllers, manifest });
734
+ const validator = precompiledValidators ? null : createValidator();
735
+ const router = Router();
736
+ const instanceCache = /* @__PURE__ */ new Map();
737
+ const coerce = normalizeCoerceOptions(options.coerce);
738
+ function getInstance(Ctor) {
739
+ if (!instanceCache.has(Ctor)) {
740
+ instanceCache.set(Ctor, new Ctor());
933
741
  }
934
- current += ch;
742
+ return instanceCache.get(Ctor);
935
743
  }
936
- if (current) parts.push(current);
937
- return parts;
938
- }
939
- function assignDeepValue(target, path3, value) {
940
- let cursor = target;
941
- for (let i = 0; i < path3.length; i++) {
942
- const key = path3[i];
943
- if (!key) continue;
944
- const isLast = i === path3.length - 1;
945
- if (isLast) {
946
- const existing = cursor[key];
947
- if (existing === void 0) {
948
- cursor[key] = value;
949
- } else if (Array.isArray(existing)) {
950
- existing.push(value);
951
- } else {
952
- cursor[key] = [existing, value];
744
+ function resolveMiddleware(items, named = {}) {
745
+ return items.map((item) => {
746
+ if (typeof item === "string") {
747
+ const fn = named[item];
748
+ if (!fn) {
749
+ throw new Error(`Named middleware "${item}" not found in middleware registry`);
750
+ }
751
+ return fn;
953
752
  }
954
- return;
753
+ return item;
754
+ });
755
+ }
756
+ for (const route of routes) {
757
+ const method = route.httpMethod.toLowerCase();
758
+ const openapiOperation = getOpenApiOperation(openapi, route);
759
+ const paramSchemaIndex = getParamSchemaIndex(openapiOperation);
760
+ const bodySchema = getRequestBodySchema(openapiOperation, route.args.body?.contentType) ?? (route.args.body ? getSchemaByRef(openapi, route.args.body.schemaRef) : null);
761
+ const coerceBodyDates = getDateCoercionOptions(coerce, "body");
762
+ const coerceQueryDates = getDateCoercionOptions(coerce, "query");
763
+ const coercePathDates = getDateCoercionOptions(coerce, "path");
764
+ const coerceHeaderDates = getDateCoercionOptions(coerce, "header");
765
+ const coerceCookieDates = getDateCoercionOptions(coerce, "cookie");
766
+ const middlewareChain = [];
767
+ if (middleware.global) {
768
+ middlewareChain.push(...resolveMiddleware(middleware.global, middleware.named || {}));
955
769
  }
956
- const next = cursor[key];
957
- if (!isPlainObject(next)) {
958
- cursor[key] = {};
770
+ if (route.controllerUse) {
771
+ middlewareChain.push(...resolveMiddleware(route.controllerUse, middleware.named || {}));
959
772
  }
960
- cursor = cursor[key];
961
- }
962
- }
963
- function parseCookies(cookieHeader) {
964
- if (!cookieHeader) return {};
965
- const cookies = {};
966
- const pairs = cookieHeader.split(";");
967
- for (const pair of pairs) {
968
- const [name, ...valueParts] = pair.trim().split("=");
969
- if (name) {
970
- cookies[name] = valueParts.join("=");
773
+ if (route.use) {
774
+ middlewareChain.push(...resolveMiddleware(route.use, middleware.named || {}));
775
+ }
776
+ if (options.auth) {
777
+ const authMw = createAuthMiddleware(options.auth, route.auth, openapi.security || []);
778
+ middlewareChain.push(authMw);
971
779
  }
780
+ router[method](route.fullPath, ...middlewareChain, async (req, res, next) => {
781
+ try {
782
+ const validationErrors = precompiledValidators ? validateRequestWithPrecompiled(route, req, precompiledValidators) : validateRequest(route, req, openapi, validator);
783
+ if (validationErrors) {
784
+ return res.status(400).json(formatValidationErrors(validationErrors));
785
+ }
786
+ const instance = getInstance(route.controllerCtor);
787
+ const handler = instance[route.methodName];
788
+ if (typeof handler !== "function") {
789
+ throw new Error(`Method ${route.methodName} not found on controller`);
790
+ }
791
+ const args = [];
792
+ if (route.args.body) {
793
+ const coercedBody = (coerceBodyDates.date || coerceBodyDates.dateTime) && bodySchema ? coerceDatesWithSchema(req.body, bodySchema, coerceBodyDates, openapi.components.schemas) : req.body;
794
+ args[route.args.body.index] = coercedBody;
795
+ }
796
+ for (const pathArg of route.args.path) {
797
+ const rawValue = req.params[pathArg.name];
798
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", pathArg.name) ?? (pathArg.schemaRef ? getSchemaByRef(openapi, pathArg.schemaRef) : null) ?? schemaFromType(pathArg.schemaType);
799
+ const coerced = coerceParamValue(rawValue, paramSchema, coercePathDates, openapi.components.schemas);
800
+ args[pathArg.index] = coerced;
801
+ }
802
+ if (route.args.query.length > 0) {
803
+ const jsonArgs = route.args.query.filter((q) => q.content === "application/json");
804
+ const standardArgs = route.args.query.filter((q) => q.content !== "application/json");
805
+ const queryArgIndex = standardArgs[0]?.index;
806
+ if (queryArgIndex !== void 0) {
807
+ args[queryArgIndex] = {};
808
+ }
809
+ if (jsonArgs.length > 0) {
810
+ for (const q of jsonArgs) {
811
+ const rawValue = req.query[q.name];
812
+ if (rawValue === void 0 || rawValue === null) continue;
813
+ let parsed = rawValue;
814
+ if (typeof rawValue === "string" && rawValue.length > 0) {
815
+ try {
816
+ parsed = JSON.parse(rawValue);
817
+ } catch (e) {
818
+ parsed = rawValue;
819
+ }
820
+ }
821
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(openapi, q.schemaRef) : null) ?? schemaFromType(q.schemaType);
822
+ const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
823
+ if (!args[q.index] || typeof args[q.index] !== "object") {
824
+ args[q.index] = {};
825
+ }
826
+ Object.assign(args[q.index], coerced);
827
+ }
828
+ }
829
+ if (standardArgs.length > 0) {
830
+ for (const q of standardArgs) {
831
+ const rawValue = req.query[q.name];
832
+ if (rawValue === void 0) continue;
833
+ const parsed = parseQueryValue(rawValue, q);
834
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(openapi, q.schemaRef) : null) ?? schemaFromType(q.schemaType);
835
+ const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
836
+ if (!args[q.index] || typeof args[q.index] !== "object") {
837
+ args[q.index] = {};
838
+ }
839
+ args[q.index][q.name] = coerced;
840
+ }
841
+ }
842
+ if (queryArgIndex !== void 0 && args[queryArgIndex]) {
843
+ const queryObj = args[queryArgIndex];
844
+ if (queryObj.page === void 0) {
845
+ queryObj.page = 1;
846
+ }
847
+ if (queryObj.pageSize === void 0) {
848
+ queryObj.pageSize = defaultPageSize;
849
+ }
850
+ if (queryObj.sort) {
851
+ queryObj.sort = normalizeSort(queryObj.sort);
852
+ }
853
+ }
854
+ }
855
+ if (route.args.headers.length > 0) {
856
+ const firstHeaderIndex = route.args.headers[0].index;
857
+ const allSameIndex = route.args.headers.every((h) => h.index === firstHeaderIndex);
858
+ if (allSameIndex) {
859
+ args[firstHeaderIndex] = {};
860
+ for (const h of route.args.headers) {
861
+ const headerValue = req.headers[h.name.toLowerCase()];
862
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(openapi, h.schemaRef) : null) ?? schemaFromType(h.schemaType);
863
+ const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
864
+ args[firstHeaderIndex][h.name] = coerced ?? void 0;
865
+ }
866
+ } else {
867
+ for (const h of route.args.headers) {
868
+ const headerValue = req.headers[h.name.toLowerCase()];
869
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(openapi, h.schemaRef) : null) ?? schemaFromType(h.schemaType);
870
+ const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
871
+ args[h.index] = coerced ?? void 0;
872
+ }
873
+ }
874
+ }
875
+ if (route.args.cookies.length > 0) {
876
+ const firstCookieIndex = route.args.cookies[0].index;
877
+ const allSameIndex = route.args.cookies.every((c) => c.index === firstCookieIndex);
878
+ const cookies = parseCookies(req.headers.cookie);
879
+ if (allSameIndex) {
880
+ args[firstCookieIndex] = {};
881
+ for (const c of route.args.cookies) {
882
+ const cookieValue = cookies[c.name];
883
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(openapi, c.name) : null) ?? schemaFromType(c.schemaType);
884
+ const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
885
+ args[firstCookieIndex][c.name] = coerced;
886
+ }
887
+ } else {
888
+ for (const c of route.args.cookies) {
889
+ const cookieValue = cookies[c.name];
890
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(openapi, c.name) : null) ?? schemaFromType(c.schemaType);
891
+ const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
892
+ args[c.index] = coerced;
893
+ }
894
+ }
895
+ }
896
+ if (args.length === 0) {
897
+ args.push(req);
898
+ }
899
+ const result = await handler.apply(instance, args);
900
+ const primaryResponse = route.responses[0];
901
+ const status = primaryResponse?.status ?? 200;
902
+ res.status(status).json(result);
903
+ } catch (error) {
904
+ next(error);
905
+ }
906
+ });
972
907
  }
973
- return cookies;
908
+ return router;
974
909
  }
910
+
911
+ // src/adapter/express/swagger.ts
912
+ import { Router as Router2 } from "express";
913
+ import { readFileSync } from "fs";
914
+ import { resolve, isAbsolute } from "path";
915
+ import swaggerUi from "swagger-ui-express";
975
916
  function setupSwagger(options = {}) {
976
917
  const {
977
918
  artifactsDir = ".adorn",
@@ -979,7 +920,7 @@ function setupSwagger(options = {}) {
979
920
  uiPath = "/docs",
980
921
  swaggerOptions = {}
981
922
  } = options;
982
- const router = Router();
923
+ const router = Router2();
983
924
  router.get(jsonPath, (req, res) => {
984
925
  const openApiPath = isAbsolute(artifactsDir) ? resolve(artifactsDir, "openapi.json") : resolve(process.cwd(), artifactsDir, "openapi.json");
985
926
  const content = readFileSync(openApiPath, "utf-8");
@@ -994,6 +935,97 @@ function setupSwagger(options = {}) {
994
935
  }));
995
936
  return router;
996
937
  }
938
+
939
+ // src/adapter/express/bootstrap.ts
940
+ import express from "express";
941
+ import path2 from "path";
942
+ function bootstrap(options) {
943
+ return new Promise(async (resolve2, reject) => {
944
+ try {
945
+ const {
946
+ controllers,
947
+ port: userPort,
948
+ host: userHost,
949
+ artifactsDir: userArtifactsDir = ".adorn",
950
+ enableSwagger = true,
951
+ swaggerPath = "/docs",
952
+ swaggerJsonPath = "/docs/openapi.json",
953
+ middleware,
954
+ auth,
955
+ coerce
956
+ } = options;
957
+ if (controllers.length === 0) {
958
+ reject(new Error("At least one controller must be provided to bootstrap()."));
959
+ return;
960
+ }
961
+ const envPort = process.env.PORT;
962
+ const port = userPort ?? (envPort !== void 0 ? Number(envPort) : 3e3);
963
+ const host = userHost ?? process.env.HOST ?? "0.0.0.0";
964
+ if (isNaN(port) || port < 0 || port > 65535) {
965
+ reject(new Error(`Invalid port: ${port}. Port must be between 0 and 65535.`));
966
+ return;
967
+ }
968
+ const absoluteArtifactsDir = path2.isAbsolute(userArtifactsDir) ? userArtifactsDir : path2.resolve(process.cwd(), userArtifactsDir);
969
+ const app = express();
970
+ app.use(express.json());
971
+ const router = await createExpressRouter({
972
+ controllers,
973
+ artifactsDir: absoluteArtifactsDir,
974
+ middleware,
975
+ auth,
976
+ coerce
977
+ });
978
+ app.use(router);
979
+ if (enableSwagger) {
980
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
981
+ const serverUrl = `http://${displayHost}:${port}`;
982
+ app.use(
983
+ setupSwagger({
984
+ artifactsDir: absoluteArtifactsDir,
985
+ jsonPath: swaggerJsonPath,
986
+ uiPath: swaggerPath,
987
+ swaggerOptions: {
988
+ servers: [{ url: serverUrl }]
989
+ }
990
+ })
991
+ );
992
+ }
993
+ const server = app.listen(port, host, () => {
994
+ const displayHost = host === "0.0.0.0" ? "localhost" : host;
995
+ const serverUrl = `http://${displayHost}:${port}`;
996
+ console.log(`\u{1F680} Server running on ${serverUrl}`);
997
+ if (enableSwagger) {
998
+ console.log(`\u{1F4DA} Swagger UI: ${serverUrl}${swaggerPath}`);
999
+ }
1000
+ const result = {
1001
+ server,
1002
+ app,
1003
+ url: serverUrl,
1004
+ port,
1005
+ host,
1006
+ close: () => new Promise((closeResolve) => {
1007
+ server.close(() => {
1008
+ console.log("Server closed gracefully");
1009
+ closeResolve();
1010
+ });
1011
+ })
1012
+ };
1013
+ resolve2(result);
1014
+ });
1015
+ server.on("error", (error) => {
1016
+ if (error.code === "EADDRINUSE") {
1017
+ reject(new Error(`Port ${port} already in use. Please choose a different port.`));
1018
+ } else if (error.code === "EACCES") {
1019
+ reject(new Error(`Permission denied for port ${port}. Ports below 1024 require root privileges.`));
1020
+ } else {
1021
+ reject(new Error(`Failed to start server: ${error.message}`));
1022
+ }
1023
+ });
1024
+ } catch (error) {
1025
+ reject(error);
1026
+ }
1027
+ });
1028
+ }
997
1029
  export {
998
1030
  ValidationErrorResponse,
999
1031
  bindRoutes,