@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 +15 -1
- package/dist/index.cjs +29 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +30 -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
|
@@ -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);
|