adorn-api 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/express.js CHANGED
@@ -249,7 +249,8 @@ function bootstrap(options) {
249
249
  swaggerPath = "/docs",
250
250
  swaggerJsonPath = "/docs/openapi.json",
251
251
  middleware,
252
- auth
252
+ auth,
253
+ coerce
253
254
  } = options;
254
255
  if (controllers.length === 0) {
255
256
  reject(new Error("At least one controller must be provided to bootstrap()."));
@@ -269,7 +270,8 @@ function bootstrap(options) {
269
270
  controllers,
270
271
  artifactsDir: absoluteArtifactsDir,
271
272
  middleware,
272
- auth
273
+ auth,
274
+ coerce
273
275
  });
274
276
  app.use(router);
275
277
  if (enableSwagger) {
@@ -324,6 +326,17 @@ function bootstrap(options) {
324
326
  }
325
327
 
326
328
  // src/adapter/express/index.ts
329
+ function normalizeCoerceOptions(coerce) {
330
+ return {
331
+ body: coerce?.body ?? false,
332
+ query: coerce?.query ?? false,
333
+ path: coerce?.path ?? false,
334
+ header: coerce?.header ?? false,
335
+ cookie: coerce?.cookie ?? false,
336
+ dateTime: coerce?.dateTime ?? false,
337
+ date: coerce?.date ?? false
338
+ };
339
+ }
327
340
  async function createExpressRouter(options) {
328
341
  const { controllers, artifactsDir = ".adorn", middleware = {} } = options;
329
342
  let manifest;
@@ -350,6 +363,7 @@ async function createExpressRouter(options) {
350
363
  const validator = precompiledValidators ? null : createValidator();
351
364
  const router = Router();
352
365
  const instanceCache = /* @__PURE__ */ new Map();
366
+ const coerce = normalizeCoerceOptions(options.coerce);
353
367
  function getInstance(Ctor) {
354
368
  if (!instanceCache.has(Ctor)) {
355
369
  instanceCache.set(Ctor, new Ctor());
@@ -412,6 +426,14 @@ async function createExpressRouter(options) {
412
426
  }
413
427
  for (const route of routes) {
414
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");
415
437
  const middlewareChain = [];
416
438
  if (middleware.global) {
417
439
  middlewareChain.push(...resolveMiddleware(middleware.global, middleware.named || {}));
@@ -439,10 +461,13 @@ async function createExpressRouter(options) {
439
461
  }
440
462
  const args = [];
441
463
  if (route.args.body) {
442
- args[route.args.body.index] = req.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;
443
466
  }
444
467
  for (const pathArg of route.args.path) {
445
- const coerced = coerceValue(req.params[pathArg.name], pathArg.schemaType);
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);
446
471
  args[pathArg.index] = coerced;
447
472
  }
448
473
  if (route.args.query.length > 0) {
@@ -452,13 +477,15 @@ async function createExpressRouter(options) {
452
477
  args[firstQueryIndex] = {};
453
478
  for (const q of route.args.query) {
454
479
  const parsed = parseQueryValue(req.query[q.name], q);
455
- const coerced = coerceValue(parsed, q.schemaType);
480
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
481
+ const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
456
482
  args[firstQueryIndex][q.name] = coerced;
457
483
  }
458
484
  } else {
459
485
  for (const q of route.args.query) {
460
486
  const parsed = parseQueryValue(req.query[q.name], q);
461
- const coerced = coerceValue(parsed, q.schemaType);
487
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
488
+ const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
462
489
  args[q.index] = coerced;
463
490
  }
464
491
  }
@@ -470,12 +497,16 @@ async function createExpressRouter(options) {
470
497
  args[firstHeaderIndex] = {};
471
498
  for (const h of route.args.headers) {
472
499
  const headerValue = req.headers[h.name.toLowerCase()];
473
- args[firstHeaderIndex][h.name] = headerValue ?? void 0;
500
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(h.schemaRef) : null) ?? schemaFromType(h.schemaType);
501
+ const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
502
+ args[firstHeaderIndex][h.name] = coerced ?? void 0;
474
503
  }
475
504
  } else {
476
505
  for (const h of route.args.headers) {
477
506
  const headerValue = req.headers[h.name.toLowerCase()];
478
- args[h.index] = headerValue ?? void 0;
507
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(h.schemaRef) : null) ?? schemaFromType(h.schemaType);
508
+ const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
509
+ args[h.index] = coerced ?? void 0;
479
510
  }
480
511
  }
481
512
  }
@@ -487,13 +518,15 @@ async function createExpressRouter(options) {
487
518
  args[firstCookieIndex] = {};
488
519
  for (const c of route.args.cookies) {
489
520
  const cookieValue = cookies[c.name];
490
- const coerced = coerceValue(cookieValue, c.schemaType);
521
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(c.schemaRef) : null) ?? schemaFromType(c.schemaType);
522
+ const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
491
523
  args[firstCookieIndex][c.name] = coerced;
492
524
  }
493
525
  } else {
494
526
  for (const c of route.args.cookies) {
495
527
  const cookieValue = cookies[c.name];
496
- const coerced = coerceValue(cookieValue, c.schemaType);
528
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(c.schemaRef) : null) ?? schemaFromType(c.schemaType);
529
+ const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
497
530
  args[c.index] = coerced;
498
531
  }
499
532
  }
@@ -512,6 +545,49 @@ async function createExpressRouter(options) {
512
545
  }
513
546
  return router;
514
547
  }
548
+ function getDateCoercionOptions(coerce, location) {
549
+ const enabled = coerce[location];
550
+ return {
551
+ dateTime: enabled && coerce.dateTime,
552
+ date: enabled && coerce.date
553
+ };
554
+ }
555
+ function toOpenApiPath(path3) {
556
+ return path3.replace(/:([^/]+)/g, "{$1}");
557
+ }
558
+ function getOpenApiOperation(openapi, route) {
559
+ const pathKey = toOpenApiPath(route.fullPath);
560
+ const pathItem = openapi.paths?.[pathKey];
561
+ if (!pathItem) return null;
562
+ return pathItem[route.httpMethod.toLowerCase()] ?? null;
563
+ }
564
+ function buildParamSchemaIndex(operation) {
565
+ const index = /* @__PURE__ */ new Map();
566
+ const params = operation?.parameters ?? [];
567
+ for (const param of params) {
568
+ if (!param?.name || !param?.in) continue;
569
+ if (param.schema) {
570
+ index.set(`${param.in}:${param.name}`, param.schema);
571
+ }
572
+ }
573
+ return index;
574
+ }
575
+ function getParamSchemaFromIndex(index, location, name) {
576
+ return index.get(`${location}:${name}`) ?? null;
577
+ }
578
+ function getRequestBodySchema(operation, contentType) {
579
+ const content = operation?.requestBody?.content;
580
+ if (!content) return null;
581
+ if (contentType && content[contentType]?.schema) {
582
+ return content[contentType].schema;
583
+ }
584
+ const first = Object.values(content)[0];
585
+ return first?.schema ?? null;
586
+ }
587
+ function schemaFromType(schemaType) {
588
+ if (!schemaType) return null;
589
+ return { type: schemaType };
590
+ }
515
591
  function validateRequestWithPrecompiled(route, req, validators) {
516
592
  const errors = [];
517
593
  if (route.args.body) {
@@ -534,12 +610,8 @@ function validateRequestWithPrecompiled(route, req, validators) {
534
610
  for (const q of route.args.query) {
535
611
  const value = req.query[q.name];
536
612
  if (value === void 0) continue;
537
- const schema = {};
538
- if (q.schemaType) {
539
- const type = Array.isArray(q.schemaType) ? q.schemaType[0] : q.schemaType;
540
- schema.type = type;
541
- }
542
- const coerced = coerceValue(value, q.schemaType);
613
+ const schema = schemaFromType(q.schemaType) ?? {};
614
+ const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, {});
543
615
  if (Object.keys(schema).length > 0 && coerced !== void 0) {
544
616
  errors.push({
545
617
  path: `#/query/${q.name}`,
@@ -552,12 +624,8 @@ function validateRequestWithPrecompiled(route, req, validators) {
552
624
  for (const p of route.args.path) {
553
625
  const value = req.params[p.name];
554
626
  if (value === void 0) continue;
555
- const schema = {};
556
- if (p.schemaType) {
557
- const type = Array.isArray(p.schemaType) ? p.schemaType[0] : p.schemaType;
558
- schema.type = type;
559
- }
560
- const coerced = coerceValue(value, p.schemaType);
627
+ const schema = schemaFromType(p.schemaType) ?? {};
628
+ const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, {});
561
629
  if (Object.keys(schema).length > 0 && coerced !== void 0) {
562
630
  errors.push({
563
631
  path: `#/path/${p.name}`,
@@ -575,6 +643,8 @@ function validateRequest(route, req, openapi, validator) {
575
643
  const schemaName = ref.replace("#/components/schemas/", "");
576
644
  return openapi.components.schemas[schemaName] || null;
577
645
  }
646
+ const openapiOperation = getOpenApiOperation(openapi, route);
647
+ const paramSchemaIndex = buildParamSchemaIndex(openapiOperation);
578
648
  const errors = [];
579
649
  if (route.args.body) {
580
650
  const bodySchema = getSchemaByRef(route.args.body.schemaRef);
@@ -595,18 +665,23 @@ function validateRequest(route, req, openapi, validator) {
595
665
  }
596
666
  for (const q of route.args.query) {
597
667
  const value = req.query[q.name];
598
- const schema = {};
599
- if (q.schemaType) {
600
- const type = Array.isArray(q.schemaType) ? q.schemaType[0] : q.schemaType;
601
- schema.type = type;
602
- }
603
- if (q.schemaRef && q.schemaRef.includes("Inline")) {
604
- const inlineSchema = getSchemaByRef(q.schemaRef);
605
- if (inlineSchema) {
606
- Object.assign(schema, inlineSchema);
668
+ const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name);
669
+ let schema = {};
670
+ if (openapiSchema) {
671
+ schema = resolveSchema(openapiSchema, openapi.components.schemas);
672
+ } else {
673
+ if (q.schemaType) {
674
+ const type = Array.isArray(q.schemaType) ? q.schemaType[0] : q.schemaType;
675
+ schema.type = type;
676
+ }
677
+ if (q.schemaRef && q.schemaRef.includes("Inline")) {
678
+ const inlineSchema = getSchemaByRef(q.schemaRef);
679
+ if (inlineSchema) {
680
+ Object.assign(schema, inlineSchema);
681
+ }
607
682
  }
608
683
  }
609
- const coerced = coerceValue(value, q.schemaType);
684
+ const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, openapi.components.schemas);
610
685
  if (Object.keys(schema).length > 0 && coerced !== void 0) {
611
686
  const validate = validator.compile(schema);
612
687
  const valid = validate(coerced);
@@ -624,18 +699,23 @@ function validateRequest(route, req, openapi, validator) {
624
699
  }
625
700
  for (const p of route.args.path) {
626
701
  const value = req.params[p.name];
627
- const schema = {};
628
- if (p.schemaType) {
629
- const type = Array.isArray(p.schemaType) ? p.schemaType[0] : p.schemaType;
630
- schema.type = type;
631
- }
632
- if (p.schemaRef && p.schemaRef.includes("Inline")) {
633
- const inlineSchema = getSchemaByRef(p.schemaRef);
634
- if (inlineSchema) {
635
- Object.assign(schema, inlineSchema);
702
+ const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", p.name);
703
+ let schema = {};
704
+ if (openapiSchema) {
705
+ schema = resolveSchema(openapiSchema, openapi.components.schemas);
706
+ } else {
707
+ if (p.schemaType) {
708
+ const type = Array.isArray(p.schemaType) ? p.schemaType[0] : p.schemaType;
709
+ schema.type = type;
710
+ }
711
+ if (p.schemaRef && p.schemaRef.includes("Inline")) {
712
+ const inlineSchema = getSchemaByRef(p.schemaRef);
713
+ if (inlineSchema) {
714
+ Object.assign(schema, inlineSchema);
715
+ }
636
716
  }
637
717
  }
638
- const coerced = coerceValue(value, p.schemaType);
718
+ const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, openapi.components.schemas);
639
719
  if (Object.keys(schema).length > 0 && coerced !== void 0) {
640
720
  const validate = validator.compile(schema);
641
721
  const valid = validate(coerced);
@@ -653,23 +733,127 @@ function validateRequest(route, req, openapi, validator) {
653
733
  }
654
734
  return errors.length > 0 ? errors : null;
655
735
  }
656
- function coerceValue(value, schemaType) {
736
+ function resolveSchema(schema, components, seen = /* @__PURE__ */ new Set()) {
737
+ const ref = schema.$ref;
738
+ if (typeof ref !== "string" || !ref.startsWith("#/components/schemas/")) {
739
+ return schema;
740
+ }
741
+ const name = ref.replace("#/components/schemas/", "");
742
+ if (seen.has(name)) return schema;
743
+ const next = components[name];
744
+ if (!next) return schema;
745
+ seen.add(name);
746
+ return resolveSchema(next, components, seen);
747
+ }
748
+ function coerceDatesWithSchema(value, schema, dateCoercion, components) {
749
+ if (!schema || !dateCoercion.date && !dateCoercion.dateTime) return value;
750
+ return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: false });
751
+ }
752
+ function coerceParamValue(value, schema, dateCoercion, components) {
753
+ if (!schema) return value;
754
+ return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: true });
755
+ }
756
+ function coerceWithSchema(value, schema, dateCoercion, components, options) {
757
+ if (value === void 0 || value === null) return value;
758
+ if (value instanceof Date) return value;
759
+ const resolved = resolveSchema(schema, components);
760
+ const override = resolved["x-adorn-coerce"];
761
+ const allowDateTime = override === true ? true : override === false ? false : dateCoercion.dateTime;
762
+ const allowDate = override === true ? true : override === false ? false : dateCoercion.date;
763
+ const byFormat = coerceDateString(value, resolved, allowDateTime, allowDate);
764
+ if (byFormat !== value) return byFormat;
765
+ const allOf = resolved.allOf;
766
+ if (Array.isArray(allOf)) {
767
+ let out = value;
768
+ for (const entry of allOf) {
769
+ out = coerceWithSchema(out, entry, { dateTime: allowDateTime, date: allowDate }, components, options);
770
+ }
771
+ return out;
772
+ }
773
+ const variants = resolved.oneOf ?? resolved.anyOf;
774
+ if (Array.isArray(variants)) {
775
+ for (const entry of variants) {
776
+ const out = coerceWithSchema(value, entry, { dateTime: allowDateTime, date: allowDate }, components, options);
777
+ if (out !== value) return out;
778
+ }
779
+ }
780
+ const schemaType = resolved.type;
781
+ const types = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
782
+ if ((types.includes("array") || resolved.items) && Array.isArray(value)) {
783
+ const itemSchema = resolved.items ?? {};
784
+ return value.map((item) => coerceWithSchema(item, itemSchema, { dateTime: allowDateTime, date: allowDate }, components, options));
785
+ }
786
+ if ((types.includes("object") || resolved.properties || resolved.additionalProperties) && isPlainObject(value)) {
787
+ const props = resolved.properties;
788
+ const out = { ...value };
789
+ if (props) {
790
+ for (const [key, propSchema] of Object.entries(props)) {
791
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
792
+ out[key] = coerceWithSchema(value[key], propSchema, { dateTime: allowDateTime, date: allowDate }, components, options);
793
+ }
794
+ }
795
+ }
796
+ const additional = resolved.additionalProperties;
797
+ if (additional && typeof additional === "object") {
798
+ for (const [key, entry] of Object.entries(value)) {
799
+ if (props && Object.prototype.hasOwnProperty.call(props, key)) continue;
800
+ out[key] = coerceWithSchema(entry, additional, { dateTime: allowDateTime, date: allowDate }, components, options);
801
+ }
802
+ }
803
+ return out;
804
+ }
805
+ if (options.coercePrimitives) {
806
+ return coercePrimitiveValue(value, types);
807
+ }
808
+ return value;
809
+ }
810
+ function coerceDateString(value, schema, allowDateTime, allowDate) {
811
+ if (typeof value !== "string") return value;
812
+ const format = schema.format;
813
+ const schemaType = schema.type;
814
+ const types = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
815
+ const allowsString = types.length === 0 || types.includes("string");
816
+ if (format === "date-time" && allowDateTime && allowsString) {
817
+ const parsed = new Date(value);
818
+ if (Number.isNaN(parsed.getTime())) {
819
+ throw new Error(`Invalid date-time: ${value}`);
820
+ }
821
+ return parsed;
822
+ }
823
+ if (format === "date" && allowDate && allowsString) {
824
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
825
+ throw new Error(`Invalid date: ${value}`);
826
+ }
827
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
828
+ if (Number.isNaN(parsed.getTime())) {
829
+ throw new Error(`Invalid date: ${value}`);
830
+ }
831
+ return parsed;
832
+ }
833
+ return value;
834
+ }
835
+ function coercePrimitiveValue(value, types) {
657
836
  if (value === void 0 || value === null) return value;
658
- const type = Array.isArray(schemaType) ? schemaType[0] : schemaType;
659
- if (type === "number" || type === "integer") {
837
+ if (types.includes("number") || types.includes("integer")) {
660
838
  const num = Number(value);
661
- if (isNaN(num)) {
839
+ if (Number.isNaN(num)) {
662
840
  throw new Error(`Invalid number: ${value}`);
663
841
  }
664
842
  return num;
665
843
  }
666
- if (type === "boolean") {
844
+ if (types.includes("boolean")) {
667
845
  if (value === "true") return true;
668
846
  if (value === "false") return false;
847
+ if (typeof value === "boolean") return value;
669
848
  throw new Error(`Invalid boolean: ${value}`);
670
849
  }
671
850
  return value;
672
851
  }
852
+ function isPlainObject(value) {
853
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
854
+ const proto = Object.getPrototypeOf(value);
855
+ return proto === Object.prototype || proto === null;
856
+ }
673
857
  function parseQueryValue(value, param) {
674
858
  if (value === void 0 || value === null) return value;
675
859
  const isArray = Array.isArray(param.schemaType) ? param.schemaType.includes("array") : param.schemaType === "array";