@voznov/zod-dto-nestjs 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -59,13 +59,15 @@ Importing this package side-effect-registers an `onCreate` hook that decorates e
59
59
 
60
60
  Supported shapes: scalars, objects (nested), arrays, tuples, records, enums, literals, unions (including discriminated), intersections, optional/nullable/default wrappers, and nested DTO references (via `oneOf` + `ApiExtraModels`).
61
61
 
62
+ `z.discriminatedUnion(key, [...])` of DTO classes emits a proper OpenAPI `discriminator: { propertyName, mapping }` alongside `oneOf`, so codegen tools (`openapi-typescript`, `openapi-generator`) generate tagged unions instead of structural ones. Falls back to plain `oneOf` if any variant can't be mapped (non-DTO class or non-literal discriminator field).
63
+
62
64
  `.default(value)` is forwarded to the OpenAPI `default` keyword.
63
65
 
64
66
  > ⚠️ **Lazy defaults are frozen at decoration time.** For `.default(() => ...)`, the thunk is invoked **once** when the swagger metadata is generated, and the resolved value is baked into the spec. Anything non-stable — `Date.now()`, `randomUUID()`, `new Date()` — will freeze at the value the server happened to produce on startup, and every endpoint's example in your docs will show that one stale value. Use a stable thunk, or a literal default.
65
67
 
66
68
  `.describe(text)` is forwarded to the OpenAPI `description` keyword. Works on the wrapper or on the inner type — `z.string().describe('Login email').optional()` and `z.string().optional().describe('Login email')` both end up with `description: 'Login email'` in the spec.
67
69
 
68
- `.refine(...)` validators run at request-validation time but are **not** reflected in the spec — JSON Schema can't express custom predicates, and a single `description` for a chain of refines (`.refine(...).refine(...).refine(...)`) would be ambiguous. Put human-readable docs in `.describe(...)` instead.
70
+ `.refine(...)` validators run at runtime (in `ZodValidationPipe` for requests, in `@ZodSerialize` / `@ZodResponse` for responses) but are **not** reflected in the spec — JSON Schema can't express custom predicates, and a single `description` for a chain of refines (`.refine(...).refine(...).refine(...)`) would be ambiguous. Put human-readable docs in `.describe(...)` instead.
69
71
 
70
72
  > ⚠️ **`in` / `out` hooks are runtime-only.** The walker only reads the schema's structure, not the options passed to `ZodDto(schema, { in, out })`. A `out: ({ password, ...rest }) => rest` correctly strips `password` from the response *body*, but the OpenAPI schema still lists `password` as a property — the spec lies about a field that runtime drops. Same for `in`: snake_case→camelCase aliases applied via `in` are invisible in the spec, so docs show only the camelCase shape. If spec-correctness matters, either omit the field from the schema itself (`schema.omit({ password: true })`) or maintain a separate response DTO.
71
73
 
