schemantic 0.3.0 → 0.4.1
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/CHANGELOG.md +204 -174
- package/LICENSE +21 -21
- package/README.md +88 -830
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +185 -61
- package/dist/cli/index.js.map +1 -1
- package/dist/core/configuration.d.ts +6 -0
- package/dist/core/configuration.d.ts.map +1 -0
- package/dist/core/configuration.js +64 -0
- package/dist/core/configuration.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/output-writer.d.ts +7 -0
- package/dist/core/output-writer.d.ts.map +1 -0
- package/dist/core/output-writer.js +255 -0
- package/dist/core/output-writer.js.map +1 -0
- package/dist/core/schemantic.d.ts +8 -2
- package/dist/core/schemantic.d.ts.map +1 -1
- package/dist/core/schemantic.js +196 -71
- package/dist/core/schemantic.js.map +1 -1
- package/dist/generators/api-client-generator.d.ts +10 -20
- package/dist/generators/api-client-generator.d.ts.map +1 -1
- package/dist/generators/api-client-generator.js +374 -424
- package/dist/generators/api-client-generator.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +7 -25
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/enum-generator.d.ts.map +1 -1
- package/dist/generators/enum-generator.js +30 -3
- package/dist/generators/enum-generator.js.map +1 -1
- package/dist/generators/hook-generator.d.ts +3 -17
- package/dist/generators/hook-generator.d.ts.map +1 -1
- package/dist/generators/hook-generator.js +51 -445
- package/dist/generators/hook-generator.js.map +1 -1
- package/dist/generators/naming.d.ts +10 -0
- package/dist/generators/naming.d.ts.map +1 -0
- package/dist/generators/naming.js +331 -0
- package/dist/generators/naming.js.map +1 -0
- package/dist/generators/object-generator.d.ts +0 -4
- package/dist/generators/object-generator.d.ts.map +1 -1
- package/dist/generators/object-generator.js +13 -101
- package/dist/generators/object-generator.js.map +1 -1
- package/dist/generators/primitive-generator.d.ts +1 -13
- package/dist/generators/primitive-generator.d.ts.map +1 -1
- package/dist/generators/primitive-generator.js +11 -76
- package/dist/generators/primitive-generator.js.map +1 -1
- package/dist/generators/projections.d.ts +10 -0
- package/dist/generators/projections.d.ts.map +1 -0
- package/dist/generators/projections.js +65 -0
- package/dist/generators/projections.js.map +1 -0
- package/dist/generators/schema-type.d.ts +23 -0
- package/dist/generators/schema-type.d.ts.map +1 -0
- package/dist/generators/schema-type.js +233 -0
- package/dist/generators/schema-type.js.map +1 -0
- package/dist/parsers/base.d.ts +2 -0
- package/dist/parsers/base.d.ts.map +1 -1
- package/dist/parsers/base.js +12 -2
- package/dist/parsers/base.js.map +1 -1
- package/dist/parsers/factory.d.ts.map +1 -1
- package/dist/parsers/factory.js +2 -1
- package/dist/parsers/factory.js.map +1 -1
- package/dist/parsers/openapi.d.ts +4 -0
- package/dist/parsers/openapi.d.ts.map +1 -1
- package/dist/parsers/openapi.js +107 -20
- package/dist/parsers/openapi.js.map +1 -1
- package/dist/plugins/branded-types.d.ts.map +1 -1
- package/dist/plugins/branded-types.js +231 -365
- package/dist/plugins/branded-types.js.map +1 -1
- package/dist/plugins/builtin.d.ts.map +1 -1
- package/dist/plugins/builtin.js +6 -141
- package/dist/plugins/builtin.js.map +1 -1
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +17 -0
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/loader.d.ts +5 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +42 -5
- package/dist/plugins/loader.js.map +1 -1
- package/dist/plugins/manager.d.ts.map +1 -1
- package/dist/plugins/manager.js +7 -7
- package/dist/plugins/manager.js.map +1 -1
- package/dist/plugins/performance-monitoring.d.ts +31 -1
- package/dist/plugins/performance-monitoring.d.ts.map +1 -1
- package/dist/plugins/performance-monitoring.js +146 -207
- package/dist/plugins/performance-monitoring.js.map +1 -1
- package/dist/plugins/request-deduplication.d.ts.map +1 -1
- package/dist/plugins/request-deduplication.js +151 -586
- package/dist/plugins/request-deduplication.js.map +1 -1
- package/dist/plugins/zod-validation.d.ts +23 -15
- package/dist/plugins/zod-validation.d.ts.map +1 -1
- package/dist/plugins/zod-validation.js +392 -268
- package/dist/plugins/zod-validation.js.map +1 -1
- package/dist/types/core.d.ts +20 -3
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/core.js +2 -0
- package/dist/types/core.js.map +1 -1
- package/dist/types/openapi.d.ts +14 -1
- package/dist/types/openapi.d.ts.map +1 -1
- package/docs/ADVANCED_PLUGINS_IMPLEMENTATION.md +24 -355
- package/docs/ADVANCED_PLUGIN_DEVELOPMENT.md +33 -309
- package/docs/API.md +80 -601
- package/docs/AUDIT.md +49 -0
- package/docs/CONFIGURATION.md +81 -715
- package/docs/FASTAPI_TUTORIAL.md +109 -1497
- package/docs/NAMING_MIGRATION.md +59 -0
- package/docs/PLUGINS.md +49 -687
- package/docs/PLUGIN_DEVELOPMENT.md +53 -720
- package/docs/REAUDIT.md +399 -0
- package/docs/RELEASING.md +88 -0
- package/docs/REMEDIATION.md +58 -0
- package/docs/superpowers/plans/2026-10-10-audit-remediation.md +113 -0
- package/package.json +114 -95
- package/dist/__tests__/setup.d.ts +0 -6
- package/dist/__tests__/setup.d.ts.map +0 -1
- package/dist/__tests__/setup.js +0 -30
- package/dist/__tests__/setup.js.map +0 -1
- package/dist/__tests__/test-config.d.ts +0 -36
- package/dist/__tests__/test-config.d.ts.map +0 -1
- package/dist/__tests__/test-config.js +0 -562
- package/dist/__tests__/test-config.js.map +0 -1
|
@@ -18,15 +18,17 @@
|
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
20
|
exports.zodValidationPlugin = void 0;
|
|
21
|
+
exports.buildZodSchema = buildZodSchema;
|
|
21
22
|
exports.memoizedValidation = memoizedValidation;
|
|
22
23
|
exports.getZodValidationPerformanceStats = getZodValidationPerformanceStats;
|
|
23
24
|
exports.clearZodValidationCaches = clearZodValidationCaches;
|
|
24
25
|
exports.getValidationCacheStats = getValidationCacheStats;
|
|
25
26
|
const schema_1 = require("../types/schema");
|
|
27
|
+
const schema_type_1 = require("../generators/schema-type");
|
|
26
28
|
/**
|
|
27
29
|
* Schema cache for performance optimization
|
|
28
30
|
*/
|
|
29
|
-
|
|
31
|
+
let schemaCaches = new WeakMap();
|
|
30
32
|
/**
|
|
31
33
|
* Memoized schema transformation for performance
|
|
32
34
|
*/
|
|
@@ -60,21 +62,20 @@ exports.zodValidationPlugin = {
|
|
|
60
62
|
const startTime = performance.now();
|
|
61
63
|
try {
|
|
62
64
|
const options = getPluginOptions(context);
|
|
63
|
-
const
|
|
65
|
+
const sourceKey = generateSchemaKey(schema);
|
|
66
|
+
const schemaKey = sourceKey === undefined ? undefined : JSON.stringify(options) + sourceKey;
|
|
64
67
|
// Use memoized transformation for performance
|
|
65
|
-
if (options.cacheSchemas && memoizedSchemaTransform.has(schemaKey)) {
|
|
68
|
+
if (options.cacheSchemas && schemaKey !== undefined && memoizedSchemaTransform.has(schemaKey)) {
|
|
66
69
|
return memoizedSchemaTransform.get(schemaKey);
|
|
67
70
|
}
|
|
68
71
|
const transformedSchema = transformSchemaForZod(schema, options);
|
|
69
|
-
if (options.cacheSchemas) {
|
|
72
|
+
if (options.cacheSchemas && schemaKey !== undefined) {
|
|
73
|
+
if (memoizedSchemaTransform.size >= 1000)
|
|
74
|
+
memoizedSchemaTransform.delete(memoizedSchemaTransform.keys().next().value);
|
|
70
75
|
memoizedSchemaTransform.set(schemaKey, transformedSchema);
|
|
71
76
|
}
|
|
72
77
|
return transformedSchema;
|
|
73
78
|
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
console.warn(`Zod validation plugin transformSchema failed:`, error);
|
|
76
|
-
return schema;
|
|
77
|
-
}
|
|
78
79
|
finally {
|
|
79
80
|
const endTime = performance.now();
|
|
80
81
|
recordPerformanceMetric("transformSchema", endTime - startTime);
|
|
@@ -113,7 +114,7 @@ exports.zodValidationPlugin = {
|
|
|
113
114
|
generatedType.dependencies.push("zod");
|
|
114
115
|
// Add branded type exports if enabled
|
|
115
116
|
if (options.generateBrandedTypes) {
|
|
116
|
-
generatedType.exports.push(`Branded${typeName}`);
|
|
117
|
+
generatedType.exports.push(`Branded${typeName}`, `createBranded${typeName}`);
|
|
117
118
|
}
|
|
118
119
|
// Add validation exports
|
|
119
120
|
generatedType.exports.push(`${typeName}Schema`, `validate${typeName}`, `parse${typeName}`);
|
|
@@ -121,9 +122,6 @@ exports.zodValidationPlugin = {
|
|
|
121
122
|
generatedType.exports.push(`is${typeName}`);
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
|
-
catch (error) {
|
|
125
|
-
console.warn(`Zod validation plugin afterTypeGeneration failed for ${typeName}:`, error);
|
|
126
|
-
}
|
|
127
125
|
finally {
|
|
128
126
|
const endTime = performance.now();
|
|
129
127
|
recordPerformanceMetric("afterTypeGeneration", endTime - startTime);
|
|
@@ -136,8 +134,9 @@ exports.zodValidationPlugin = {
|
|
|
136
134
|
const startTime = performance.now();
|
|
137
135
|
try {
|
|
138
136
|
const options = getPluginOptions(context);
|
|
137
|
+
const validationErrorName = chooseValidationErrorName(generatedClient, context);
|
|
139
138
|
// Add validation middleware to client
|
|
140
|
-
const validationMiddleware = generateValidationMiddleware(generatedClient, context, options);
|
|
139
|
+
const validationMiddleware = generateValidationMiddleware(generatedClient, context, options, validationErrorName);
|
|
141
140
|
// Add request/response validation helpers
|
|
142
141
|
const validationHelpers = generateValidationHelpers(options);
|
|
143
142
|
// Update client content with validation integration
|
|
@@ -171,10 +170,7 @@ exports.zodValidationPlugin = {
|
|
|
171
170
|
generatedClient.dependencies.push("zod");
|
|
172
171
|
}
|
|
173
172
|
// Add validation exports
|
|
174
|
-
generatedClient.exports.push("validateRequest", "validateResponse", "
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
console.warn(`Zod validation plugin afterClientGeneration failed:`, error);
|
|
173
|
+
generatedClient.exports.push("validateRequest", "validateResponse", validationErrorName, "createValidationPipeline", "createLazyValidator");
|
|
178
174
|
}
|
|
179
175
|
finally {
|
|
180
176
|
const endTime = performance.now();
|
|
@@ -186,7 +182,7 @@ exports.zodValidationPlugin = {
|
|
|
186
182
|
* Get plugin options from generation context
|
|
187
183
|
*/
|
|
188
184
|
function getPluginOptions(context) {
|
|
189
|
-
const pluginConfig = context.config.plugins?.find((p) => p.name === "zod-validation");
|
|
185
|
+
const pluginConfig = context.config.plugins?.find((p) => p.enabled && (p.name === "zod-validation" || p.name === "validation"));
|
|
190
186
|
return {
|
|
191
187
|
strictMode: true,
|
|
192
188
|
cacheSchemas: true,
|
|
@@ -201,16 +197,29 @@ function getPluginOptions(context) {
|
|
|
201
197
|
* Generate a unique key for schema caching
|
|
202
198
|
*/
|
|
203
199
|
function generateSchemaKey(schema) {
|
|
204
|
-
|
|
200
|
+
// Circular in-memory schema graphs have no JSON cache identity. Bypass caching
|
|
201
|
+
// instead of inventing a sentinel shared by unrelated recursive graphs.
|
|
202
|
+
try {
|
|
203
|
+
return JSON.stringify(schema);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error instanceof TypeError && /circular/i.test(error.message))
|
|
207
|
+
return undefined;
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
205
210
|
}
|
|
206
211
|
/**
|
|
207
212
|
* Transform schema to include Zod validation metadata
|
|
208
213
|
*/
|
|
209
|
-
function transformSchemaForZod(schema, options) {
|
|
214
|
+
function transformSchemaForZod(schema, options, seen = new WeakMap()) {
|
|
210
215
|
if (!(0, schema_1.isOpenAPISchemaObject)(schema)) {
|
|
211
216
|
return schema;
|
|
212
217
|
}
|
|
218
|
+
const existing = seen.get(schema);
|
|
219
|
+
if (existing)
|
|
220
|
+
return existing;
|
|
213
221
|
const transformedSchema = { ...schema };
|
|
222
|
+
seen.set(schema, transformedSchema);
|
|
214
223
|
// Add Zod validation metadata
|
|
215
224
|
transformedSchema._zodValidation = {
|
|
216
225
|
strictMode: options.strictMode,
|
|
@@ -220,9 +229,9 @@ function transformSchemaForZod(schema, options) {
|
|
|
220
229
|
// Process properties recursively
|
|
221
230
|
if (schema.properties) {
|
|
222
231
|
transformedSchema.properties = {};
|
|
223
|
-
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
232
|
+
for (const [propName, propSchema] of Object.entries(schema.properties ?? {})) {
|
|
224
233
|
if (typeof propSchema === "object" && propSchema !== null) {
|
|
225
|
-
transformedSchema.properties[propName] = transformSchemaForZod(propSchema, options);
|
|
234
|
+
transformedSchema.properties[propName] = transformSchemaForZod(propSchema, options, seen);
|
|
226
235
|
}
|
|
227
236
|
else {
|
|
228
237
|
transformedSchema.properties[propName] = propSchema;
|
|
@@ -235,13 +244,18 @@ function transformSchemaForZod(schema, options) {
|
|
|
235
244
|
* Generate Zod schema for a type
|
|
236
245
|
*/
|
|
237
246
|
async function generateZodSchema(typeName, schema, options, context) {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
247
|
+
const sourceKey = generateSchemaKey(schema);
|
|
248
|
+
const cacheKey = sourceKey === undefined ? undefined : JSON.stringify([typeName, context?.config.typePrefix, context?.config.typeSuffix, options, sourceKey]);
|
|
249
|
+
let cache = context ? schemaCaches.get(context) : undefined;
|
|
250
|
+
if (context && !cache) {
|
|
251
|
+
cache = new Map();
|
|
252
|
+
schemaCaches.set(context, cache);
|
|
253
|
+
}
|
|
254
|
+
if (options.cacheSchemas && cacheKey !== undefined && cache?.has(cacheKey))
|
|
255
|
+
return cache.get(cacheKey);
|
|
242
256
|
const zodSchema = buildZodSchema(schema, options, 0, context);
|
|
243
|
-
if (options.cacheSchemas) {
|
|
244
|
-
|
|
257
|
+
if (options.cacheSchemas && cacheKey !== undefined) {
|
|
258
|
+
cache?.set(cacheKey, zodSchema);
|
|
245
259
|
}
|
|
246
260
|
return zodSchema;
|
|
247
261
|
}
|
|
@@ -249,62 +263,153 @@ async function generateZodSchema(typeName, schema, options, context) {
|
|
|
249
263
|
* Build Zod schema from OpenAPI schema
|
|
250
264
|
*/
|
|
251
265
|
function buildZodSchema(schema, options, depth = 0, context) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
266
|
+
if (depth > 30)
|
|
267
|
+
return "z.unknown()";
|
|
268
|
+
if (typeof schema === "object" && schema !== null && "$ref" in schema && schema.$ref) {
|
|
269
|
+
const ref = schema.$ref;
|
|
270
|
+
const siblings = Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "$ref" && !key.startsWith("_") && !["description", "title", "deprecated", "example"].includes(key)));
|
|
271
|
+
const nullable = siblings.nullable === true;
|
|
272
|
+
delete siblings.nullable;
|
|
273
|
+
const resolved = context?.schemaResolver?.(ref);
|
|
274
|
+
let referenceSchema;
|
|
275
|
+
if (Object.keys(siblings).length && resolved) {
|
|
276
|
+
const inheritedType = (0, schema_1.isOpenAPISchemaObject)(resolved) && ["string", "number", "integer", "array", "boolean"].includes(String(resolved.type)) ? { type: resolved.type } : {};
|
|
277
|
+
referenceSchema = `z.intersection(${buildZodSchema(resolved, { ...options, strictMode: false }, depth + 1, context)}, ${buildZodSchema({ ...inheritedType, ...siblings }, { ...options, strictMode: false }, depth + 1, context)})`;
|
|
257
278
|
}
|
|
279
|
+
else if (/^#\/components\/schemas\/[^/]+$/.test(ref)) {
|
|
280
|
+
referenceSchema = `z.lazy(() => ${formatTypeNameWithContext((0, schema_type_1.referenceName)(ref), context)}Schema)`;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
referenceSchema = resolved ? buildZodSchema(resolved, options, depth + 1, context) : "z.unknown()";
|
|
284
|
+
}
|
|
285
|
+
return nullable ? `${referenceSchema}.nullable()` : referenceSchema;
|
|
258
286
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
"
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
287
|
+
if (!(0, schema_1.isOpenAPISchemaObject)(schema))
|
|
288
|
+
return "z.unknown()";
|
|
289
|
+
if (schema["x-schemantic-binary"] === true)
|
|
290
|
+
return "z.instanceof(globalThis.Blob)";
|
|
291
|
+
const build = (part) => buildZodSchema(part, options, depth + 1, context);
|
|
292
|
+
let result;
|
|
293
|
+
if (schema.enum?.length) {
|
|
294
|
+
const literals = schema.enum.map(value => `z.literal(${JSON.stringify(value)})`);
|
|
295
|
+
const literalSchema = literals.length === 1 ? literals[0] : `z.union([${literals.join(", ")}])`;
|
|
296
|
+
const own = { ...schema };
|
|
297
|
+
delete own.enum;
|
|
298
|
+
result = `z.intersection(${literalSchema}, ${build(own)})`;
|
|
299
|
+
}
|
|
300
|
+
else if (schema.const !== undefined) {
|
|
301
|
+
const own = { ...schema };
|
|
302
|
+
delete own.const;
|
|
303
|
+
result = `z.intersection(z.literal(${JSON.stringify(schema.const)}), ${build(own)})`;
|
|
304
|
+
}
|
|
305
|
+
else if (schema.oneOf?.length || schema.anyOf?.length || schema.allOf?.length) {
|
|
306
|
+
// A composition and its sibling constraints apply together. Implicit strict
|
|
307
|
+
// mode must not reject fields contributed by another intersected schema;
|
|
308
|
+
// explicit additionalProperties:false still applies within each branch.
|
|
309
|
+
const compositionOptions = { ...options, strictMode: false };
|
|
310
|
+
const hasSiblingFields = !!schema.properties || !!schema.required?.length || schema.additionalProperties !== undefined;
|
|
311
|
+
const compositionCount = [schema.oneOf, schema.anyOf, schema.allOf].filter(parts => parts?.length).length;
|
|
312
|
+
const intersectsFields = hasSiblingFields || compositionCount > 1;
|
|
313
|
+
const unionOptions = intersectsFields ? compositionOptions : options;
|
|
314
|
+
const buildPart = (part, partOptions = compositionOptions, inlineReference = true) => {
|
|
315
|
+
if (typeof part === "object" && part !== null && "$ref" in part && typeof part.$ref === "string") {
|
|
316
|
+
const resolved = context?.schemaResolver?.(part.$ref);
|
|
317
|
+
if (resolved && inlineReference) {
|
|
318
|
+
const siblings = { ...part };
|
|
319
|
+
delete siblings.$ref;
|
|
320
|
+
const nullable = siblings.nullable === true;
|
|
321
|
+
delete siblings.nullable;
|
|
322
|
+
// Retain the entire reference expression instead of substituting only
|
|
323
|
+
// its target and losing minLength/properties/required beside $ref.
|
|
324
|
+
return buildZodSchema({ allOf: [resolved, siblings], ...(nullable ? { nullable: true } : {}) }, partOptions, depth + 1, context);
|
|
274
325
|
}
|
|
275
326
|
}
|
|
327
|
+
return buildZodSchema(part, partOptions, depth + 1, context);
|
|
328
|
+
};
|
|
329
|
+
const parts = [];
|
|
330
|
+
if (schema.oneOf?.length) {
|
|
331
|
+
const variants = schema.oneOf.map(part => buildPart(part, unionOptions, intersectsFields));
|
|
332
|
+
const union = variants.length === 1 ? variants[0] : `z.union([${variants.join(", ")}])`;
|
|
333
|
+
parts.push(`${union}.refine((value: unknown) => [${variants.join(", ")}].filter(variant => variant.safeParse(value).success).length === 1, { message: "Expected exactly one matching schema" })`);
|
|
334
|
+
}
|
|
335
|
+
if (schema.anyOf?.length) {
|
|
336
|
+
const variants = schema.anyOf.map(part => buildPart(part, unionOptions, intersectsFields));
|
|
337
|
+
parts.push(variants.length === 1 ? variants[0] : `z.union([${variants.join(", ")}])`);
|
|
276
338
|
}
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
339
|
+
if (schema.allOf?.length)
|
|
340
|
+
parts.push(...schema.allOf.map(part => buildPart(part)));
|
|
341
|
+
const own = { ...schema };
|
|
342
|
+
delete own.oneOf;
|
|
343
|
+
delete own.anyOf;
|
|
344
|
+
delete own.allOf;
|
|
345
|
+
delete own.nullable;
|
|
346
|
+
delete own.not;
|
|
347
|
+
const siblingValidator = buildZodSchema(own, compositionOptions, depth + 1, context);
|
|
348
|
+
if (siblingValidator !== "z.unknown()")
|
|
349
|
+
parts.push(siblingValidator);
|
|
350
|
+
result = parts.reduce((left, right) => `z.intersection(${left}, ${right})`);
|
|
351
|
+
}
|
|
352
|
+
else if (Array.isArray(schema.type)) {
|
|
353
|
+
const parts = schema.type.map(type => build({ ...schema, type, nullable: false }));
|
|
354
|
+
result = parts.length === 1 ? parts[0] : `z.union([${parts.join(", ")}])`;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
switch (schema.type) {
|
|
358
|
+
case "string":
|
|
359
|
+
result = buildStringSchema(schema, options);
|
|
360
|
+
break;
|
|
361
|
+
case "number":
|
|
362
|
+
case "integer":
|
|
363
|
+
result = buildNumberSchema(schema, options);
|
|
364
|
+
break;
|
|
365
|
+
case "boolean":
|
|
366
|
+
result = "z.boolean()";
|
|
367
|
+
break;
|
|
368
|
+
case "null":
|
|
369
|
+
result = "z.null()";
|
|
370
|
+
break;
|
|
371
|
+
case "array":
|
|
372
|
+
result = `z.array(${schema.items ? build(schema.items) : "z.unknown()"})`;
|
|
373
|
+
if (schema.minItems !== undefined)
|
|
374
|
+
result += `.min(${schema.minItems})`;
|
|
375
|
+
if (schema.maxItems !== undefined)
|
|
376
|
+
result += `.max(${schema.maxItems})`;
|
|
377
|
+
if (schema.uniqueItems)
|
|
378
|
+
result += `.refine((items: unknown[]) => items.every((item, index) => items.findIndex(other => { const canonical = (value: unknown): string => Array.isArray(value) ? '[' + value.map(canonical).join(',') + ']' : value !== null && typeof value === 'object' ? '{' + Object.keys(value).sort().map(key => JSON.stringify(key) + ':' + canonical((value as Record<string, unknown>)[key])).join(',') + '}' : JSON.stringify(value) ?? 'undefined'; return canonical(other) === canonical(item); }) === index), { message: "Array items must be unique" })`;
|
|
379
|
+
break;
|
|
380
|
+
case "object":
|
|
381
|
+
result = buildObjectSchema(schema, options, depth, context);
|
|
382
|
+
break;
|
|
383
|
+
default: result = schema.properties || schema.required || schema.additionalProperties !== undefined
|
|
384
|
+
? buildObjectSchema(schema, options, depth, context) : buildUntypedConstraintSchema(schema, options, depth, context);
|
|
280
385
|
}
|
|
281
386
|
}
|
|
282
|
-
if (
|
|
283
|
-
|
|
387
|
+
if (schema.not)
|
|
388
|
+
result += `.refine((value: unknown) => !(${build(schema.not)}).safeParse(value).success, { message: "Value matches prohibited schema" })`;
|
|
389
|
+
return schema.nullable ? `${result}.nullable()` : result;
|
|
390
|
+
}
|
|
391
|
+
/** Validate applicable keywords even when a sibling schema does not declare type. */
|
|
392
|
+
function buildUntypedConstraintSchema(schema, options, depth, context) {
|
|
393
|
+
let result = "z.unknown()";
|
|
394
|
+
const check = (applies, validator) => {
|
|
395
|
+
result += `.refine((value: unknown) => !(${applies}) || (${validator}).safeParse(value).success)`;
|
|
396
|
+
};
|
|
397
|
+
if (schema.minLength !== undefined || schema.maxLength !== undefined || schema.pattern || schema.format) {
|
|
398
|
+
check("typeof value === 'string'", buildStringSchema({ ...schema, type: "string" }, options));
|
|
284
399
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return "z.unknown()";
|
|
400
|
+
if (schema.minimum !== undefined || schema.maximum !== undefined || schema.exclusiveMinimum !== undefined || schema.exclusiveMaximum !== undefined || schema.multipleOf !== undefined) {
|
|
401
|
+
check("typeof value === 'number'", buildNumberSchema({ ...schema, type: "number" }, options));
|
|
288
402
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
case "array": {
|
|
298
|
-
const itemSchema = schema.items
|
|
299
|
-
? buildZodSchema(schema.items, options, depth + 1, context)
|
|
300
|
-
: "z.unknown()";
|
|
301
|
-
return `z.array(${itemSchema})`;
|
|
302
|
-
}
|
|
303
|
-
case "object":
|
|
304
|
-
return buildObjectSchema(schema, options, depth, context);
|
|
305
|
-
default:
|
|
306
|
-
return "z.unknown()";
|
|
403
|
+
if (schema.minProperties !== undefined || schema.maxProperties !== undefined) {
|
|
404
|
+
check("typeof value === 'object' && value !== null && !Array.isArray(value)", appendObjectPropertyConstraints("z.record(z.string(), z.unknown())", schema));
|
|
405
|
+
}
|
|
406
|
+
if (schema.items || schema.minItems !== undefined || schema.maxItems !== undefined || schema.uniqueItems) {
|
|
407
|
+
const arraySchema = { ...schema, type: "array" };
|
|
408
|
+
delete arraySchema.not;
|
|
409
|
+
delete arraySchema.nullable;
|
|
410
|
+
check("Array.isArray(value)", buildZodSchema(arraySchema, options, depth + 1, context));
|
|
307
411
|
}
|
|
412
|
+
return result;
|
|
308
413
|
}
|
|
309
414
|
/**
|
|
310
415
|
* Helper function to format type names consistently
|
|
@@ -314,13 +419,7 @@ function buildZodSchema(schema, options, depth = 0, context) {
|
|
|
314
419
|
* Format a type name using optional generation context to apply prefix/suffix.
|
|
315
420
|
*/
|
|
316
421
|
function formatTypeNameWithContext(name, context) {
|
|
317
|
-
|
|
318
|
-
const pascalCase = tokens
|
|
319
|
-
.map((word) => (word ? word.charAt(0).toUpperCase() + word.slice(1) : ""))
|
|
320
|
-
.join("");
|
|
321
|
-
const prefix = context?.config?.typePrefix ?? "";
|
|
322
|
-
const suffix = context?.config?.typeSuffix ?? "";
|
|
323
|
-
return `${prefix || ""}${pascalCase}${suffix || ""}`;
|
|
422
|
+
return (0, schema_type_1.typeName)(name, context?.config.typePrefix, context?.config.typeSuffix);
|
|
324
423
|
}
|
|
325
424
|
/**
|
|
326
425
|
* Build Zod string schema with validation rules
|
|
@@ -334,7 +433,7 @@ function buildStringSchema(schema, _options) {
|
|
|
334
433
|
zodSchema += `.max(${schema.maxLength})`;
|
|
335
434
|
}
|
|
336
435
|
if (schema.pattern) {
|
|
337
|
-
zodSchema += `.regex(
|
|
436
|
+
zodSchema += `.regex(new RegExp(${JSON.stringify(schema.pattern)}))`;
|
|
338
437
|
}
|
|
339
438
|
if (schema.format) {
|
|
340
439
|
switch (schema.format) {
|
|
@@ -368,7 +467,7 @@ function buildStringSchema(schema, _options) {
|
|
|
368
467
|
if (_options.customErrorMessages && schema.description) {
|
|
369
468
|
const customMessage = _options.customErrorMessages[schema.description];
|
|
370
469
|
if (customMessage) {
|
|
371
|
-
zodSchema += `.describe(
|
|
470
|
+
zodSchema += `.describe(${JSON.stringify(customMessage)})`;
|
|
372
471
|
}
|
|
373
472
|
}
|
|
374
473
|
return zodSchema;
|
|
@@ -384,6 +483,16 @@ function buildNumberSchema(schema, _options) {
|
|
|
384
483
|
if (schema.maximum !== undefined) {
|
|
385
484
|
zodSchema += `.max(${schema.maximum})`;
|
|
386
485
|
}
|
|
486
|
+
if (schema.exclusiveMinimum !== undefined) {
|
|
487
|
+
const bound = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : schema.exclusiveMinimum ? schema.minimum : undefined;
|
|
488
|
+
if (bound !== undefined)
|
|
489
|
+
zodSchema += `.gt(${bound})`;
|
|
490
|
+
}
|
|
491
|
+
if (schema.exclusiveMaximum !== undefined) {
|
|
492
|
+
const bound = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : schema.exclusiveMaximum ? schema.maximum : undefined;
|
|
493
|
+
if (bound !== undefined)
|
|
494
|
+
zodSchema += `.lt(${bound})`;
|
|
495
|
+
}
|
|
387
496
|
if (schema.multipleOf !== undefined) {
|
|
388
497
|
zodSchema += `.multipleOf(${schema.multipleOf})`;
|
|
389
498
|
}
|
|
@@ -393,161 +502,156 @@ function buildNumberSchema(schema, _options) {
|
|
|
393
502
|
* Build Zod object schema
|
|
394
503
|
*/
|
|
395
504
|
function buildObjectSchema(schema, options, depth, context) {
|
|
396
|
-
if (!schema.properties) {
|
|
397
|
-
|
|
505
|
+
if (!schema.properties && !schema.required?.length) {
|
|
506
|
+
const values = typeof schema.additionalProperties === "object"
|
|
507
|
+
? buildZodSchema(schema.additionalProperties, options, depth + 1, context) : "z.unknown()";
|
|
508
|
+
const base = schema.additionalProperties === false ? "z.object({}).strict()" : `z.record(z.string(), ${values})`;
|
|
509
|
+
return appendObjectPropertyConstraints(base, schema);
|
|
398
510
|
}
|
|
399
511
|
const properties = [];
|
|
400
512
|
const required = schema.required || [];
|
|
401
|
-
const
|
|
402
|
-
const wantsCamel = context?.config?.namingConvention === "camelCase";
|
|
403
|
-
// Helper: convert prop name to camelCase similar to generator
|
|
404
|
-
const toCamel = (str) => {
|
|
405
|
-
if (!str)
|
|
406
|
-
return str;
|
|
407
|
-
// sanitize: collapse non-alnum into underscore, trim
|
|
408
|
-
const sanitized = str
|
|
409
|
-
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
410
|
-
.replace(/^_+|_+$/g, "")
|
|
411
|
-
.replace(/_+/g, "_");
|
|
412
|
-
const parts = sanitized.split(/_+/).filter(Boolean);
|
|
413
|
-
if (parts.length === 0)
|
|
414
|
-
return "";
|
|
415
|
-
const head = parts[0] ?? "";
|
|
416
|
-
const tail = parts.slice(1);
|
|
417
|
-
const camelHead = head ? head.charAt(0).toLowerCase() + head.slice(1) : "";
|
|
418
|
-
const camelTail = tail.map((p) => p ? p.charAt(0).toUpperCase() + p.slice(1) : "");
|
|
419
|
-
return camelHead + camelTail.join("");
|
|
420
|
-
};
|
|
421
|
-
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
513
|
+
for (const [propName, propSchema] of Object.entries(schema.properties ?? {})) {
|
|
422
514
|
if (typeof propSchema === "object" && propSchema !== null) {
|
|
423
515
|
let propZodSchema = buildZodSchema(propSchema, options, depth + 1, context);
|
|
424
516
|
if (!required.includes(propName)) {
|
|
425
517
|
propZodSchema += ".optional()";
|
|
426
518
|
}
|
|
427
|
-
properties.push(` ${propName}: ${propZodSchema}`);
|
|
428
|
-
if (wantsCamel) {
|
|
429
|
-
const camel = toCamel(propName);
|
|
430
|
-
if (camel !== propName) {
|
|
431
|
-
transformPairs.push(` ${camel}: val[${JSON.stringify(propName)}]`);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
519
|
+
properties.push(` ${(0, schema_type_1.propertyKey)(propName)}: ${propZodSchema}`);
|
|
434
520
|
}
|
|
435
521
|
}
|
|
436
|
-
const
|
|
522
|
+
for (const name of required)
|
|
523
|
+
if (!schema.properties?.[name])
|
|
524
|
+
properties.push(` ${(0, schema_type_1.propertyKey)(name)}: z.unknown().refine((value: unknown) => value !== undefined, { message: "Required property" })`);
|
|
525
|
+
const strictMode = schema.additionalProperties === true ? ".passthrough()"
|
|
526
|
+
: typeof schema.additionalProperties === "object" ? `.catchall(${buildZodSchema(schema.additionalProperties, options, depth + 1, context)})`
|
|
527
|
+
: options.strictMode || schema.additionalProperties === false ? ".strict()" : ".passthrough()";
|
|
437
528
|
const base = `z.object({\n${properties.join(",\n")}\n})${strictMode}`;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
//
|
|
443
|
-
|
|
529
|
+
return appendObjectPropertyConstraints(base, schema);
|
|
530
|
+
}
|
|
531
|
+
function appendObjectPropertyConstraints(base, schema) {
|
|
532
|
+
// Zod intersections may reconcile strict-object unrecognized-key issues against
|
|
533
|
+
// sibling schemas. OpenAPI's explicit additionalProperties:false applies to
|
|
534
|
+
// this schema's own declared properties, so check the original input first.
|
|
535
|
+
const allowedKeys = JSON.stringify(Object.keys(schema.properties ?? {}));
|
|
536
|
+
let result = schema.additionalProperties === false
|
|
537
|
+
? `z.unknown().refine((value: unknown) => value === null || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).every(key => ${allowedKeys}.includes(key)), { message: "Unexpected object property" }).pipe(${base})`
|
|
538
|
+
: base;
|
|
539
|
+
if (schema.minProperties !== undefined)
|
|
540
|
+
result += `.refine((value: Record<string, unknown>) => Object.keys(value).length >= ${schema.minProperties}, { message: "Too few properties" })`;
|
|
541
|
+
if (schema.maxProperties !== undefined)
|
|
542
|
+
result += `.refine((value: Record<string, unknown>) => Object.keys(value).length <= ${schema.maxProperties}, { message: "Too many properties" })`;
|
|
543
|
+
return result;
|
|
444
544
|
}
|
|
445
545
|
/**
|
|
446
546
|
* Build Zod union/discriminatedUnion schema for oneOf
|
|
447
547
|
*/
|
|
448
|
-
function buildOneOfSchema(schema, options, context) {
|
|
449
|
-
const variants = (schema.oneOf || []).map((variant) => {
|
|
450
|
-
if (typeof variant === "object" &&
|
|
451
|
-
variant !== null &&
|
|
452
|
-
"$ref" in variant) {
|
|
453
|
-
const refPath = variant["$ref"];
|
|
454
|
-
const name = refPath.split("/").pop() || "Unknown";
|
|
455
|
-
return `${formatTypeNameWithContext(name, context)}Schema`;
|
|
456
|
-
}
|
|
457
|
-
return buildZodSchema(variant, options, 0, context);
|
|
458
|
-
});
|
|
459
|
-
const discriminator = schema.discriminator?.propertyName;
|
|
460
|
-
if (discriminator) {
|
|
461
|
-
return `z.discriminatedUnion(${JSON.stringify(discriminator)}, [${variants.join(", ")}])`;
|
|
462
|
-
}
|
|
463
|
-
return `z.union([${variants.join(", ")}])`;
|
|
464
|
-
}
|
|
465
548
|
/**
|
|
466
549
|
* Generate branded type for compile-time safety
|
|
467
550
|
*/
|
|
468
551
|
function generateBrandedType(typeName, _generatedType) {
|
|
469
|
-
return `
|
|
470
|
-
/**
|
|
471
|
-
* Branded type for ${typeName} with compile-time guarantees
|
|
472
|
-
*/
|
|
473
|
-
export type Branded${typeName} = ${typeName} & { __brand: '${typeName}' };
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Create a branded ${typeName} instance
|
|
477
|
-
*/
|
|
478
|
-
export function createBranded${typeName}(data: ${typeName}): Branded${typeName} {
|
|
479
|
-
return data as Branded${typeName};
|
|
552
|
+
return `
|
|
553
|
+
/**
|
|
554
|
+
* Branded type for ${typeName} with compile-time guarantees
|
|
555
|
+
*/
|
|
556
|
+
export type Branded${typeName} = ${typeName} & { __brand: '${typeName}' };
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Create a branded ${typeName} instance
|
|
560
|
+
*/
|
|
561
|
+
export function createBranded${typeName}(data: ${typeName}): Branded${typeName} {
|
|
562
|
+
return data as Branded${typeName};
|
|
480
563
|
}`;
|
|
481
564
|
}
|
|
482
565
|
/**
|
|
483
566
|
* Generate runtime type guard
|
|
484
567
|
*/
|
|
485
568
|
function generateTypeGuard(typeName, _zodSchema) {
|
|
486
|
-
return `
|
|
487
|
-
/**
|
|
488
|
-
* Runtime type guard for ${typeName}
|
|
489
|
-
*/
|
|
490
|
-
export function is${typeName}(value: unknown): value is ${typeName} {
|
|
491
|
-
return ${typeName}Schema.safeParse(value).success;
|
|
569
|
+
return `
|
|
570
|
+
/**
|
|
571
|
+
* Runtime type guard for ${typeName}
|
|
572
|
+
*/
|
|
573
|
+
export function is${typeName}(value: unknown): value is ${typeName} {
|
|
574
|
+
return ${typeName}Schema.safeParse(value).success;
|
|
492
575
|
}`;
|
|
493
576
|
}
|
|
494
577
|
/**
|
|
495
578
|
* Generate Zod imports based on options
|
|
496
579
|
*/
|
|
497
|
-
function generateZodImports(
|
|
580
|
+
function generateZodImports(_options) {
|
|
498
581
|
const imports = ["z"];
|
|
499
|
-
if (options.generateBrandedTypes) {
|
|
500
|
-
imports.push("ZodType");
|
|
501
|
-
}
|
|
502
582
|
return `import { ${imports.join(", ")} } from 'zod';`;
|
|
503
583
|
}
|
|
504
584
|
/**
|
|
505
585
|
* Generate validation module for a type
|
|
506
586
|
*/
|
|
507
587
|
function generateValidationModule(typeName, zodSchema, brandedType, typeGuard, _options) {
|
|
508
|
-
return `
|
|
509
|
-
/**
|
|
510
|
-
* Zod validation schema for ${typeName}
|
|
511
|
-
*/
|
|
512
|
-
export const ${typeName}Schema = ${zodSchema};
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Validate ${typeName} data with detailed error reporting
|
|
516
|
-
*/
|
|
517
|
-
export function validate${typeName}(data: unknown): { success: true; data: ${typeName} } | { success: false; errors: string[] } {
|
|
518
|
-
const result = ${typeName}Schema.safeParse(data);
|
|
519
|
-
|
|
520
|
-
if (!result.success) {
|
|
521
|
-
const issues = result.error?.issues ?? [];
|
|
522
|
-
console.warn('Response validation failed:', issues);
|
|
523
|
-
// Return a structured failure result so consumers can handle errors without requiring a runtime ValidationError class
|
|
524
|
-
return {
|
|
525
|
-
success: false,
|
|
526
|
-
errors: issues.length > 0
|
|
527
|
-
? issues.map((iss: z.core.$ZodIssue) => (iss.path && iss.path.length > 0 ? iss.path.join('.') + ': ' + iss.message : '(root): ' + iss.message))
|
|
528
|
-
: []
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return { success: true, data: result.data as ${typeName} };
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Parse ${typeName} data with exception on validation failure
|
|
537
|
-
*/
|
|
538
|
-
export function parse${typeName}(data: unknown): ${typeName} {
|
|
539
|
-
return ${typeName}Schema.parse(data) as ${typeName};
|
|
540
|
-
}
|
|
588
|
+
return `
|
|
589
|
+
/**
|
|
590
|
+
* Zod validation schema for ${typeName}
|
|
591
|
+
*/
|
|
592
|
+
export const ${typeName}Schema: z.ZodType = ${zodSchema};
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Validate ${typeName} data with detailed error reporting
|
|
596
|
+
*/
|
|
597
|
+
export function validate${typeName}(data: unknown): { success: true; data: ${typeName} } | { success: false; errors: string[] } {
|
|
598
|
+
const result = ${typeName}Schema.safeParse(data);
|
|
599
|
+
|
|
600
|
+
if (!result.success) {
|
|
601
|
+
const issues = result.error?.issues ?? [];
|
|
602
|
+
console.warn('Response validation failed:', issues);
|
|
603
|
+
// Return a structured failure result so consumers can handle errors without requiring a runtime ValidationError class
|
|
604
|
+
return {
|
|
605
|
+
success: false,
|
|
606
|
+
errors: issues.length > 0
|
|
607
|
+
? issues.map((iss: z.core.$ZodIssue) => (iss.path && iss.path.length > 0 ? iss.path.join('.') + ': ' + iss.message : '(root): ' + iss.message))
|
|
608
|
+
: []
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { success: true, data: result.data as ${typeName} };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Parse ${typeName} data with exception on validation failure
|
|
617
|
+
*/
|
|
618
|
+
export function parse${typeName}(data: unknown): ${typeName} {
|
|
619
|
+
return ${typeName}Schema.parse(data) as ${typeName};
|
|
620
|
+
}
|
|
541
621
|
${brandedType}${typeGuard}`;
|
|
542
622
|
}
|
|
623
|
+
/** Runtime helper names must be distinct across type/client exports and imported bindings. */
|
|
624
|
+
function chooseValidationErrorName(client, context) {
|
|
625
|
+
const claimed = new Set(client.exports);
|
|
626
|
+
for (const type of context.generatedTypes.values()) {
|
|
627
|
+
claimed.add(type.name);
|
|
628
|
+
for (const name of type.exports)
|
|
629
|
+
claimed.add(name);
|
|
630
|
+
}
|
|
631
|
+
for (const generatedClient of context.generatedClients.values()) {
|
|
632
|
+
claimed.add(generatedClient.name);
|
|
633
|
+
for (const name of generatedClient.exports)
|
|
634
|
+
claimed.add(name);
|
|
635
|
+
}
|
|
636
|
+
// Cover imported aliases and local bindings supplied by custom client generators.
|
|
637
|
+
for (const identifier of client.content.match(/\b[A-Za-z_$][\w$]*\b/g) ?? [])
|
|
638
|
+
claimed.add(identifier);
|
|
639
|
+
if (!claimed.has("ValidationError"))
|
|
640
|
+
return "ValidationError";
|
|
641
|
+
let name = "ResponseValidationError";
|
|
642
|
+
let suffix = 0;
|
|
643
|
+
while (claimed.has(name))
|
|
644
|
+
name = `ResponseValidationError${++suffix}`;
|
|
645
|
+
return name;
|
|
646
|
+
}
|
|
543
647
|
/**
|
|
544
648
|
* Generate validation middleware for API client
|
|
545
649
|
*/
|
|
546
|
-
function generateValidationMiddleware(_generatedClient, _context, options) {
|
|
650
|
+
function generateValidationMiddleware(_generatedClient, _context, options, errorName) {
|
|
547
651
|
// Ensure z/ZodType import is present in client content
|
|
548
652
|
if (!_generatedClient.content.includes("from 'zod'")) {
|
|
549
653
|
_generatedClient.content =
|
|
550
|
-
`import { z
|
|
654
|
+
`import { z } from 'zod';\nimport type { ZodType } from 'zod';\n` + _generatedClient.content;
|
|
551
655
|
}
|
|
552
656
|
else if (!_generatedClient.content.includes("ZodType")) {
|
|
553
657
|
// Append ZodType to existing import if only z is imported
|
|
@@ -562,83 +666,85 @@ function generateValidationMiddleware(_generatedClient, _context, options) {
|
|
|
562
666
|
});
|
|
563
667
|
}
|
|
564
668
|
const strictModeCode = options.strictMode
|
|
565
|
-
?
|
|
566
|
-
: "
|
|
567
|
-
return `
|
|
568
|
-
/**
|
|
569
|
-
* Validation middleware for API requests and responses
|
|
570
|
-
*/
|
|
571
|
-
export class
|
|
572
|
-
public issues: readonly z.core.$ZodIssue[];
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
.join('
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
this.
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
669
|
+
? `throw new ${errorName}(result.error?.issues ?? [], data);`
|
|
670
|
+
: "return data as T;";
|
|
671
|
+
return `
|
|
672
|
+
/**
|
|
673
|
+
* Validation middleware for API requests and responses
|
|
674
|
+
*/
|
|
675
|
+
export class ${errorName} extends Error {
|
|
676
|
+
public issues: readonly z.core.$ZodIssue[];
|
|
677
|
+
public data: unknown;
|
|
678
|
+
constructor(issues: readonly z.core.$ZodIssue[], data: unknown) {
|
|
679
|
+
const message = issues
|
|
680
|
+
.map((iss) => (iss.path && iss.path.length > 0 ? iss.path.join('.') + ': ' + iss.message : '(root): ' + iss.message))
|
|
681
|
+
.join('; ');
|
|
682
|
+
super('Validation failed: ' + message);
|
|
683
|
+
this.name = '${errorName}';
|
|
684
|
+
this.issues = issues;
|
|
685
|
+
this.data = data;
|
|
686
|
+
Object.setPrototypeOf(this, ${errorName}.prototype);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Validate request data before sending
|
|
692
|
+
*/
|
|
693
|
+
export function validateRequest<T>(data: unknown, schema: ZodType<T>): T {
|
|
694
|
+
const result = schema.safeParse(data);
|
|
695
|
+
|
|
696
|
+
if (!result.success) {
|
|
697
|
+
const issues = result.error?.issues ?? [];
|
|
698
|
+
// Use Zod v4 core issue type
|
|
699
|
+
throw new ${errorName}(issues as readonly z.core.$ZodIssue[], data);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return result.data;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Validate response data after receiving
|
|
707
|
+
*/
|
|
708
|
+
export function validateResponse<T>(data: unknown, schema: ZodType<T>): T {
|
|
709
|
+
const result = schema.safeParse(data);
|
|
710
|
+
|
|
711
|
+
if (!result.success) {
|
|
712
|
+
const issues = result.error?.issues ?? [];
|
|
713
|
+
console.warn('Response validation failed:', issues);
|
|
714
|
+
${strictModeCode}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return result.data;
|
|
612
718
|
}`;
|
|
613
719
|
}
|
|
614
720
|
/**
|
|
615
721
|
* Generate validation helper functions
|
|
616
722
|
*/
|
|
617
723
|
function generateValidationHelpers(_options) {
|
|
618
|
-
return `
|
|
619
|
-
/**
|
|
620
|
-
* Utility functions for validation operations
|
|
621
|
-
*/
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Create a validation pipeline for multiple schemas
|
|
625
|
-
*/
|
|
626
|
-
export function createValidationPipeline<T>(...schemas: ZodType<unknown>[]): ZodType<T> {
|
|
627
|
-
return schemas.reduce((acc, schema) => acc.pipe(schema)) as ZodType<T>;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
/**
|
|
631
|
-
* Lazy validation for performance optimization
|
|
632
|
-
*/
|
|
633
|
-
export function createLazyValidator<T>(schemaFactory: () => ZodType<T>) {
|
|
634
|
-
let schema: ZodType<T> | null = null;
|
|
635
|
-
|
|
636
|
-
return (data: unknown): T => {
|
|
637
|
-
if (!schema) {
|
|
638
|
-
schema = schemaFactory();
|
|
639
|
-
}
|
|
640
|
-
return schema.parse(data);
|
|
641
|
-
};
|
|
724
|
+
return `
|
|
725
|
+
/**
|
|
726
|
+
* Utility functions for validation operations
|
|
727
|
+
*/
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Create a validation pipeline for multiple schemas
|
|
731
|
+
*/
|
|
732
|
+
export function createValidationPipeline<T>(...schemas: ZodType<unknown>[]): ZodType<T> {
|
|
733
|
+
return schemas.reduce((acc, schema) => acc.pipe(schema)) as ZodType<T>;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Lazy validation for performance optimization
|
|
738
|
+
*/
|
|
739
|
+
export function createLazyValidator<T>(schemaFactory: () => ZodType<T>) {
|
|
740
|
+
let schema: ZodType<T> | null = null;
|
|
741
|
+
|
|
742
|
+
return (data: unknown): T => {
|
|
743
|
+
if (!schema) {
|
|
744
|
+
schema = schemaFactory();
|
|
745
|
+
}
|
|
746
|
+
return schema.parse(data);
|
|
747
|
+
};
|
|
642
748
|
}`;
|
|
643
749
|
}
|
|
644
750
|
/**
|
|
@@ -720,16 +826,34 @@ class BoundedValidationCache {
|
|
|
720
826
|
}
|
|
721
827
|
}
|
|
722
828
|
const validationCache = new BoundedValidationCache();
|
|
829
|
+
const validationSchemaIds = new WeakMap();
|
|
830
|
+
let nextValidationSchemaId = 0;
|
|
723
831
|
function memoizedValidation(data, schema, cacheKey, schemaVersion = "1.0") {
|
|
724
|
-
|
|
832
|
+
// Cache primitive inputs/results only. Caller-owned objects and stateful references
|
|
833
|
+
// must be validated on every call rather than reusing mutable results.
|
|
834
|
+
if (!cacheKey || (data !== null && typeof data === "object") || typeof data === "function" || typeof data === "symbol") {
|
|
835
|
+
return schema.parse(data);
|
|
836
|
+
}
|
|
837
|
+
let schemaId = validationSchemaIds.get(schema);
|
|
838
|
+
if (schemaId === undefined) {
|
|
839
|
+
schemaId = ++nextValidationSchemaId;
|
|
840
|
+
validationSchemaIds.set(schema, schemaId);
|
|
841
|
+
}
|
|
842
|
+
let identity;
|
|
843
|
+
try {
|
|
844
|
+
identity = JSON.stringify([cacheKey, schemaId, schemaVersion, typeof data, typeof data === "number" && Object.is(data, -0) ? "-0" : String(data)]);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
725
847
|
return schema.parse(data);
|
|
726
848
|
}
|
|
727
|
-
const cached = validationCache.get(
|
|
849
|
+
const cached = validationCache.get(identity);
|
|
728
850
|
if (cached && cached.schemaVersion === schemaVersion) {
|
|
729
851
|
return cached.result;
|
|
730
852
|
}
|
|
731
853
|
const result = schema.parse(data);
|
|
732
|
-
|
|
854
|
+
if ((result !== null && typeof result === "object") || typeof result === "function" || typeof result === "symbol")
|
|
855
|
+
return result;
|
|
856
|
+
validationCache.set(identity, {
|
|
733
857
|
result,
|
|
734
858
|
timestamp: Date.now(),
|
|
735
859
|
schemaVersion,
|
|
@@ -757,7 +881,7 @@ function getZodValidationPerformanceStats() {
|
|
|
757
881
|
* Clear performance metrics and caches
|
|
758
882
|
*/
|
|
759
883
|
function clearZodValidationCaches() {
|
|
760
|
-
|
|
884
|
+
schemaCaches = new WeakMap();
|
|
761
885
|
memoizedSchemaTransform.clear();
|
|
762
886
|
performanceMetrics.clear();
|
|
763
887
|
validationCache.clear();
|