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.cjs CHANGED
@@ -284,7 +284,8 @@ function bootstrap(options) {
284
284
  swaggerPath = "/docs",
285
285
  swaggerJsonPath = "/docs/openapi.json",
286
286
  middleware,
287
- auth
287
+ auth,
288
+ coerce
288
289
  } = options;
289
290
  if (controllers.length === 0) {
290
291
  reject(new Error("At least one controller must be provided to bootstrap()."));
@@ -304,7 +305,8 @@ function bootstrap(options) {
304
305
  controllers,
305
306
  artifactsDir: absoluteArtifactsDir,
306
307
  middleware,
307
- auth
308
+ auth,
309
+ coerce
308
310
  });
309
311
  app.use(router);
310
312
  if (enableSwagger) {
@@ -359,6 +361,17 @@ function bootstrap(options) {
359
361
  }
360
362
 
361
363
  // src/adapter/express/index.ts
364
+ function normalizeCoerceOptions(coerce) {
365
+ return {
366
+ body: coerce?.body ?? false,
367
+ query: coerce?.query ?? false,
368
+ path: coerce?.path ?? false,
369
+ header: coerce?.header ?? false,
370
+ cookie: coerce?.cookie ?? false,
371
+ dateTime: coerce?.dateTime ?? false,
372
+ date: coerce?.date ?? false
373
+ };
374
+ }
362
375
  async function createExpressRouter(options) {
363
376
  const { controllers, artifactsDir = ".adorn", middleware = {} } = options;
364
377
  let manifest;
@@ -385,6 +398,7 @@ async function createExpressRouter(options) {
385
398
  const validator = precompiledValidators ? null : createValidator();
386
399
  const router = (0, import_express2.Router)();
387
400
  const instanceCache = /* @__PURE__ */ new Map();
401
+ const coerce = normalizeCoerceOptions(options.coerce);
388
402
  function getInstance(Ctor) {
389
403
  if (!instanceCache.has(Ctor)) {
390
404
  instanceCache.set(Ctor, new Ctor());
@@ -447,6 +461,14 @@ async function createExpressRouter(options) {
447
461
  }
448
462
  for (const route of routes) {
449
463
  const method = route.httpMethod.toLowerCase();
464
+ const openapiOperation = getOpenApiOperation(openapi, route);
465
+ const paramSchemaIndex = buildParamSchemaIndex(openapiOperation);
466
+ const bodySchema = getRequestBodySchema(openapiOperation, route.args.body?.contentType) ?? (route.args.body ? getSchemaByRef(route.args.body.schemaRef) : null);
467
+ const coerceBodyDates = getDateCoercionOptions(coerce, "body");
468
+ const coerceQueryDates = getDateCoercionOptions(coerce, "query");
469
+ const coercePathDates = getDateCoercionOptions(coerce, "path");
470
+ const coerceHeaderDates = getDateCoercionOptions(coerce, "header");
471
+ const coerceCookieDates = getDateCoercionOptions(coerce, "cookie");
450
472
  const middlewareChain = [];
451
473
  if (middleware.global) {
452
474
  middlewareChain.push(...resolveMiddleware(middleware.global, middleware.named || {}));
@@ -474,10 +496,13 @@ async function createExpressRouter(options) {
474
496
  }
475
497
  const args = [];
476
498
  if (route.args.body) {
477
- args[route.args.body.index] = req.body;
499
+ const coercedBody = (coerceBodyDates.date || coerceBodyDates.dateTime) && bodySchema ? coerceDatesWithSchema(req.body, bodySchema, coerceBodyDates, openapi.components.schemas) : req.body;
500
+ args[route.args.body.index] = coercedBody;
478
501
  }
479
502
  for (const pathArg of route.args.path) {
480
- const coerced = coerceValue(req.params[pathArg.name], pathArg.schemaType);
503
+ const rawValue = req.params[pathArg.name];
504
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", pathArg.name) ?? (pathArg.schemaRef ? getSchemaByRef(pathArg.schemaRef) : null) ?? schemaFromType(pathArg.schemaType);
505
+ const coerced = coerceParamValue(rawValue, paramSchema, coercePathDates, openapi.components.schemas);
481
506
  args[pathArg.index] = coerced;
482
507
  }
483
508
  if (route.args.query.length > 0) {
@@ -487,13 +512,15 @@ async function createExpressRouter(options) {
487
512
  args[firstQueryIndex] = {};
488
513
  for (const q of route.args.query) {
489
514
  const parsed = parseQueryValue(req.query[q.name], q);
490
- const coerced = coerceValue(parsed, q.schemaType);
515
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
516
+ const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
491
517
  args[firstQueryIndex][q.name] = coerced;
492
518
  }
493
519
  } else {
494
520
  for (const q of route.args.query) {
495
521
  const parsed = parseQueryValue(req.query[q.name], q);
496
- const coerced = coerceValue(parsed, q.schemaType);
522
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name) ?? (q.schemaRef ? getSchemaByRef(q.schemaRef) : null) ?? schemaFromType(q.schemaType);
523
+ const coerced = coerceParamValue(parsed, paramSchema, coerceQueryDates, openapi.components.schemas);
497
524
  args[q.index] = coerced;
498
525
  }
499
526
  }
@@ -505,12 +532,16 @@ async function createExpressRouter(options) {
505
532
  args[firstHeaderIndex] = {};
506
533
  for (const h of route.args.headers) {
507
534
  const headerValue = req.headers[h.name.toLowerCase()];
508
- args[firstHeaderIndex][h.name] = headerValue ?? void 0;
535
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(h.schemaRef) : null) ?? schemaFromType(h.schemaType);
536
+ const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
537
+ args[firstHeaderIndex][h.name] = coerced ?? void 0;
509
538
  }
510
539
  } else {
511
540
  for (const h of route.args.headers) {
512
541
  const headerValue = req.headers[h.name.toLowerCase()];
513
- args[h.index] = headerValue ?? void 0;
542
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "header", h.name) ?? (h.schemaRef ? getSchemaByRef(h.schemaRef) : null) ?? schemaFromType(h.schemaType);
543
+ const coerced = coerceParamValue(headerValue, paramSchema, coerceHeaderDates, openapi.components.schemas);
544
+ args[h.index] = coerced ?? void 0;
514
545
  }
515
546
  }
516
547
  }
@@ -522,13 +553,15 @@ async function createExpressRouter(options) {
522
553
  args[firstCookieIndex] = {};
523
554
  for (const c of route.args.cookies) {
524
555
  const cookieValue = cookies[c.name];
525
- const coerced = coerceValue(cookieValue, c.schemaType);
556
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(c.schemaRef) : null) ?? schemaFromType(c.schemaType);
557
+ const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
526
558
  args[firstCookieIndex][c.name] = coerced;
527
559
  }
528
560
  } else {
529
561
  for (const c of route.args.cookies) {
530
562
  const cookieValue = cookies[c.name];
531
- const coerced = coerceValue(cookieValue, c.schemaType);
563
+ const paramSchema = getParamSchemaFromIndex(paramSchemaIndex, "cookie", c.name) ?? (c.schemaRef ? getSchemaByRef(c.schemaRef) : null) ?? schemaFromType(c.schemaType);
564
+ const coerced = coerceParamValue(cookieValue, paramSchema, coerceCookieDates, openapi.components.schemas);
532
565
  args[c.index] = coerced;
533
566
  }
534
567
  }
@@ -547,6 +580,49 @@ async function createExpressRouter(options) {
547
580
  }
548
581
  return router;
549
582
  }
583
+ function getDateCoercionOptions(coerce, location) {
584
+ const enabled = coerce[location];
585
+ return {
586
+ dateTime: enabled && coerce.dateTime,
587
+ date: enabled && coerce.date
588
+ };
589
+ }
590
+ function toOpenApiPath(path3) {
591
+ return path3.replace(/:([^/]+)/g, "{$1}");
592
+ }
593
+ function getOpenApiOperation(openapi, route) {
594
+ const pathKey = toOpenApiPath(route.fullPath);
595
+ const pathItem = openapi.paths?.[pathKey];
596
+ if (!pathItem) return null;
597
+ return pathItem[route.httpMethod.toLowerCase()] ?? null;
598
+ }
599
+ function buildParamSchemaIndex(operation) {
600
+ const index = /* @__PURE__ */ new Map();
601
+ const params = operation?.parameters ?? [];
602
+ for (const param of params) {
603
+ if (!param?.name || !param?.in) continue;
604
+ if (param.schema) {
605
+ index.set(`${param.in}:${param.name}`, param.schema);
606
+ }
607
+ }
608
+ return index;
609
+ }
610
+ function getParamSchemaFromIndex(index, location, name) {
611
+ return index.get(`${location}:${name}`) ?? null;
612
+ }
613
+ function getRequestBodySchema(operation, contentType) {
614
+ const content = operation?.requestBody?.content;
615
+ if (!content) return null;
616
+ if (contentType && content[contentType]?.schema) {
617
+ return content[contentType].schema;
618
+ }
619
+ const first = Object.values(content)[0];
620
+ return first?.schema ?? null;
621
+ }
622
+ function schemaFromType(schemaType) {
623
+ if (!schemaType) return null;
624
+ return { type: schemaType };
625
+ }
550
626
  function validateRequestWithPrecompiled(route, req, validators) {
551
627
  const errors = [];
552
628
  if (route.args.body) {
@@ -569,12 +645,8 @@ function validateRequestWithPrecompiled(route, req, validators) {
569
645
  for (const q of route.args.query) {
570
646
  const value = req.query[q.name];
571
647
  if (value === void 0) continue;
572
- const schema = {};
573
- if (q.schemaType) {
574
- const type = Array.isArray(q.schemaType) ? q.schemaType[0] : q.schemaType;
575
- schema.type = type;
576
- }
577
- const coerced = coerceValue(value, q.schemaType);
648
+ const schema = schemaFromType(q.schemaType) ?? {};
649
+ const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, {});
578
650
  if (Object.keys(schema).length > 0 && coerced !== void 0) {
579
651
  errors.push({
580
652
  path: `#/query/${q.name}`,
@@ -587,12 +659,8 @@ function validateRequestWithPrecompiled(route, req, validators) {
587
659
  for (const p of route.args.path) {
588
660
  const value = req.params[p.name];
589
661
  if (value === void 0) continue;
590
- const schema = {};
591
- if (p.schemaType) {
592
- const type = Array.isArray(p.schemaType) ? p.schemaType[0] : p.schemaType;
593
- schema.type = type;
594
- }
595
- const coerced = coerceValue(value, p.schemaType);
662
+ const schema = schemaFromType(p.schemaType) ?? {};
663
+ const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, {});
596
664
  if (Object.keys(schema).length > 0 && coerced !== void 0) {
597
665
  errors.push({
598
666
  path: `#/path/${p.name}`,
@@ -610,6 +678,8 @@ function validateRequest(route, req, openapi, validator) {
610
678
  const schemaName = ref.replace("#/components/schemas/", "");
611
679
  return openapi.components.schemas[schemaName] || null;
612
680
  }
681
+ const openapiOperation = getOpenApiOperation(openapi, route);
682
+ const paramSchemaIndex = buildParamSchemaIndex(openapiOperation);
613
683
  const errors = [];
614
684
  if (route.args.body) {
615
685
  const bodySchema = getSchemaByRef(route.args.body.schemaRef);
@@ -630,18 +700,23 @@ function validateRequest(route, req, openapi, validator) {
630
700
  }
631
701
  for (const q of route.args.query) {
632
702
  const value = req.query[q.name];
633
- const schema = {};
634
- if (q.schemaType) {
635
- const type = Array.isArray(q.schemaType) ? q.schemaType[0] : q.schemaType;
636
- schema.type = type;
637
- }
638
- if (q.schemaRef && q.schemaRef.includes("Inline")) {
639
- const inlineSchema = getSchemaByRef(q.schemaRef);
640
- if (inlineSchema) {
641
- Object.assign(schema, inlineSchema);
703
+ const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "query", q.name);
704
+ let schema = {};
705
+ if (openapiSchema) {
706
+ schema = resolveSchema(openapiSchema, openapi.components.schemas);
707
+ } else {
708
+ if (q.schemaType) {
709
+ const type = Array.isArray(q.schemaType) ? q.schemaType[0] : q.schemaType;
710
+ schema.type = type;
711
+ }
712
+ if (q.schemaRef && q.schemaRef.includes("Inline")) {
713
+ const inlineSchema = getSchemaByRef(q.schemaRef);
714
+ if (inlineSchema) {
715
+ Object.assign(schema, inlineSchema);
716
+ }
642
717
  }
643
718
  }
644
- const coerced = coerceValue(value, q.schemaType);
719
+ const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, openapi.components.schemas);
645
720
  if (Object.keys(schema).length > 0 && coerced !== void 0) {
646
721
  const validate = validator.compile(schema);
647
722
  const valid = validate(coerced);
@@ -659,18 +734,23 @@ function validateRequest(route, req, openapi, validator) {
659
734
  }
660
735
  for (const p of route.args.path) {
661
736
  const value = req.params[p.name];
662
- const schema = {};
663
- if (p.schemaType) {
664
- const type = Array.isArray(p.schemaType) ? p.schemaType[0] : p.schemaType;
665
- schema.type = type;
666
- }
667
- if (p.schemaRef && p.schemaRef.includes("Inline")) {
668
- const inlineSchema = getSchemaByRef(p.schemaRef);
669
- if (inlineSchema) {
670
- Object.assign(schema, inlineSchema);
737
+ const openapiSchema = getParamSchemaFromIndex(paramSchemaIndex, "path", p.name);
738
+ let schema = {};
739
+ if (openapiSchema) {
740
+ schema = resolveSchema(openapiSchema, openapi.components.schemas);
741
+ } else {
742
+ if (p.schemaType) {
743
+ const type = Array.isArray(p.schemaType) ? p.schemaType[0] : p.schemaType;
744
+ schema.type = type;
745
+ }
746
+ if (p.schemaRef && p.schemaRef.includes("Inline")) {
747
+ const inlineSchema = getSchemaByRef(p.schemaRef);
748
+ if (inlineSchema) {
749
+ Object.assign(schema, inlineSchema);
750
+ }
671
751
  }
672
752
  }
673
- const coerced = coerceValue(value, p.schemaType);
753
+ const coerced = coerceParamValue(value, schema, { dateTime: false, date: false }, openapi.components.schemas);
674
754
  if (Object.keys(schema).length > 0 && coerced !== void 0) {
675
755
  const validate = validator.compile(schema);
676
756
  const valid = validate(coerced);
@@ -688,23 +768,127 @@ function validateRequest(route, req, openapi, validator) {
688
768
  }
689
769
  return errors.length > 0 ? errors : null;
690
770
  }
691
- function coerceValue(value, schemaType) {
771
+ function resolveSchema(schema, components, seen = /* @__PURE__ */ new Set()) {
772
+ const ref = schema.$ref;
773
+ if (typeof ref !== "string" || !ref.startsWith("#/components/schemas/")) {
774
+ return schema;
775
+ }
776
+ const name = ref.replace("#/components/schemas/", "");
777
+ if (seen.has(name)) return schema;
778
+ const next = components[name];
779
+ if (!next) return schema;
780
+ seen.add(name);
781
+ return resolveSchema(next, components, seen);
782
+ }
783
+ function coerceDatesWithSchema(value, schema, dateCoercion, components) {
784
+ if (!schema || !dateCoercion.date && !dateCoercion.dateTime) return value;
785
+ return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: false });
786
+ }
787
+ function coerceParamValue(value, schema, dateCoercion, components) {
788
+ if (!schema) return value;
789
+ return coerceWithSchema(value, schema, dateCoercion, components, { coercePrimitives: true });
790
+ }
791
+ function coerceWithSchema(value, schema, dateCoercion, components, options) {
792
+ if (value === void 0 || value === null) return value;
793
+ if (value instanceof Date) return value;
794
+ const resolved = resolveSchema(schema, components);
795
+ const override = resolved["x-adorn-coerce"];
796
+ const allowDateTime = override === true ? true : override === false ? false : dateCoercion.dateTime;
797
+ const allowDate = override === true ? true : override === false ? false : dateCoercion.date;
798
+ const byFormat = coerceDateString(value, resolved, allowDateTime, allowDate);
799
+ if (byFormat !== value) return byFormat;
800
+ const allOf = resolved.allOf;
801
+ if (Array.isArray(allOf)) {
802
+ let out = value;
803
+ for (const entry of allOf) {
804
+ out = coerceWithSchema(out, entry, { dateTime: allowDateTime, date: allowDate }, components, options);
805
+ }
806
+ return out;
807
+ }
808
+ const variants = resolved.oneOf ?? resolved.anyOf;
809
+ if (Array.isArray(variants)) {
810
+ for (const entry of variants) {
811
+ const out = coerceWithSchema(value, entry, { dateTime: allowDateTime, date: allowDate }, components, options);
812
+ if (out !== value) return out;
813
+ }
814
+ }
815
+ const schemaType = resolved.type;
816
+ const types = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
817
+ if ((types.includes("array") || resolved.items) && Array.isArray(value)) {
818
+ const itemSchema = resolved.items ?? {};
819
+ return value.map((item) => coerceWithSchema(item, itemSchema, { dateTime: allowDateTime, date: allowDate }, components, options));
820
+ }
821
+ if ((types.includes("object") || resolved.properties || resolved.additionalProperties) && isPlainObject(value)) {
822
+ const props = resolved.properties;
823
+ const out = { ...value };
824
+ if (props) {
825
+ for (const [key, propSchema] of Object.entries(props)) {
826
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
827
+ out[key] = coerceWithSchema(value[key], propSchema, { dateTime: allowDateTime, date: allowDate }, components, options);
828
+ }
829
+ }
830
+ }
831
+ const additional = resolved.additionalProperties;
832
+ if (additional && typeof additional === "object") {
833
+ for (const [key, entry] of Object.entries(value)) {
834
+ if (props && Object.prototype.hasOwnProperty.call(props, key)) continue;
835
+ out[key] = coerceWithSchema(entry, additional, { dateTime: allowDateTime, date: allowDate }, components, options);
836
+ }
837
+ }
838
+ return out;
839
+ }
840
+ if (options.coercePrimitives) {
841
+ return coercePrimitiveValue(value, types);
842
+ }
843
+ return value;
844
+ }
845
+ function coerceDateString(value, schema, allowDateTime, allowDate) {
846
+ if (typeof value !== "string") return value;
847
+ const format = schema.format;
848
+ const schemaType = schema.type;
849
+ const types = Array.isArray(schemaType) ? schemaType : schemaType ? [schemaType] : [];
850
+ const allowsString = types.length === 0 || types.includes("string");
851
+ if (format === "date-time" && allowDateTime && allowsString) {
852
+ const parsed = new Date(value);
853
+ if (Number.isNaN(parsed.getTime())) {
854
+ throw new Error(`Invalid date-time: ${value}`);
855
+ }
856
+ return parsed;
857
+ }
858
+ if (format === "date" && allowDate && allowsString) {
859
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
860
+ throw new Error(`Invalid date: ${value}`);
861
+ }
862
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
863
+ if (Number.isNaN(parsed.getTime())) {
864
+ throw new Error(`Invalid date: ${value}`);
865
+ }
866
+ return parsed;
867
+ }
868
+ return value;
869
+ }
870
+ function coercePrimitiveValue(value, types) {
692
871
  if (value === void 0 || value === null) return value;
693
- const type = Array.isArray(schemaType) ? schemaType[0] : schemaType;
694
- if (type === "number" || type === "integer") {
872
+ if (types.includes("number") || types.includes("integer")) {
695
873
  const num = Number(value);
696
- if (isNaN(num)) {
874
+ if (Number.isNaN(num)) {
697
875
  throw new Error(`Invalid number: ${value}`);
698
876
  }
699
877
  return num;
700
878
  }
701
- if (type === "boolean") {
879
+ if (types.includes("boolean")) {
702
880
  if (value === "true") return true;
703
881
  if (value === "false") return false;
882
+ if (typeof value === "boolean") return value;
704
883
  throw new Error(`Invalid boolean: ${value}`);
705
884
  }
706
885
  return value;
707
886
  }
887
+ function isPlainObject(value) {
888
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
889
+ const proto = Object.getPrototypeOf(value);
890
+ return proto === Object.prototype || proto === null;
891
+ }
708
892
  function parseQueryValue(value, param) {
709
893
  if (value === void 0 || value === null) return value;
710
894
  const isArray = Array.isArray(param.schemaType) ? param.schemaType.includes("array") : param.schemaType === "array";