@@ -208,6 +210,18 @@ Both decorators accept the full `ToDtoOptions` bag (`preprocessors`, `observers`
208
210
  async create(): Promise<NoteDto> { /* ... */ }
209
211
  ```
210
212
 
213
+ ### Async refines on the response schema
214
+
215
+ When the method returns a Promise, the decorator awaits it and parses through `safeParseAsync` — that's the only signal it uses. So if your schema has async validation (`z.string().refine(async ...)`, async transforms — typical when the same schema is reused for request validation), make the method `async` and you'll get the async parse path; failures still throw `ZodDtoSerializationError` and flow through your exception filter, not raw Zod async errors.
216
+
217
+ ```ts
218
+ class Repo {
219
+ // Promise return → safeParseAsync. Works with async refines on the schema.
220
+ @ZodSerialize(SchemaWithAsyncRefine)
221
+ async findOne(id: string): Promise<...> { /* ... */ }
222
+ }
223
+ ```
224
+
211
225
  ## API
212
226
 
213
227
  | Export | Description |
package/dist/index.cjs CHANGED
@@ -1139,6 +1139,7 @@ var import_zod_dto4 = require("@voznov/zod-dto");
1139
1139
  // src/swagger.ts
1140
1140
  var import_swagger = require("@nestjs/swagger");
1141
1141
  var import_open_api_spec = require("@nestjs/swagger/dist/interfaces/open-api-spec.interface");
1142
+ var import_schema_object_metadata = require("@nestjs/swagger/dist/interfaces/schema-object-metadata.interface");
1142
1143
  var import_zod_dto = require("@voznov/zod-dto");
1143
1144
  var import_zod = require("zod");
1144
1145
 
@@ -1158,6 +1159,14 @@ var schemaObjectToApiPropertyOptions = (so, selfRequired) => {
1158
1159
  if ("oneOf" in so || "anyOf" in so || "allOf" in so) {
1159
1160
  return { ...so, type: Array, required: selfRequired };
1160
1161
  }
1162
+ if (so.type === "object") {
1163
+ return {
1164
+ ...so,
1165
+ type: "object",
1166
+ properties: so.properties ?? {},
1167
+ selfRequired
1168
+ };
1169
+ }
1161
1170
  return { ...so, required: selfRequired };
1162
1171
  };
1163
1172
  var leaf = (so) => ({
@@ -1311,6 +1320,21 @@ var applyDecoratorsImpl = (schema) => {
1311
1320
  innerSchemas_.forEach((innerSchema) => innerSchemas.add(innerSchema));
1312
1321
  return so.oneOf?.length === 1 ? so.oneOf[0] : so;
1313
1322
  });
1323
+ if (schema instanceof import_zod.z.ZodDiscriminatedUnion) {
1324
+ const propertyName = schema._zod.def.discriminator;
1325
+ const mapping = {};
1326
+ const allMappable = schema.options.every((option, i) => {
1327
+ if (!(option instanceof import_zod.z.ZodObject)) return false;
1328
+ const field = option.shape[propertyName];
1329
+ if (!(field instanceof import_zod.z.ZodLiteral)) return false;
1330
+ if (!("$ref" in oneOf[i])) return false;
1331
+ for (const literalValue of field.values) mapping[String(literalValue)] = oneOf[i].$ref;
1332
+ return true;
1333
+ });
1334
+ if (allMappable) {
1335
+ return { so: { oneOf, discriminator: { propertyName, mapping } }, selfRequired: true, innerSchemas };
1336
+ }
1337
+ }
1314
1338
  return { so: { oneOf }, selfRequired: true, innerSchemas };
1315
1339
  }
1316
1340
  if (schema instanceof import_zod.z.ZodIntersection) {
@@ -1362,6 +1386,9 @@ var import_zod2 = require("zod");
1362
1386
  var ZodDtoSerializationError = class extends import_zod_dto3.ZodDtoValidationError {
1363
1387
  };
1364
1388
  var resolveSchema = (schema, target, propertyKey, decoratorName) => {
1389
+ if (schema !== void 0 && !(schema instanceof import_zod2.z.ZodType) && !(0, import_zod_dto3.isZodDtoClass)(schema)) {
1390
+ throw new Error(`${decoratorName} on ${target.constructor.name}.${String(propertyKey)}: schema argument must be a Zod type or DTO class (got ${typeof schema}).`);
1391
+ }
1365
1392
  if (schema) return schema;
1366
1393
  const rt = Reflect.getMetadata("design:returntype", target, propertyKey);
1367
1394
  if (rt instanceof import_zod2.z.ZodType) return rt;
@@ -1381,26 +1408,18 @@ var wrapMethod = (schema, options, decoratorName) => (target, methodName, descri
1381
1408
  [methodName](...args) {
1382
1409
  const out = originalMethod.apply(this, args);
1383
1410
  if (out instanceof Promise) {
1384
- return out.then((v) => serialize(resolved, v));
1411
+ return out.then(async (v) => serialize.async(resolved, v));
1385
1412
  }
1386
1413
  return serialize(resolved, out);
1387
1414
  }
1388
1415
  }[methodName];
1389
1416
  redecorateFromReflect(originalMethod, descriptor.value);
1390
1417
  };
1391
- var API_RESPONSE_KEY = "swagger/apiResponse";
1392
1418
  var wrapApiResponse = (schema, options) => (target, propertyKey, descriptor) => {
1393
1419
  const resolved = resolveSchema(schema, target, propertyKey, "@ZodResponse");
1394
- const status = options?.status ?? 200;
1395
- const existing = descriptor.value ? Reflect.getMetadata(API_RESPONSE_KEY, descriptor.value) : void 0;
1396
- if (existing?.[status]) {
1397
- console.warn(
1398
- `@ZodResponse on ${target.constructor.name}.${String(propertyKey)}: another response decorator already set metadata for status ${status} \u2014 they will silent-merge. Remove the duplicate to avoid mixed spec output.`
1399
- );
1400
- }
1401
1420
  const { so, innerSchemas } = applySwaggerDecorators(resolved);
1402
1421
  const schemaForApi = Object.keys(so).length === 1 && Array.isArray(so.oneOf) && so.oneOf.length === 1 ? so.oneOf[0] : so;
1403
- (0, import_swagger2.ApiResponse)({ status, description: options?.description, schema: schemaForApi })(target, propertyKey, descriptor);
1422
+ (0, import_swagger2.ApiResponse)({ status: options?.status ?? 200, description: options?.description, schema: schemaForApi })(target, propertyKey, descriptor);
1404
1423
  if (innerSchemas.size > 0) {
1405
1424
  const classTarget = typeof target === "function" ? target : target.constructor;
1406
1425
  (0, import_swagger2.ApiExtraModels)(...innerSchemas)(classTarget);