@voznov/zod-dto-nestjs 0.3.1 → 0.3.2
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 +15 -1
- package/dist/index.cjs +20 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +21 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
@@ -1311,6 +1311,21 @@ var applyDecoratorsImpl = (schema) => {
|
|
|
1311
1311
|
innerSchemas_.forEach((innerSchema) => innerSchemas.add(innerSchema));
|
|
1312
1312
|
return so.oneOf?.length === 1 ? so.oneOf[0] : so;
|
|
1313
1313
|
});
|
|
1314
|
+
if (schema instanceof import_zod.z.ZodDiscriminatedUnion) {
|
|
1315
|
+
const propertyName = schema._zod.def.discriminator;
|
|
1316
|
+
const mapping = {};
|
|
1317
|
+
const allMappable = schema.options.every((option, i) => {
|
|
1318
|
+
if (!(option instanceof import_zod.z.ZodObject)) return false;
|
|
1319
|
+
const field = option.shape[propertyName];
|
|
1320
|
+
if (!(field instanceof import_zod.z.ZodLiteral)) return false;
|
|
1321
|
+
if (!("$ref" in oneOf[i])) return false;
|
|
1322
|
+
for (const literalValue of field.values) mapping[String(literalValue)] = oneOf[i].$ref;
|
|
1323
|
+
return true;
|
|
1324
|
+
});
|
|
1325
|
+
if (allMappable) {
|
|
1326
|
+
return { so: { oneOf, discriminator: { propertyName, mapping } }, selfRequired: true, innerSchemas };
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1314
1329
|
return { so: { oneOf }, selfRequired: true, innerSchemas };
|
|
1315
1330
|
}
|
|
1316
1331
|
if (schema instanceof import_zod.z.ZodIntersection) {
|
|
@@ -1362,6 +1377,9 @@ var import_zod2 = require("zod");
|
|
|
1362
1377
|
var ZodDtoSerializationError = class extends import_zod_dto3.ZodDtoValidationError {
|
|
1363
1378
|
};
|
|
1364
1379
|
var resolveSchema = (schema, target, propertyKey, decoratorName) => {
|
|
1380
|
+
if (schema !== void 0 && !(schema instanceof import_zod2.z.ZodType) && !(0, import_zod_dto3.isZodDtoClass)(schema)) {
|
|
1381
|
+
throw new Error(`${decoratorName} on ${target.constructor.name}.${String(propertyKey)}: schema argument must be a Zod type or DTO class (got ${typeof schema}).`);
|
|
1382
|
+
}
|
|
1365
1383
|
if (schema) return schema;
|
|
1366
1384
|
const rt = Reflect.getMetadata("design:returntype", target, propertyKey);
|
|
1367
1385
|
if (rt instanceof import_zod2.z.ZodType) return rt;
|
|
@@ -1381,26 +1399,18 @@ var wrapMethod = (schema, options, decoratorName) => (target, methodName, descri
|
|
|
1381
1399
|
[methodName](...args) {
|
|
1382
1400
|
const out = originalMethod.apply(this, args);
|
|
1383
1401
|
if (out instanceof Promise) {
|
|
1384
|
-
return out.then((v) => serialize(resolved, v));
|
|
1402
|
+
return out.then(async (v) => serialize.async(resolved, v));
|
|
1385
1403
|
}
|
|
1386
1404
|
return serialize(resolved, out);
|
|
1387
1405
|
}
|
|
1388
1406
|
}[methodName];
|
|
1389
1407
|
redecorateFromReflect(originalMethod, descriptor.value);
|
|
1390
1408
|
};
|
|
1391
|
-
var API_RESPONSE_KEY = "swagger/apiResponse";
|
|
1392
1409
|
var wrapApiResponse = (schema, options) => (target, propertyKey, descriptor) => {
|
|
1393
1410
|
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
1411
|
const { so, innerSchemas } = applySwaggerDecorators(resolved);
|
|
1402
1412
|
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);
|
|
1413
|
+
(0, import_swagger2.ApiResponse)({ status: options?.status ?? 200, description: options?.description, schema: schemaForApi })(target, propertyKey, descriptor);
|
|
1404
1414
|
if (innerSchemas.size > 0) {
|
|
1405
1415
|
const classTarget = typeof target === "function" ? target : target.constructor;
|
|
1406
1416
|
(0, import_swagger2.ApiExtraModels)(...innerSchemas)(classTarget);
|