zod-nest 1.2.0 → 1.3.0

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/index.d.mts CHANGED
@@ -543,10 +543,18 @@ interface ApplyZodNestOptions {
543
543
  * - Every `components.schemas[<DtoClassName>]` placeholder with an
544
544
  * `x-zod-nest-dto` marker is replaced by the Zod-derived JSON Schema body,
545
545
  * keyed by the marker's `dtoId` (renaming as needed).
546
+ * - Every `@Query()` / `@Param()` / `@Headers()` / `@Cookie()` marker
547
+ * parameter is expanded into one parameter per top-level property of the
548
+ * DTO's schema (`expandParamMarkers`). The synthetic `components.schemas.Object`
549
+ * that `@nestjs/swagger` materialises for the marker placeholder is pruned
550
+ * when it has no remaining referrers.
546
551
  * - The I/O suffix truth table is applied — equal input/output bodies collapse
547
552
  * to one `components.schemas[id]`; divergent bodies split as
548
553
  * `id` (input) + `idOutput` (output), with response-side refs rewritten.
549
554
  * - Every `$ref` whose target is missing throws `ZodNestDocumentError(DANGLING_REF)`.
555
+ * - `doc.openapi` is set to `'3.1.0'` — zod-nest emits OpenAPI 3.1 only; this
556
+ * guarantees the version string matches the emitted body regardless of the
557
+ * `DocumentBuilder` configuration on the caller side.
550
558
  *
551
559
  * Composable with other doc-transform passes — apply other mutations before
552
560
  * or after this function. The `app` argument is required because the
@@ -555,7 +563,7 @@ interface ApplyZodNestOptions {
555
563
  */
556
564
  declare const applyZodNest: (doc: OpenAPIObject, opts: ApplyZodNestOptions) => OpenAPIObject;
557
565
 
558
- type ZodNestDocumentErrorCode = 'AMBIGUOUS_RENAME' | 'DANGLING_REF';
566
+ type ZodNestDocumentErrorCode = 'AMBIGUOUS_RENAME' | 'DANGLING_REF' | 'UNEXPANDABLE_PARAM_DTO';
559
567
  /**
560
568
  * Thrown by `applyZodNest` when the doc cannot be processed cleanly. Surfaces
561
569
  * at doc-build time so typos / mis-registrations fail in CI, not at runtime.
@@ -568,6 +576,11 @@ type ZodNestDocumentErrorCode = 'AMBIGUOUS_RENAME' | 'DANGLING_REF';
568
576
  * that no longer exists after `applyZodNest`. Usually means a marker was
569
577
  * stripped but its rename target wasn't populated, or a user-supplied pre-pass
570
578
  * left a stale ref.
579
+ *
580
+ * `UNEXPANDABLE_PARAM_DTO`: a `@Query()` / `@Param()` / `@Headers()` /
581
+ * `@Cookie()` handler argument resolved to a `createZodDto` whose schema is
582
+ * not an object — the marker parameter can't be expanded into individual
583
+ * parameters because there's no top-level `properties` record to iterate.
571
584
  */
572
585
  declare class ZodNestDocumentError extends ZodNestError {
573
586
  readonly code: ZodNestDocumentErrorCode;
package/dist/index.d.ts CHANGED
@@ -543,10 +543,18 @@ interface ApplyZodNestOptions {
543
543
  * - Every `components.schemas[<DtoClassName>]` placeholder with an
544
544
  * `x-zod-nest-dto` marker is replaced by the Zod-derived JSON Schema body,
545
545
  * keyed by the marker's `dtoId` (renaming as needed).
546
+ * - Every `@Query()` / `@Param()` / `@Headers()` / `@Cookie()` marker
547
+ * parameter is expanded into one parameter per top-level property of the
548
+ * DTO's schema (`expandParamMarkers`). The synthetic `components.schemas.Object`
549
+ * that `@nestjs/swagger` materialises for the marker placeholder is pruned
550
+ * when it has no remaining referrers.
546
551
  * - The I/O suffix truth table is applied — equal input/output bodies collapse
547
552
  * to one `components.schemas[id]`; divergent bodies split as
548
553
  * `id` (input) + `idOutput` (output), with response-side refs rewritten.
549
554
  * - Every `$ref` whose target is missing throws `ZodNestDocumentError(DANGLING_REF)`.
555
+ * - `doc.openapi` is set to `'3.1.0'` — zod-nest emits OpenAPI 3.1 only; this
556
+ * guarantees the version string matches the emitted body regardless of the
557
+ * `DocumentBuilder` configuration on the caller side.
550
558
  *
551
559
  * Composable with other doc-transform passes — apply other mutations before
552
560
  * or after this function. The `app` argument is required because the
@@ -555,7 +563,7 @@ interface ApplyZodNestOptions {
555
563
  */
556
564
  declare const applyZodNest: (doc: OpenAPIObject, opts: ApplyZodNestOptions) => OpenAPIObject;
557
565
 
558
- type ZodNestDocumentErrorCode = 'AMBIGUOUS_RENAME' | 'DANGLING_REF';
566
+ type ZodNestDocumentErrorCode = 'AMBIGUOUS_RENAME' | 'DANGLING_REF' | 'UNEXPANDABLE_PARAM_DTO';
559
567
  /**
560
568
  * Thrown by `applyZodNest` when the doc cannot be processed cleanly. Surfaces
561
569
  * at doc-build time so typos / mis-registrations fail in CI, not at runtime.
@@ -568,6 +576,11 @@ type ZodNestDocumentErrorCode = 'AMBIGUOUS_RENAME' | 'DANGLING_REF';
568
576
  * that no longer exists after `applyZodNest`. Usually means a marker was
569
577
  * stripped but its rename target wasn't populated, or a user-supplied pre-pass
570
578
  * left a stale ref.
579
+ *
580
+ * `UNEXPANDABLE_PARAM_DTO`: a `@Query()` / `@Param()` / `@Headers()` /
581
+ * `@Cookie()` handler argument resolved to a `createZodDto` whose schema is
582
+ * not an object — the marker parameter can't be expanded into individual
583
+ * parameters because there's no top-level `properties` record to iterate.
571
584
  */
572
585
  declare class ZodNestDocumentError extends ZodNestError {
573
586
  readonly code: ZodNestDocumentErrorCode;
package/dist/index.js CHANGED
@@ -1273,9 +1273,20 @@ var collectRefsFromOperation = /* @__PURE__ */ __name((operation, classToDtoId,
1273
1273
  continue;
1274
1274
  }
1275
1275
  collectRefFromSchema(param.schema, classToDtoId, ids);
1276
+ collectIdFromMarkerParam(param, ids);
1276
1277
  }
1277
1278
  }
1278
1279
  }, "collectRefsFromOperation");
1280
+ var collectIdFromMarkerParam = /* @__PURE__ */ __name((param, ids) => {
1281
+ if (param.__zodNestDto !== true) {
1282
+ return;
1283
+ }
1284
+ const dtoId = param.dtoId;
1285
+ if (typeof dtoId !== "string" || dtoId === "") {
1286
+ return;
1287
+ }
1288
+ ids.add(dtoId);
1289
+ }, "collectIdFromMarkerParam");
1279
1290
  var collectRefsFromContent = /* @__PURE__ */ __name((content, classToDtoId, ids) => {
1280
1291
  if (!isPlainRecord2(content)) {
1281
1292
  return;
@@ -1426,6 +1437,131 @@ var hintFor = /* @__PURE__ */ __name((ref, collected) => {
1426
1437
  return "no DTO with this id was registered \u2014 check for a meta.id typo or a DTO used without createZodDto";
1427
1438
  }, "hintFor");
1428
1439
 
1440
+ // src/document/expand-param-markers.ts
1441
+ var isPlainRecord3 = /* @__PURE__ */ __name((value) => value !== null && typeof value === "object" && !Array.isArray(value), "isPlainRecord");
1442
+ var expandParamMarkers = /* @__PURE__ */ __name((params) => {
1443
+ const { doc, inputSchemas, outputSchemas } = params;
1444
+ const paths = doc.paths;
1445
+ if (!isPlainRecord3(paths)) {
1446
+ return;
1447
+ }
1448
+ for (const pathItem of Object.values(paths)) {
1449
+ if (!isPlainRecord3(pathItem)) {
1450
+ continue;
1451
+ }
1452
+ for (const method of HTTP_METHODS) {
1453
+ const op = pathItem[method];
1454
+ if (!isPlainRecord3(op)) {
1455
+ continue;
1456
+ }
1457
+ const parameters = op.parameters;
1458
+ if (!Array.isArray(parameters)) {
1459
+ continue;
1460
+ }
1461
+ op.parameters = expandParameterList(parameters, inputSchemas, outputSchemas);
1462
+ }
1463
+ }
1464
+ pruneOrphanObjectSchema(doc);
1465
+ }, "expandParamMarkers");
1466
+ var expandParameterList = /* @__PURE__ */ __name((parameters, inputSchemas, outputSchemas) => {
1467
+ const result = [];
1468
+ for (const param of parameters) {
1469
+ const marker = readMarker2(param);
1470
+ if (marker === void 0) {
1471
+ result.push(param);
1472
+ continue;
1473
+ }
1474
+ const map = marker.io === "output" ? outputSchemas : inputSchemas;
1475
+ const body = map.get(marker.dtoId);
1476
+ result.push(...expandOne(marker, body));
1477
+ }
1478
+ return result;
1479
+ }, "expandParameterList");
1480
+ var readMarker2 = /* @__PURE__ */ __name((value) => {
1481
+ if (!isPlainRecord3(value)) {
1482
+ return void 0;
1483
+ }
1484
+ if (value.__zodNestDto !== true) {
1485
+ return void 0;
1486
+ }
1487
+ if (typeof value.dtoId !== "string" || value.dtoId === "") {
1488
+ return void 0;
1489
+ }
1490
+ if (value.io !== "input" && value.io !== "output") {
1491
+ return void 0;
1492
+ }
1493
+ if (typeof value.in !== "string" || value.in === "") {
1494
+ return void 0;
1495
+ }
1496
+ return value;
1497
+ }, "readMarker");
1498
+ var expandOne = /* @__PURE__ */ __name((marker, body) => {
1499
+ if (!isPlainRecord3(body) || !isPlainRecord3(body.properties)) {
1500
+ throw new ZodNestDocumentError("UNEXPANDABLE_PARAM_DTO", `Cannot expand \`@${capitalize(marker.in)}() x: ${marker.dtoId}\` \u2014 the DTO's schema is not an object with \`properties\`. Non-body parameter DTOs must be object schemas; arrays, unions, primitives, etc. cannot be split into individual parameters. Use \`@Body()\` for non-object DTOs, or restructure the schema as an object whose fields become the params.`, {
1501
+ dtoId: marker.dtoId,
1502
+ in: marker.in,
1503
+ io: marker.io
1504
+ });
1505
+ }
1506
+ const properties = body.properties;
1507
+ const requiredSet = collectRequired(body.required);
1508
+ const out = [];
1509
+ for (const [propName, propSchemaRaw] of Object.entries(properties)) {
1510
+ if (!isPlainRecord3(propSchemaRaw)) {
1511
+ continue;
1512
+ }
1513
+ out.push(buildParameter(marker, propName, propSchemaRaw, requiredSet.has(propName)));
1514
+ }
1515
+ return out;
1516
+ }, "expandOne");
1517
+ var collectRequired = /* @__PURE__ */ __name((value) => {
1518
+ if (!Array.isArray(value)) {
1519
+ return /* @__PURE__ */ new Set();
1520
+ }
1521
+ const out = /* @__PURE__ */ new Set();
1522
+ for (const item of value) {
1523
+ if (typeof item === "string") {
1524
+ out.add(item);
1525
+ }
1526
+ }
1527
+ return out;
1528
+ }, "collectRequired");
1529
+ var buildParameter = /* @__PURE__ */ __name((marker, name, schema, required) => {
1530
+ let effectiveRequired = required;
1531
+ if (marker.in === "path" && !effectiveRequired) {
1532
+ console.warn(`[zod-nest] Path parameter \`${name}\` on DTO \`${marker.dtoId}\` is marked optional in the Zod schema; OpenAPI 3.1 requires path parameters to be required. Coercing \`required: true\` so the emitted document is spec-valid. Fix by removing \`.optional()\` / \`.nullish()\` from the field, or by switching the decorator to @Query() / @Headers() if the field is genuinely optional.`);
1533
+ effectiveRequired = true;
1534
+ }
1535
+ const entry = {
1536
+ name,
1537
+ in: marker.in,
1538
+ required: effectiveRequired,
1539
+ schema
1540
+ };
1541
+ if (typeof schema.description === "string") {
1542
+ entry.description = schema.description;
1543
+ }
1544
+ return entry;
1545
+ }, "buildParameter");
1546
+ var capitalize = /* @__PURE__ */ __name((value) => value.charAt(0).toUpperCase() + value.slice(1), "capitalize");
1547
+ var pruneOrphanObjectSchema = /* @__PURE__ */ __name((doc) => {
1548
+ const schemas = doc.components?.schemas;
1549
+ if (schemas === void 0 || !Object.prototype.hasOwnProperty.call(schemas, "Object")) {
1550
+ return;
1551
+ }
1552
+ let referenced = false;
1553
+ const targetRef = `${COMPONENTS_SCHEMAS_PREFIX}Object`;
1554
+ walkRefs(doc, (ref) => {
1555
+ if (ref === targetRef) {
1556
+ referenced = true;
1557
+ }
1558
+ return void 0;
1559
+ });
1560
+ if (!referenced) {
1561
+ delete schemas.Object;
1562
+ }
1563
+ }, "pruneOrphanObjectSchema");
1564
+
1429
1565
  // src/document/expose-closure.ts
1430
1566
  var extendExposureViaRefs = /* @__PURE__ */ __name((collected, inputSchemas, outputSchemas) => ({
1431
1567
  ...collected,
@@ -1616,13 +1752,43 @@ var rewriteResponseSubtree = /* @__PURE__ */ __name((pathItem, divergentOutputId
1616
1752
  // src/document/strip-markers.ts
1617
1753
  var stripMarkers = /* @__PURE__ */ __name((doc) => {
1618
1754
  const schemas = doc.components?.schemas;
1619
- if (schemas === void 0) {
1755
+ if (schemas !== void 0) {
1756
+ for (const schema of Object.values(schemas)) {
1757
+ stripMarkerFromSchema(schema);
1758
+ }
1759
+ }
1760
+ stripMarkerParameters(doc);
1761
+ }, "stripMarkers");
1762
+ var stripMarkerParameters = /* @__PURE__ */ __name((doc) => {
1763
+ const paths = doc.paths;
1764
+ if (paths === null || typeof paths !== "object") {
1620
1765
  return;
1621
1766
  }
1622
- for (const schema of Object.values(schemas)) {
1623
- stripMarkerFromSchema(schema);
1767
+ for (const pathItem of Object.values(paths)) {
1768
+ if (pathItem === null || typeof pathItem !== "object") {
1769
+ continue;
1770
+ }
1771
+ const pathRecord = pathItem;
1772
+ for (const method of HTTP_METHODS) {
1773
+ const op = pathRecord[method];
1774
+ if (op === null || typeof op !== "object") {
1775
+ continue;
1776
+ }
1777
+ const opRecord = op;
1778
+ const parameters = opRecord.parameters;
1779
+ if (!Array.isArray(parameters)) {
1780
+ continue;
1781
+ }
1782
+ opRecord.parameters = parameters.filter((param) => !isMarkerParam(param));
1783
+ }
1784
+ }
1785
+ }, "stripMarkerParameters");
1786
+ var isMarkerParam = /* @__PURE__ */ __name((value) => {
1787
+ if (value === null || typeof value !== "object") {
1788
+ return false;
1624
1789
  }
1625
- }, "stripMarkers");
1790
+ return value.__zodNestDto === true;
1791
+ }, "isMarkerParam");
1626
1792
  var stripMarkerFromSchema = /* @__PURE__ */ __name((schema) => {
1627
1793
  if (schema === null || typeof schema !== "object") {
1628
1794
  return;
@@ -1642,6 +1808,7 @@ var stripMarkerFromSchema = /* @__PURE__ */ __name((schema) => {
1642
1808
  }, "stripMarkerFromSchema");
1643
1809
 
1644
1810
  // src/document/apply-zod-nest.ts
1811
+ var OPENAPI_VERSION = "3.1.0";
1645
1812
  var applyZodNest = /* @__PURE__ */ __name((doc, opts) => {
1646
1813
  const registry = opts.registry ?? defaultRegistry;
1647
1814
  const collected = collectUsage(doc, opts.app);
@@ -1658,6 +1825,11 @@ var applyZodNest = /* @__PURE__ */ __name((doc, opts) => {
1658
1825
  collected: extended,
1659
1826
  collisions: registry.getCollisions()
1660
1827
  });
1828
+ expandParamMarkers({
1829
+ doc,
1830
+ inputSchemas,
1831
+ outputSchemas
1832
+ });
1661
1833
  rewriteRefs2({
1662
1834
  doc,
1663
1835
  renames,
@@ -1668,6 +1840,7 @@ var applyZodNest = /* @__PURE__ */ __name((doc, opts) => {
1668
1840
  doc,
1669
1841
  collected: extended
1670
1842
  });
1843
+ doc.openapi = OPENAPI_VERSION;
1671
1844
  return doc;
1672
1845
  }, "applyZodNest");
1673
1846