codehooks-js 1.3.25 → 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.
@@ -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
+ }