codehooks-js 1.3.24 → 1.4.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/README.md +28 -0
- package/crudlify/index.mjs +4 -0
- package/crudlify/lib/schema/json-schema/index.mjs +35 -19
- package/index.js +76 -7
- package/openapi/crudlify-docs.mjs +823 -0
- package/openapi/generator.mjs +417 -0
- package/openapi/index.mjs +221 -0
- package/openapi/schema-converter.mjs +668 -0
- package/openapi/swagger-ui.mjs +92 -0
- package/package.json +12 -3
- package/types/index.d.ts +270 -10
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema conversion utilities
|
|
3
|
+
* Converts Zod, Yup, and JSON-Schema to OpenAPI-compatible JSON Schema
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect the schema type
|
|
8
|
+
* @param {object} schema - The schema object
|
|
9
|
+
* @returns {'zod' | 'yup' | 'json-schema' | 'unknown'}
|
|
10
|
+
*/
|
|
11
|
+
export function detectSchemaType(schema) {
|
|
12
|
+
if (!schema || typeof schema !== 'object') {
|
|
13
|
+
return 'unknown';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Zod detection - multiple checks for robustness
|
|
17
|
+
// Check 1: has .parse method and _def
|
|
18
|
+
if (typeof schema.parse === 'function' && schema._def) {
|
|
19
|
+
return 'zod';
|
|
20
|
+
}
|
|
21
|
+
// Check 2: has _def with typeName (Zod internal)
|
|
22
|
+
if (schema._def && typeof schema._def.typeName === 'string' && schema._def.typeName.startsWith('Zod')) {
|
|
23
|
+
return 'zod';
|
|
24
|
+
}
|
|
25
|
+
// Check 3: has .safeParse (Zod v3 method)
|
|
26
|
+
if (typeof schema.safeParse === 'function' && typeof schema.parse === 'function') {
|
|
27
|
+
return 'zod';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Yup detection - has .validate method and .fields or __isYupSchema__
|
|
31
|
+
if (typeof schema.validate === 'function' && (schema.fields || schema.__isYupSchema__)) {
|
|
32
|
+
return 'yup';
|
|
33
|
+
}
|
|
34
|
+
// Yup also has .isValid and .cast
|
|
35
|
+
if (typeof schema.validate === 'function' && typeof schema.isValid === 'function' && typeof schema.cast === 'function') {
|
|
36
|
+
return 'yup';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// JSON-Schema detection - has type or properties at top level
|
|
40
|
+
if (schema.type || schema.properties || schema.$schema) {
|
|
41
|
+
return 'json-schema';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 'unknown';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Convert any supported schema to JSON Schema
|
|
49
|
+
* @param {object} schema - Zod, Yup, or JSON-Schema object
|
|
50
|
+
* @param {string} [schemaType] - Override schema type detection
|
|
51
|
+
* @returns {Promise<object>} JSON Schema
|
|
52
|
+
*/
|
|
53
|
+
export async function convertToJsonSchema(schema, schemaType) {
|
|
54
|
+
const type = schemaType || detectSchemaType(schema);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
switch (type) {
|
|
58
|
+
case 'zod':
|
|
59
|
+
return await convertZodSchema(schema);
|
|
60
|
+
case 'yup':
|
|
61
|
+
return await convertYupSchema(schema);
|
|
62
|
+
case 'json-schema':
|
|
63
|
+
return cleanJsonSchema(schema);
|
|
64
|
+
default:
|
|
65
|
+
console.warn('[OpenAPI] Unknown schema type, using generic object schema');
|
|
66
|
+
return { type: 'object' };
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn('[OpenAPI] Schema conversion failed:', error.message);
|
|
70
|
+
return { type: 'object' };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert Zod schema to JSON Schema
|
|
76
|
+
* Uses built-in converter for serverless compatibility
|
|
77
|
+
* @param {object} zodSchema - Zod schema
|
|
78
|
+
* @returns {Promise<object>} JSON Schema
|
|
79
|
+
*/
|
|
80
|
+
async function convertZodSchema(zodSchema) {
|
|
81
|
+
return convertZodManually(zodSchema);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Infer Zod type from schema properties when typeName isn't reliable
|
|
86
|
+
* @param {object} schema - Zod schema
|
|
87
|
+
* @param {object} def - Schema _def
|
|
88
|
+
* @param {string} typeName - The typeName (may be empty/different after bundling)
|
|
89
|
+
* @returns {object|null} JSON Schema or null if can't infer
|
|
90
|
+
*/
|
|
91
|
+
function inferZodType(schema, def, typeName) {
|
|
92
|
+
// Check if typeName contains type hints (case insensitive)
|
|
93
|
+
const typeNameLower = (typeName || '').toLowerCase();
|
|
94
|
+
|
|
95
|
+
// Method-based detection (survives webpack bundling)
|
|
96
|
+
// ZodString has .email(), .url(), .uuid(), .trim() methods
|
|
97
|
+
// ZodNumber has .int(), .positive(), .negative(), .finite() methods
|
|
98
|
+
// ZodBoolean has no unique methods but we check for it
|
|
99
|
+
const hasStringMethods = typeof schema?.email === 'function' ||
|
|
100
|
+
typeof schema?.url === 'function' ||
|
|
101
|
+
typeof schema?.uuid === 'function' ||
|
|
102
|
+
typeof schema?.trim === 'function' ||
|
|
103
|
+
typeof schema?.toLowerCase === 'function';
|
|
104
|
+
|
|
105
|
+
const hasNumberMethods = typeof schema?.int === 'function' ||
|
|
106
|
+
typeof schema?.positive === 'function' ||
|
|
107
|
+
typeof schema?.negative === 'function' ||
|
|
108
|
+
typeof schema?.finite === 'function' ||
|
|
109
|
+
typeof schema?.multipleOf === 'function';
|
|
110
|
+
|
|
111
|
+
const hasArrayMethods = typeof schema?.element === 'function' ||
|
|
112
|
+
typeof schema?.nonempty === 'function';
|
|
113
|
+
|
|
114
|
+
// Boolean detection: has no distinguishing methods, but check _def structure
|
|
115
|
+
// ZodBoolean has _def with typeName and optionally coerce, but no innerType/values/type/shape/schema
|
|
116
|
+
const looksLikeBoolean = typeof schema?.parse === 'function' &&
|
|
117
|
+
!hasStringMethods &&
|
|
118
|
+
!hasNumberMethods &&
|
|
119
|
+
!hasArrayMethods &&
|
|
120
|
+
!schema.shape &&
|
|
121
|
+
!def?.innerType &&
|
|
122
|
+
!def?.values &&
|
|
123
|
+
!def?.type &&
|
|
124
|
+
!def?.schema &&
|
|
125
|
+
def?.typeName; // Has a typeName but nothing else distinctive
|
|
126
|
+
|
|
127
|
+
// String detection
|
|
128
|
+
if (typeNameLower.includes('string') || hasStringMethods) {
|
|
129
|
+
const result = { type: 'string' };
|
|
130
|
+
if (def.checks) {
|
|
131
|
+
for (const check of def.checks) {
|
|
132
|
+
// Support both old Zod (check.kind/check.value) and new Zod (check._zod.def)
|
|
133
|
+
const checkDef = check._zod?.def || check;
|
|
134
|
+
const kind = checkDef.check || checkDef.kind;
|
|
135
|
+
if (kind === 'email') result.format = 'email';
|
|
136
|
+
if (kind === 'url') result.format = 'uri';
|
|
137
|
+
if (kind === 'uuid') result.format = 'uuid';
|
|
138
|
+
if (kind === 'min' || kind === 'min_length') result.minLength = checkDef.minimum ?? checkDef.value;
|
|
139
|
+
if (kind === 'max' || kind === 'max_length') result.maxLength = checkDef.maximum ?? checkDef.value;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Description can be on def or on schema object directly
|
|
143
|
+
if (def.description || schema.description) result.description = def.description || schema.description;
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Number/Integer detection
|
|
148
|
+
if (typeNameLower.includes('number') || typeNameLower.includes('int') || hasNumberMethods) {
|
|
149
|
+
const result = { type: 'number' };
|
|
150
|
+
if (def.checks) {
|
|
151
|
+
for (const check of def.checks) {
|
|
152
|
+
// Support both old Zod (check.kind/check.value) and new Zod (check._zod.def)
|
|
153
|
+
const checkDef = check._zod?.def || check;
|
|
154
|
+
const kind = checkDef.check || checkDef.kind;
|
|
155
|
+
if (kind === 'int') result.type = 'integer';
|
|
156
|
+
// Old Zod: min/max, New Zod: greater_than/less_than
|
|
157
|
+
if (kind === 'min' || kind === 'greater_than') result.minimum = checkDef.value ?? checkDef.minimum;
|
|
158
|
+
if (kind === 'max' || kind === 'less_than') result.maximum = checkDef.value ?? checkDef.maximum;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Description can be on def or on schema object directly
|
|
162
|
+
if (def.description || schema.description) result.description = def.description || schema.description;
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Boolean detection
|
|
167
|
+
if (typeNameLower.includes('boolean') || typeNameLower.includes('bool') || looksLikeBoolean) {
|
|
168
|
+
const result = { type: 'boolean' };
|
|
169
|
+
if (def.description || schema.description) result.description = def.description || schema.description;
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Date detection
|
|
174
|
+
if (typeNameLower.includes('date')) {
|
|
175
|
+
return { type: 'string', format: 'date-time' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Array detection
|
|
179
|
+
if (typeNameLower.includes('array') || hasArrayMethods) {
|
|
180
|
+
// Support both old (def.type) and new (def.element) Zod structures
|
|
181
|
+
const elementSchema = def.element || (typeof def.type === 'object' ? def.type : null);
|
|
182
|
+
return {
|
|
183
|
+
type: 'array',
|
|
184
|
+
items: elementSchema ? convertZodManually(elementSchema) : {}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Enum detection
|
|
189
|
+
if (typeNameLower.includes('enum') || (def.values && Array.isArray(def.values))) {
|
|
190
|
+
return { type: 'string', enum: def.values };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Literal detection
|
|
194
|
+
if (typeNameLower.includes('literal') || (def.value !== undefined && !def.innerType)) {
|
|
195
|
+
const value = def.value;
|
|
196
|
+
return { type: typeof value, enum: [value] };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Optional/Nullable detection
|
|
200
|
+
if (typeNameLower.includes('optional') || typeNameLower.includes('nullable')) {
|
|
201
|
+
if (def.innerType) {
|
|
202
|
+
return convertZodManually(def.innerType);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Default detection - check for defaultValue property (more reliable than typeName)
|
|
207
|
+
if ((typeNameLower.includes('default') || def.defaultValue !== undefined) && def.innerType) {
|
|
208
|
+
const inner = convertZodManually(def.innerType);
|
|
209
|
+
try {
|
|
210
|
+
inner.default = typeof def.defaultValue === 'function' ? def.defaultValue() : def.defaultValue;
|
|
211
|
+
} catch (e) {
|
|
212
|
+
// Ignore
|
|
213
|
+
}
|
|
214
|
+
// Preserve description from outer schema
|
|
215
|
+
if (!inner.description && schema.description) {
|
|
216
|
+
inner.description = schema.description;
|
|
217
|
+
}
|
|
218
|
+
return inner;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Effects detection (refine, transform)
|
|
222
|
+
if (typeNameLower.includes('effect') && def.schema) {
|
|
223
|
+
return convertZodManually(def.schema);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If we have an innerType but couldn't match, try to unwrap it
|
|
227
|
+
if (def.innerType) {
|
|
228
|
+
return convertZodManually(def.innerType);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return null; // Couldn't infer, let the main function handle it
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Manual Zod conversion (handles Zod v3.x)
|
|
236
|
+
* @param {object} zodSchema - Zod schema
|
|
237
|
+
* @returns {object} JSON Schema
|
|
238
|
+
*/
|
|
239
|
+
function convertZodManually(zodSchema) {
|
|
240
|
+
if (!zodSchema) return { type: 'object' };
|
|
241
|
+
|
|
242
|
+
// Try multiple approaches to get the shape (Zod v3 compatibility)
|
|
243
|
+
let shape = null;
|
|
244
|
+
|
|
245
|
+
// Approach 1: Direct .shape property (getter in Zod v3)
|
|
246
|
+
try {
|
|
247
|
+
if (zodSchema.shape && typeof zodSchema.shape === 'object') {
|
|
248
|
+
shape = zodSchema.shape;
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// .shape getter might throw
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Approach 2: _def.shape (might be function or object)
|
|
255
|
+
if (!shape && zodSchema._def) {
|
|
256
|
+
try {
|
|
257
|
+
if (typeof zodSchema._def.shape === 'function') {
|
|
258
|
+
shape = zodSchema._def.shape();
|
|
259
|
+
} else if (zodSchema._def.shape && typeof zodSchema._def.shape === 'object') {
|
|
260
|
+
shape = zodSchema._def.shape;
|
|
261
|
+
}
|
|
262
|
+
} catch (e) {
|
|
263
|
+
// _def.shape() might throw
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Approach 3: Check if it's ZodObject and try _cached property
|
|
268
|
+
if (!shape && zodSchema._def?.typeName === 'ZodObject') {
|
|
269
|
+
try {
|
|
270
|
+
// Some Zod versions cache the shape
|
|
271
|
+
if (zodSchema._cached?.shape) {
|
|
272
|
+
shape = zodSchema._cached.shape;
|
|
273
|
+
}
|
|
274
|
+
} catch (e) {
|
|
275
|
+
// Ignore
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// If we found a shape, convert it
|
|
280
|
+
if (shape && typeof shape === 'object' && Object.keys(shape).length > 0) {
|
|
281
|
+
const properties = {};
|
|
282
|
+
const required = [];
|
|
283
|
+
|
|
284
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
285
|
+
if (fieldSchema) {
|
|
286
|
+
properties[key] = convertZodManually(fieldSchema);
|
|
287
|
+
|
|
288
|
+
// Check if optional (support both old and new Zod structures)
|
|
289
|
+
// Fields with defaults are also not required
|
|
290
|
+
const fieldDef = fieldSchema._def;
|
|
291
|
+
const fieldType = fieldDef?.typeName || fieldDef?.type || '';
|
|
292
|
+
const isOptional = fieldType === 'ZodOptional' || fieldType === 'optional' ||
|
|
293
|
+
fieldType === 'ZodNullable' || fieldType === 'nullable' ||
|
|
294
|
+
fieldType === 'ZodDefault' || fieldType === 'default';
|
|
295
|
+
if (!isOptional) {
|
|
296
|
+
required.push(key);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
type: 'object',
|
|
303
|
+
properties,
|
|
304
|
+
...(required.length > 0 && { required })
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const def = zodSchema._def;
|
|
309
|
+
if (!def) return { type: 'object' };
|
|
310
|
+
|
|
311
|
+
// Support both old Zod (typeName) and new Zod (type) structures
|
|
312
|
+
const typeName = def.typeName || def.type || '';
|
|
313
|
+
|
|
314
|
+
// Fallback type detection if typeName is missing or different
|
|
315
|
+
// Check for characteristic Zod properties
|
|
316
|
+
const inferredType = inferZodType(zodSchema, def, typeName);
|
|
317
|
+
if (inferredType) {
|
|
318
|
+
return inferredType;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Handle string with refinements (like .email())
|
|
322
|
+
if (typeName === 'ZodString' || typeName === 'string') {
|
|
323
|
+
const result = { type: 'string' };
|
|
324
|
+
// Check for email, url, uuid checks
|
|
325
|
+
if (def.checks) {
|
|
326
|
+
for (const check of def.checks) {
|
|
327
|
+
// Support both old Zod (check.kind/check.value) and new Zod (check._zod.def)
|
|
328
|
+
const checkDef = check._zod?.def || check;
|
|
329
|
+
const kind = checkDef.check || checkDef.kind;
|
|
330
|
+
if (kind === 'email') result.format = 'email';
|
|
331
|
+
if (kind === 'url') result.format = 'uri';
|
|
332
|
+
if (kind === 'uuid') result.format = 'uuid';
|
|
333
|
+
if (kind === 'min' || kind === 'min_length') result.minLength = checkDef.minimum ?? checkDef.value;
|
|
334
|
+
if (kind === 'max' || kind === 'max_length') result.maxLength = checkDef.maximum ?? checkDef.value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Description can be on def or on schema object directly
|
|
338
|
+
if (def.description || zodSchema.description) result.description = def.description || zodSchema.description;
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (typeName === 'ZodNumber' || typeName === 'number') {
|
|
343
|
+
const result = { type: 'number' };
|
|
344
|
+
if (def.checks) {
|
|
345
|
+
for (const check of def.checks) {
|
|
346
|
+
// Support both old Zod (check.kind/check.value) and new Zod (check._zod.def)
|
|
347
|
+
const checkDef = check._zod?.def || check;
|
|
348
|
+
const kind = checkDef.check || checkDef.kind;
|
|
349
|
+
if (kind === 'int') result.type = 'integer';
|
|
350
|
+
// Old Zod: min/max, New Zod: greater_than/less_than
|
|
351
|
+
if (kind === 'min' || kind === 'greater_than') result.minimum = checkDef.value ?? checkDef.minimum;
|
|
352
|
+
if (kind === 'max' || kind === 'less_than') result.maximum = checkDef.value ?? checkDef.maximum;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// Description can be on def or on schema object directly
|
|
356
|
+
if (def.description || zodSchema.description) result.description = def.description || zodSchema.description;
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (typeName === 'ZodBoolean' || typeName === 'boolean') {
|
|
361
|
+
const result = { type: 'boolean' };
|
|
362
|
+
if (def.description || zodSchema.description) result.description = def.description || zodSchema.description;
|
|
363
|
+
return result;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (typeName === 'ZodArray' || typeName === 'array') {
|
|
367
|
+
// Support both old (def.type) and new (def.element) Zod structures
|
|
368
|
+
const elementSchema = def.element || def.type;
|
|
369
|
+
return {
|
|
370
|
+
type: 'array',
|
|
371
|
+
items: elementSchema && typeof elementSchema === 'object' ? convertZodManually(elementSchema) : {}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Fallback for ZodObject if .shape wasn't available earlier
|
|
376
|
+
if (typeName === 'ZodObject' || typeName === 'object') {
|
|
377
|
+
const properties = {};
|
|
378
|
+
const required = [];
|
|
379
|
+
|
|
380
|
+
// Try _def.shape as fallback
|
|
381
|
+
let shape = null;
|
|
382
|
+
if (def.shape) {
|
|
383
|
+
shape = typeof def.shape === 'function' ? def.shape() : def.shape;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (shape && typeof shape === 'object') {
|
|
387
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
388
|
+
if (fieldSchema) {
|
|
389
|
+
properties[key] = convertZodManually(fieldSchema);
|
|
390
|
+
const fieldType = fieldSchema._def?.typeName || fieldSchema._def?.type || '';
|
|
391
|
+
const isOptional = fieldType === 'ZodOptional' || fieldType === 'optional' ||
|
|
392
|
+
fieldType === 'ZodNullable' || fieldType === 'nullable' ||
|
|
393
|
+
fieldType === 'ZodDefault' || fieldType === 'default';
|
|
394
|
+
if (!isOptional) {
|
|
395
|
+
required.push(key);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
type: 'object',
|
|
403
|
+
properties,
|
|
404
|
+
...(required.length > 0 && { required })
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (typeName === 'ZodEnum' || typeName === 'enum') {
|
|
409
|
+
return { type: 'string', enum: def.values };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (typeName === 'ZodLiteral' || typeName === 'literal') {
|
|
413
|
+
const value = def.value;
|
|
414
|
+
return { type: typeof value, enum: [value] };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (typeName === 'ZodOptional' || typeName === 'optional' ||
|
|
418
|
+
typeName === 'ZodNullable' || typeName === 'nullable') {
|
|
419
|
+
return convertZodManually(def.innerType);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (typeName === 'ZodDefault' || typeName === 'default') {
|
|
423
|
+
const inner = convertZodManually(def.innerType);
|
|
424
|
+
try {
|
|
425
|
+
inner.default = typeof def.defaultValue === 'function' ? def.defaultValue() : def.defaultValue;
|
|
426
|
+
} catch (e) {
|
|
427
|
+
// Ignore default value errors
|
|
428
|
+
}
|
|
429
|
+
// Preserve description from outer schema if inner doesn't have one
|
|
430
|
+
if (!inner.description && zodSchema.description) {
|
|
431
|
+
inner.description = zodSchema.description;
|
|
432
|
+
}
|
|
433
|
+
return inner;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (typeName === 'ZodEffects' || typeName === 'effects') {
|
|
437
|
+
// .refine(), .transform(), etc - use inner schema
|
|
438
|
+
return convertZodManually(def.schema);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Structural fallbacks when typeName is mangled/missing
|
|
442
|
+
|
|
443
|
+
// Default: has defaultValue and innerType
|
|
444
|
+
if (def.defaultValue !== undefined && def.innerType) {
|
|
445
|
+
const inner = convertZodManually(def.innerType);
|
|
446
|
+
try {
|
|
447
|
+
inner.default = typeof def.defaultValue === 'function' ? def.defaultValue() : def.defaultValue;
|
|
448
|
+
} catch (e) {
|
|
449
|
+
// Ignore
|
|
450
|
+
}
|
|
451
|
+
// Preserve description from outer schema
|
|
452
|
+
if (!inner.description && zodSchema.description) {
|
|
453
|
+
inner.description = zodSchema.description;
|
|
454
|
+
}
|
|
455
|
+
return inner;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Optional/Nullable: has innerType but no defaultValue
|
|
459
|
+
if (def.innerType) {
|
|
460
|
+
return convertZodManually(def.innerType);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Effects: has schema property
|
|
464
|
+
if (def.schema) {
|
|
465
|
+
return convertZodManually(def.schema);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Boolean: simple schema with no distinctive properties
|
|
469
|
+
// (no innerType, no values, no type, no shape, no schema)
|
|
470
|
+
if (!def.innerType && !def.values && !def.type && !def.schema) {
|
|
471
|
+
// Could be boolean - check if coerce exists (Zod boolean feature)
|
|
472
|
+
// or if it's truly a simple primitive
|
|
473
|
+
if (def.coerce !== undefined || Object.keys(def).length <= 3) {
|
|
474
|
+
return { type: 'boolean' };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Fallback
|
|
479
|
+
return { type: 'object' };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Convert Yup schema to JSON Schema
|
|
484
|
+
* Uses built-in converter for serverless compatibility
|
|
485
|
+
* @param {object} yupSchema - Yup schema
|
|
486
|
+
* @returns {Promise<object>} JSON Schema
|
|
487
|
+
*/
|
|
488
|
+
async function convertYupSchema(yupSchema) {
|
|
489
|
+
return convertYupManually(yupSchema);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Manual Yup conversion fallback using .describe()
|
|
494
|
+
* @param {object} yupSchema - Yup schema
|
|
495
|
+
* @returns {object} JSON Schema
|
|
496
|
+
*/
|
|
497
|
+
function convertYupManually(yupSchema) {
|
|
498
|
+
if (!yupSchema.describe) {
|
|
499
|
+
return { type: 'object' };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const desc = yupSchema.describe();
|
|
503
|
+
return yupDescToJsonSchema(desc);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Convert Yup describe() output to JSON Schema
|
|
508
|
+
* @param {object} desc - Yup description object
|
|
509
|
+
* @returns {object} JSON Schema
|
|
510
|
+
*/
|
|
511
|
+
function yupDescToJsonSchema(desc) {
|
|
512
|
+
const schema = {};
|
|
513
|
+
|
|
514
|
+
switch (desc.type) {
|
|
515
|
+
case 'string':
|
|
516
|
+
schema.type = 'string';
|
|
517
|
+
// Extract string constraints from tests
|
|
518
|
+
if (desc.tests) {
|
|
519
|
+
for (const test of desc.tests) {
|
|
520
|
+
if (test.name === 'min' && test.params?.min !== undefined) {
|
|
521
|
+
schema.minLength = test.params.min;
|
|
522
|
+
}
|
|
523
|
+
if (test.name === 'max' && test.params?.max !== undefined) {
|
|
524
|
+
schema.maxLength = test.params.max;
|
|
525
|
+
}
|
|
526
|
+
if (test.name === 'email') {
|
|
527
|
+
schema.format = 'email';
|
|
528
|
+
}
|
|
529
|
+
if (test.name === 'url') {
|
|
530
|
+
schema.format = 'uri';
|
|
531
|
+
}
|
|
532
|
+
if (test.name === 'uuid') {
|
|
533
|
+
schema.format = 'uuid';
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
break;
|
|
538
|
+
case 'number':
|
|
539
|
+
schema.type = 'number';
|
|
540
|
+
// Extract number constraints from tests
|
|
541
|
+
if (desc.tests) {
|
|
542
|
+
for (const test of desc.tests) {
|
|
543
|
+
if (test.name === 'min' && test.params?.min !== undefined) {
|
|
544
|
+
schema.minimum = test.params.min;
|
|
545
|
+
}
|
|
546
|
+
if (test.name === 'max' && test.params?.max !== undefined) {
|
|
547
|
+
schema.maximum = test.params.max;
|
|
548
|
+
}
|
|
549
|
+
if (test.name === 'integer') {
|
|
550
|
+
schema.type = 'integer';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
break;
|
|
555
|
+
case 'boolean':
|
|
556
|
+
schema.type = 'boolean';
|
|
557
|
+
break;
|
|
558
|
+
case 'date':
|
|
559
|
+
schema.type = 'string';
|
|
560
|
+
schema.format = 'date-time';
|
|
561
|
+
break;
|
|
562
|
+
case 'array':
|
|
563
|
+
schema.type = 'array';
|
|
564
|
+
if (desc.innerType) {
|
|
565
|
+
schema.items = yupDescToJsonSchema(desc.innerType);
|
|
566
|
+
}
|
|
567
|
+
// Extract array constraints from tests
|
|
568
|
+
if (desc.tests) {
|
|
569
|
+
for (const test of desc.tests) {
|
|
570
|
+
if (test.name === 'min' && test.params?.min !== undefined) {
|
|
571
|
+
schema.minItems = test.params.min;
|
|
572
|
+
}
|
|
573
|
+
if (test.name === 'max' && test.params?.max !== undefined) {
|
|
574
|
+
schema.maxItems = test.params.max;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
case 'object':
|
|
580
|
+
schema.type = 'object';
|
|
581
|
+
if (desc.fields) {
|
|
582
|
+
schema.properties = {};
|
|
583
|
+
const required = [];
|
|
584
|
+
for (const [key, fieldDesc] of Object.entries(desc.fields)) {
|
|
585
|
+
schema.properties[key] = yupDescToJsonSchema(fieldDesc);
|
|
586
|
+
if (!fieldDesc.optional && !fieldDesc.nullable) {
|
|
587
|
+
required.push(key);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (required.length > 0) {
|
|
591
|
+
schema.required = required;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
break;
|
|
595
|
+
default:
|
|
596
|
+
schema.type = 'object';
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Add metadata
|
|
600
|
+
if (desc.label) {
|
|
601
|
+
schema.title = desc.label;
|
|
602
|
+
}
|
|
603
|
+
if (desc.meta?.description) {
|
|
604
|
+
schema.description = desc.meta.description;
|
|
605
|
+
}
|
|
606
|
+
if (desc.default !== undefined) {
|
|
607
|
+
schema.default = desc.default;
|
|
608
|
+
}
|
|
609
|
+
if (desc.oneOf && desc.oneOf.length > 0) {
|
|
610
|
+
schema.enum = desc.oneOf;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return schema;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Clean JSON Schema for OpenAPI compatibility
|
|
618
|
+
* @param {object} schema - JSON Schema
|
|
619
|
+
* @returns {object} Cleaned schema
|
|
620
|
+
*/
|
|
621
|
+
function cleanJsonSchema(schema) {
|
|
622
|
+
if (!schema || typeof schema !== 'object') {
|
|
623
|
+
return schema;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const cleaned = { ...schema };
|
|
627
|
+
|
|
628
|
+
// Remove $schema as it's not needed in OpenAPI components
|
|
629
|
+
delete cleaned.$schema;
|
|
630
|
+
|
|
631
|
+
// Remove $id if present
|
|
632
|
+
delete cleaned.$id;
|
|
633
|
+
|
|
634
|
+
// Recursively clean nested schemas
|
|
635
|
+
if (cleaned.properties) {
|
|
636
|
+
cleaned.properties = {};
|
|
637
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
638
|
+
cleaned.properties[key] = cleanJsonSchema(value);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (cleaned.items) {
|
|
643
|
+
cleaned.items = cleanJsonSchema(schema.items);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (cleaned.additionalProperties && typeof cleaned.additionalProperties === 'object') {
|
|
647
|
+
cleaned.additionalProperties = cleanJsonSchema(schema.additionalProperties);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return cleaned;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Convert multiple schemas (collection map)
|
|
655
|
+
* @param {object} schemas - Map of collection name to schema
|
|
656
|
+
* @returns {Promise<object>} Map of collection name to JSON Schema
|
|
657
|
+
*/
|
|
658
|
+
export async function convertSchemas(schemas) {
|
|
659
|
+
const result = {};
|
|
660
|
+
|
|
661
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
662
|
+
if (schema) {
|
|
663
|
+
result[name] = await convertToJsonSchema(schema);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return result;
|
|
668
|
+
}
|