ormbridge 0.13.0 → 0.14.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.
Files changed (60) hide show
  1. package/README.md +5 -4
  2. package/dist/cliRunner.js +18 -0
  3. package/dist/cliRunner.js.map +1 -1
  4. package/dist/config.d.ts +5 -0
  5. package/dist/config.js +17 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/emitters/jpa.d.ts +17 -2
  8. package/dist/emitters/jpa.js +55 -7
  9. package/dist/emitters/jpa.js.map +1 -1
  10. package/dist/emitters/jpaKeyClass.d.ts +18 -0
  11. package/dist/emitters/jpaKeyClass.js +88 -0
  12. package/dist/emitters/jpaKeyClass.js.map +1 -0
  13. package/dist/emitters/jpaKeys.d.ts +1 -7
  14. package/dist/emitters/jpaKeys.js +0 -43
  15. package/dist/emitters/jpaKeys.js.map +1 -1
  16. package/dist/emitters/jpaRelations.d.ts +5 -1
  17. package/dist/emitters/jpaRelations.js +7 -1
  18. package/dist/emitters/jpaRelations.js.map +1 -1
  19. package/dist/emitters/jpaRender.d.ts +5 -1
  20. package/dist/emitters/jpaRender.js +11 -4
  21. package/dist/emitters/jpaRender.js.map +1 -1
  22. package/dist/emitters/jpaTypes.d.ts +9 -3
  23. package/dist/emitters/jpaTypes.js +8 -0
  24. package/dist/emitters/jpaTypes.js.map +1 -1
  25. package/dist/formats.d.ts +11 -0
  26. package/dist/formats.js +13 -3
  27. package/dist/formats.js.map +1 -1
  28. package/dist/index.d.ts +5 -2
  29. package/dist/index.js +3 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/io.js +62 -21
  32. package/dist/io.js.map +1 -1
  33. package/dist/parsers/jpaBuild.js +2 -1
  34. package/dist/parsers/jpaBuild.js.map +1 -1
  35. package/dist/parsers/jpaClasses.js +7 -1
  36. package/dist/parsers/jpaClasses.js.map +1 -1
  37. package/dist/parsers/jpaColumns.d.ts +4 -1
  38. package/dist/parsers/jpaColumns.js +8 -2
  39. package/dist/parsers/jpaColumns.js.map +1 -1
  40. package/dist/parsers/jpaEntities.js +15 -0
  41. package/dist/parsers/jpaEntities.js.map +1 -1
  42. package/dist/parsers/jpaModel.d.ts +6 -0
  43. package/dist/parsers/sqlDdl.js +1306 -65
  44. package/dist/parsers/sqlDdl.js.map +1 -1
  45. package/dist/parsers/sqlMigrationOrder.d.ts +8 -0
  46. package/dist/parsers/sqlMigrationOrder.js +110 -0
  47. package/dist/parsers/sqlMigrationOrder.js.map +1 -0
  48. package/dist/parsers/sqlSyntax.d.ts +6 -0
  49. package/dist/parsers/sqlSyntax.js +15 -2
  50. package/dist/parsers/sqlSyntax.js.map +1 -1
  51. package/dist/parsers/zod.d.ts +11 -0
  52. package/dist/parsers/zod.js +796 -0
  53. package/dist/parsers/zod.js.map +1 -0
  54. package/dist/parsers/zodFields.d.ts +73 -0
  55. package/dist/parsers/zodFields.js +681 -0
  56. package/dist/parsers/zodFields.js.map +1 -0
  57. package/dist/parsers/zodSyntax.d.ts +72 -0
  58. package/dist/parsers/zodSyntax.js +347 -0
  59. package/dist/parsers/zodSyntax.js.map +1 -0
  60. package/package.json +1 -1
@@ -0,0 +1,796 @@
1
+ import { toPascalCase, toSnakeCase } from '../naming.js';
2
+ import { err, ok } from '../result.js';
3
+ import { getTypeScriptParser } from './typescriptSyntax.js';
4
+ import { OBJECT_BASES, objectEnumValues, resolveFieldNode, } from './zodFields.js';
5
+ import { describeZodFile, enumMembers, literalOf, objectMembers, shapeReference, stripWrappers, unwindChain, } from './zodSyntax.js';
6
+ // ---------------------------------------------------------------------------
7
+ // Static tables
8
+ // ---------------------------------------------------------------------------
9
+ /** Methods that reshape an object schema; the reader applies them to the property list. */
10
+ const SHAPE_STEPS = new Set([
11
+ 'extend',
12
+ 'safeExtend',
13
+ 'merge',
14
+ 'pick',
15
+ 'omit',
16
+ 'partial',
17
+ 'required',
18
+ 'deepPartial',
19
+ ]);
20
+ /** Methods on an object schema that change nothing the shared model stores. */
21
+ const QUIET_OBJECT_STEPS = new Set([
22
+ 'strict',
23
+ 'strip',
24
+ 'passthrough',
25
+ 'loose',
26
+ 'nonstrict',
27
+ 'catchall',
28
+ 'describe',
29
+ 'meta',
30
+ ]);
31
+ /** Methods on an object schema that add rules the shared model cannot store. */
32
+ const RULE_OBJECT_STEPS = new Set([
33
+ 'refine',
34
+ 'superRefine',
35
+ 'transform',
36
+ 'pipe',
37
+ 'brand',
38
+ 'check',
39
+ 'overwrite',
40
+ 'readonly',
41
+ ]);
42
+ /** The suffixes the Zod writer adds to the schemas it derives from a model's row schema. */
43
+ const VARIANT_PATTERN = /^(.+?)(Create|Update|WithRelations)(Schema)?$/;
44
+ const ENUM_BASES = new Set(['enum', 'nativeEnum']);
45
+ /** Steps that leave a declared enum an enum (anything else makes it a property type to reuse). */
46
+ const ENUM_QUIET_STEPS = new Set([
47
+ 'describe',
48
+ 'meta',
49
+ 'brand',
50
+ 'readonly',
51
+ ]);
52
+ // ---------------------------------------------------------------------------
53
+ // Entry point
54
+ // ---------------------------------------------------------------------------
55
+ export async function parseZod(sources, options) {
56
+ const parserResult = await getTypeScriptParser();
57
+ if (!parserResult.ok) {
58
+ return parserResult;
59
+ }
60
+ const parser = parserResult.value;
61
+ const project = {
62
+ options,
63
+ files: [],
64
+ declarations: [],
65
+ byFile: new Map(),
66
+ global: new Map(),
67
+ tsEnums: new Map(),
68
+ kinds: new Map(),
69
+ modelNames: new Map(),
70
+ enumNames: new Map(),
71
+ enums: [],
72
+ shapes: new Map(),
73
+ hosts: new Map(),
74
+ warnings: [],
75
+ };
76
+ // The trees stay alive until the schema is built: the syntax nodes point into them.
77
+ const trees = [];
78
+ for (const source of sources) {
79
+ const tree = parser.parse(source.text);
80
+ trees.push(tree);
81
+ const file = describeZodFile(source.path, tree.rootNode);
82
+ project.files.push(file);
83
+ if (tree.rootNode.hasError) {
84
+ project.warnings.push(`${file.label}: the file contains TypeScript syntax errors; some schemas or properties may be missing from the output.`);
85
+ }
86
+ collectDeclarations(project, file, tree.rootNode);
87
+ }
88
+ const schema = buildSchema(project);
89
+ if (schema.models.length === 0) {
90
+ const checkedPaths = sources
91
+ .map((source) => source.path)
92
+ .join(', ');
93
+ return err('NO_MODELS_FOUND', `No Zod object schemas were found in: ${checkedPaths}. A model is a top-level \`export const UserSchema = z.object({ ... })\`; ` +
94
+ `schemas written inline, inside functions or only as TypeScript types do not make a model.`);
95
+ }
96
+ return ok(schema);
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Declarations
100
+ // ---------------------------------------------------------------------------
101
+ function collectDeclarations(project, file, root) {
102
+ const local = new Map();
103
+ project.byFile.set(file, local);
104
+ for (const statement of root.namedChildren) {
105
+ const declaration = statement.type === 'export_statement'
106
+ ? (statement.childForFieldName('declaration') ?? statement)
107
+ : statement;
108
+ if (declaration.type === 'enum_declaration') {
109
+ const nameNode = declaration.childForFieldName('name');
110
+ if (nameNode !== null) {
111
+ project.tsEnums.set(nameNode.text, { file, node: declaration });
112
+ }
113
+ continue;
114
+ }
115
+ if (declaration.type !== 'lexical_declaration' &&
116
+ declaration.type !== 'variable_declaration') {
117
+ continue;
118
+ }
119
+ for (const declarator of declaration.namedChildren) {
120
+ if (declarator.type !== 'variable_declarator') {
121
+ continue;
122
+ }
123
+ const nameNode = declarator.childForFieldName('name');
124
+ const valueNode = declarator.childForFieldName('value');
125
+ if (nameNode === null || valueNode === null) {
126
+ continue;
127
+ }
128
+ const entry = {
129
+ file,
130
+ name: nameNode.text,
131
+ node: valueNode,
132
+ };
133
+ project.declarations.push(entry);
134
+ local.set(entry.name, entry);
135
+ if (!project.global.has(entry.name)) {
136
+ project.global.set(entry.name, entry);
137
+ }
138
+ }
139
+ }
140
+ }
141
+ /** Finds a declared name, preferring the file that uses it over the other input files. */
142
+ function lookup(project, file, name) {
143
+ return project.byFile.get(file)?.get(name) ?? project.global.get(name);
144
+ }
145
+ function chainOf(declaration) {
146
+ return unwindChain(declaration.file, declaration.node);
147
+ }
148
+ function isStringLiteralUnion(file, chain) {
149
+ if (chain.base?.name !== 'union') {
150
+ return false;
151
+ }
152
+ const list = chain.base.args[0];
153
+ const items = list !== undefined && stripWrappers(list).type === 'array'
154
+ ? stripWrappers(list).namedChildren.filter((child) => child.type !== 'comment')
155
+ : [];
156
+ return (items.length > 1 &&
157
+ items.every((item) => {
158
+ const inner = unwindChain(file, item);
159
+ const argument = inner?.base?.args[0];
160
+ return (inner?.base?.name === 'literal' &&
161
+ inner.steps.length === 0 &&
162
+ argument !== undefined &&
163
+ typeof literalOf(argument) === 'string');
164
+ }));
165
+ }
166
+ /** Decides what a top-level declaration is. */
167
+ function classify(project, declaration) {
168
+ const known = project.kinds.get(declaration);
169
+ if (known !== undefined) {
170
+ return known;
171
+ }
172
+ // Guards against a schema defined in terms of itself.
173
+ project.kinds.set(declaration, 'other');
174
+ const result = classifyUncached(project, declaration);
175
+ project.kinds.set(declaration, result);
176
+ return result;
177
+ }
178
+ function classifyUncached(project, declaration) {
179
+ const value = stripWrappers(declaration.node);
180
+ if (value.type === 'array') {
181
+ return value.namedChildren.length > 0 &&
182
+ value.namedChildren.every((item) => typeof literalOf(item) === 'string')
183
+ ? 'values'
184
+ : 'other';
185
+ }
186
+ if (value.type === 'object') {
187
+ const { members } = objectMembers(value);
188
+ if (members.length === 0) {
189
+ return 'other';
190
+ }
191
+ if (members.every((member) => member.kind === 'property' &&
192
+ ['string', 'number'].includes(typeof literalOf(member.node)))) {
193
+ return 'values';
194
+ }
195
+ return 'shape';
196
+ }
197
+ const chain = chainOf(declaration);
198
+ if (chain === undefined) {
199
+ return 'other';
200
+ }
201
+ if (chain.base !== undefined) {
202
+ if (OBJECT_BASES.has(chain.base.name)) {
203
+ return 'object';
204
+ }
205
+ const enumLike = ENUM_BASES.has(chain.base.name) ||
206
+ isStringLiteralUnion(declaration.file, chain);
207
+ if (enumLike &&
208
+ chain.steps.every((step) => ENUM_QUIET_STEPS.has(step.name))) {
209
+ return 'enum';
210
+ }
211
+ return 'alias';
212
+ }
213
+ const target = lookup(project, declaration.file, chain.ref ?? '');
214
+ const targetKind = target === undefined ? undefined : classify(project, target);
215
+ if ((targetKind === 'object' || targetKind === 'derived') &&
216
+ chain.steps.length > 0 &&
217
+ chain.steps.every((step) => SHAPE_STEPS.has(step.name) ||
218
+ QUIET_OBJECT_STEPS.has(step.name) ||
219
+ RULE_OBJECT_STEPS.has(step.name))) {
220
+ return isVariantName(project, declaration) ? 'variant' : 'derived';
221
+ }
222
+ if (targetKind === 'enum' && chain.steps.length === 0) {
223
+ return 'enum';
224
+ }
225
+ if (targetKind === 'object' || targetKind === 'derived') {
226
+ // `const Account = UserSchema` is another name for the same schema, not a second model.
227
+ return 'alias';
228
+ }
229
+ if (targetKind === 'alias' || targetKind === 'enum') {
230
+ return 'alias';
231
+ }
232
+ if (target === undefined && chain.steps.some(looksLikeShapeStep)) {
233
+ project.warnings.push(`${schemaName(declaration.name)}: the schema is built from "${chain.ref ?? ''}", which is not in the input files, so it could not be resolved and was skipped. Pass the file that declares it too.`);
234
+ }
235
+ return 'other';
236
+ }
237
+ function looksLikeShapeStep(step) {
238
+ return (step.name === 'extend' ||
239
+ step.name === 'merge' ||
240
+ step.name === 'pick' ||
241
+ step.name === 'omit');
242
+ }
243
+ /** True for the schemas the Zod writer derives from a row schema (`PostCreateSchema`, `PostUpdateSchema`, ...). */
244
+ function isVariantName(project, declaration) {
245
+ const match = VARIANT_PATTERN.exec(declaration.name);
246
+ const base = match?.[1];
247
+ if (base === undefined) {
248
+ return false;
249
+ }
250
+ for (const candidate of [`${base}Schema`, base]) {
251
+ const target = lookup(project, declaration.file, candidate);
252
+ if (target !== undefined && target !== declaration) {
253
+ const kind = classify(project, target);
254
+ if (kind === 'object' || kind === 'derived') {
255
+ return true;
256
+ }
257
+ }
258
+ }
259
+ return false;
260
+ }
261
+ /** `UserSchema` -> `User`, `userProfileSchema` -> `UserProfile`. */
262
+ function schemaName(variableName) {
263
+ const stripped = variableName.replace(/Schema$/, '');
264
+ const base = stripped === '' ? variableName : stripped;
265
+ return base.charAt(0).toUpperCase() + base.slice(1);
266
+ }
267
+ // ---------------------------------------------------------------------------
268
+ // Shapes
269
+ // ---------------------------------------------------------------------------
270
+ function pushEntry(entries, entry) {
271
+ const index = entries.findIndex((existing) => existing.key === entry.key);
272
+ if (index >= 0) {
273
+ entries[index] = entry;
274
+ }
275
+ else {
276
+ entries.push(entry);
277
+ }
278
+ }
279
+ /** The properties named by `{ a: true, b: true }` (the mask of pick, omit, partial and required). */
280
+ function maskKeys(node) {
281
+ if (node === undefined) {
282
+ return undefined;
283
+ }
284
+ const current = stripWrappers(node);
285
+ if (current.type !== 'object') {
286
+ return undefined;
287
+ }
288
+ return objectMembers(current)
289
+ .members.filter((member) => member.kind === 'property')
290
+ .filter((member) => literalOf(member.node) !== false)
291
+ .map((member) => member.key);
292
+ }
293
+ /**
294
+ * The properties an expression stands for: an object literal, another schema's `.shape` or a const object
295
+ * of schemas. Returns undefined (and records a problem) when it cannot be read.
296
+ */
297
+ function entriesOfNode(project, file, node, shape, stack) {
298
+ const current = stripWrappers(node);
299
+ if (current.type === 'object') {
300
+ const entries = [];
301
+ const { members, skipped } = objectMembers(current);
302
+ for (const member of members) {
303
+ if (member.kind === 'property') {
304
+ pushEntry(entries, {
305
+ key: member.key,
306
+ file,
307
+ node: member.node,
308
+ partial: false,
309
+ required: false,
310
+ });
311
+ continue;
312
+ }
313
+ const spread = entriesOfNode(project, file, member.node, shape, stack);
314
+ for (const entry of spread ?? []) {
315
+ pushEntry(entries, entry);
316
+ }
317
+ }
318
+ for (const text of skipped) {
319
+ shape.problems.push(`\`${text.replace(/\s+/g, ' ').slice(0, 40)}\` is not a property this reader understands and was skipped.`);
320
+ }
321
+ return entries;
322
+ }
323
+ if (current.type === 'call_expression') {
324
+ // An object schema written in place: `.merge(z.object({ ... }))`.
325
+ const chain = unwindChain(file, current);
326
+ const argument = chain?.base?.args[0];
327
+ if (chain?.base !== undefined &&
328
+ OBJECT_BASES.has(chain.base.name) &&
329
+ argument !== undefined) {
330
+ return entriesOfNode(project, file, argument, shape, stack);
331
+ }
332
+ }
333
+ const name = shapeReference(current);
334
+ const target = name === undefined ? undefined : lookup(project, file, name);
335
+ if (target === undefined) {
336
+ shape.problems.push(`\`${current.text.replace(/\s+/g, ' ').slice(0, 40)}\` is not declared in the input files, so its properties could not be added.`);
337
+ return undefined;
338
+ }
339
+ const kind = classify(project, target);
340
+ if (kind === 'shape' || kind === 'object' || kind === 'derived') {
341
+ return shapeOf(project, target, stack)?.entries.map((entry) => ({ ...entry }));
342
+ }
343
+ shape.problems.push(`"${target.name}" is not an object schema, so its properties could not be added.`);
344
+ return undefined;
345
+ }
346
+ /** The properties of an object schema with its extend, pick, omit, partial and required calls applied. */
347
+ function shapeOf(project, declaration, stack = new Set()) {
348
+ if (project.shapes.has(declaration)) {
349
+ return project.shapes.get(declaration);
350
+ }
351
+ if (stack.has(declaration)) {
352
+ return undefined;
353
+ }
354
+ stack.add(declaration);
355
+ const shape = { entries: [], dropped: [], problems: [] };
356
+ const kind = classify(project, declaration);
357
+ let steps = [];
358
+ if (kind === 'shape') {
359
+ shape.entries =
360
+ entriesOfNode(project, declaration.file, declaration.node, shape, stack) ?? [];
361
+ }
362
+ else {
363
+ const chain = chainOf(declaration);
364
+ if (chain === undefined) {
365
+ stack.delete(declaration);
366
+ return undefined;
367
+ }
368
+ steps = chain.steps;
369
+ if (chain.base !== undefined) {
370
+ const argument = chain.base.args[0];
371
+ shape.entries =
372
+ argument === undefined
373
+ ? []
374
+ : (entriesOfNode(project, declaration.file, argument, shape, stack) ??
375
+ []);
376
+ }
377
+ else {
378
+ const target = lookup(project, declaration.file, chain.ref ?? '');
379
+ const inherited = target === undefined ? undefined : shapeOf(project, target, stack);
380
+ shape.entries = (inherited?.entries ?? []).map((entry) => ({ ...entry }));
381
+ }
382
+ }
383
+ for (const step of steps) {
384
+ applyShapeStep(project, declaration, shape, step, stack);
385
+ }
386
+ stack.delete(declaration);
387
+ project.shapes.set(declaration, shape);
388
+ return shape;
389
+ }
390
+ function applyShapeStep(project, declaration, shape, step, stack) {
391
+ const argument = step.args[0];
392
+ switch (step.name) {
393
+ case 'extend':
394
+ case 'safeExtend':
395
+ case 'merge': {
396
+ const added = argument === undefined
397
+ ? undefined
398
+ : entriesOfNode(project, declaration.file, argument, shape, stack);
399
+ for (const entry of added ?? []) {
400
+ pushEntry(shape.entries, entry);
401
+ }
402
+ return;
403
+ }
404
+ case 'pick':
405
+ case 'omit': {
406
+ const keys = maskKeys(argument);
407
+ if (keys === undefined) {
408
+ shape.problems.push(`.${step.name}() needs an object literal of property names; it was ignored.`);
409
+ return;
410
+ }
411
+ shape.entries = shape.entries.filter((entry) => step.name === 'pick'
412
+ ? keys.includes(entry.key)
413
+ : !keys.includes(entry.key));
414
+ return;
415
+ }
416
+ case 'partial':
417
+ case 'deepPartial':
418
+ case 'required': {
419
+ const keys = maskKeys(argument);
420
+ if (argument !== undefined && keys === undefined) {
421
+ shape.problems.push(`.${step.name}() needs an object literal of property names; it was ignored.`);
422
+ return;
423
+ }
424
+ for (const entry of shape.entries) {
425
+ if (keys === undefined || keys.includes(entry.key)) {
426
+ entry.partial = step.name !== 'required';
427
+ entry.required = step.name === 'required';
428
+ }
429
+ }
430
+ if (step.name === 'deepPartial') {
431
+ shape.dropped.push('.deepPartial() (only the top level was made optional)');
432
+ }
433
+ return;
434
+ }
435
+ default:
436
+ break;
437
+ }
438
+ if (RULE_OBJECT_STEPS.has(step.name)) {
439
+ shape.dropped.push(`.${step.name}()`);
440
+ }
441
+ }
442
+ // ---------------------------------------------------------------------------
443
+ // Field host: what a name means to the property resolver
444
+ // ---------------------------------------------------------------------------
445
+ function enumValuesOf(project, name) {
446
+ const tsEnum = project.tsEnums.get(name);
447
+ if (tsEnum !== undefined) {
448
+ const members = enumMembers(tsEnum.node);
449
+ const numeric = members.some((member) => typeof member.value === 'number');
450
+ return {
451
+ numeric,
452
+ values: numeric
453
+ ? []
454
+ : members.map((member) => ({
455
+ name: member.name,
456
+ dbValue: String(member.value),
457
+ })),
458
+ };
459
+ }
460
+ const declaration = project.global.get(name);
461
+ if (declaration === undefined) {
462
+ return undefined;
463
+ }
464
+ const value = stripWrappers(declaration.node);
465
+ if (value.type === 'object') {
466
+ return objectEnumValues(value);
467
+ }
468
+ if (value.type === 'array') {
469
+ const values = [];
470
+ for (const item of value.namedChildren) {
471
+ const literal = literalOf(item);
472
+ if (typeof literal === 'string') {
473
+ values.push({ name: literal, dbValue: literal });
474
+ }
475
+ }
476
+ return { values, numeric: false };
477
+ }
478
+ return undefined;
479
+ }
480
+ function sameValues(first, second) {
481
+ return (first.length === second.length &&
482
+ first.every((value, index) => value.name === second[index]?.name &&
483
+ value.dbValue === second[index]?.dbValue));
484
+ }
485
+ function addEnum(project, wanted, values) {
486
+ let name = wanted;
487
+ let counter = 2;
488
+ for (;;) {
489
+ const existing = project.enums.find((candidate) => candidate.name === name);
490
+ if (existing === undefined) {
491
+ project.enums.push({ name, values });
492
+ return name;
493
+ }
494
+ if (sameValues(existing.values, values)) {
495
+ return name;
496
+ }
497
+ name = `${wanted}${counter}`;
498
+ counter += 1;
499
+ }
500
+ }
501
+ function hostFor(project, file) {
502
+ const cached = project.hosts.get(file);
503
+ if (cached !== undefined) {
504
+ return cached;
505
+ }
506
+ const host = {
507
+ kindOf(name) {
508
+ const declaration = lookup(project, file, name);
509
+ if (declaration === undefined) {
510
+ return project.tsEnums.has(name) ? 'values' : undefined;
511
+ }
512
+ switch (classify(project, declaration)) {
513
+ case 'object':
514
+ case 'derived':
515
+ return 'model';
516
+ case 'enum':
517
+ return project.enumNames.has(declaration) ? 'enum' : 'alias';
518
+ case 'alias':
519
+ return 'alias';
520
+ case 'values':
521
+ return 'values';
522
+ default:
523
+ return undefined;
524
+ }
525
+ },
526
+ modelNameOf(name) {
527
+ const declaration = lookup(project, file, name);
528
+ return declaration === undefined
529
+ ? undefined
530
+ : project.modelNames.get(declaration);
531
+ },
532
+ enumNameOf(name) {
533
+ const declaration = lookup(project, file, name);
534
+ return declaration === undefined
535
+ ? undefined
536
+ : project.enumNames.get(declaration);
537
+ },
538
+ enumMemberName(enumName, value) {
539
+ return project.enums
540
+ .find((candidate) => candidate.name === enumName)
541
+ ?.values.find((member) => member.dbValue === value)?.name;
542
+ },
543
+ aliasOf(name) {
544
+ const declaration = lookup(project, file, name);
545
+ return declaration === undefined
546
+ ? undefined
547
+ : { file: declaration.file, node: declaration.node };
548
+ },
549
+ valuesOf(name) {
550
+ return enumValuesOf(project, name);
551
+ },
552
+ inlineEnum(model, key, values) {
553
+ return addEnum(project, `${model}${toPascalCase(key)}`, values);
554
+ },
555
+ };
556
+ project.hosts.set(file, host);
557
+ return host;
558
+ }
559
+ // ---------------------------------------------------------------------------
560
+ // Building the schema
561
+ // ---------------------------------------------------------------------------
562
+ function uniqueName(wanted, used, what, warnings) {
563
+ let name = wanted;
564
+ let counter = 2;
565
+ while (used.has(name)) {
566
+ name = `${wanted}${counter}`;
567
+ counter += 1;
568
+ }
569
+ if (name !== wanted) {
570
+ warnings.push(`${wanted}: another ${what} already has this name; this one was named "${name}".`);
571
+ }
572
+ used.add(name);
573
+ return name;
574
+ }
575
+ /** Registers the declared enums (`const RoleSchema = z.enum([...])`) in declaration order. */
576
+ function registerEnums(project) {
577
+ const used = new Set();
578
+ for (const declaration of project.declarations) {
579
+ if (classify(project, declaration) !== 'enum') {
580
+ continue;
581
+ }
582
+ let captured;
583
+ const base = hostFor(project, declaration.file);
584
+ const capturing = {
585
+ ...base,
586
+ inlineEnum(_model, _key, values) {
587
+ captured = values;
588
+ return '';
589
+ },
590
+ };
591
+ const scope = {
592
+ host: capturing,
593
+ file: declaration.file,
594
+ model: '',
595
+ key: '',
596
+ };
597
+ resolveFieldNode(scope, declaration.node);
598
+ if (captured === undefined) {
599
+ project.kinds.set(declaration, 'alias');
600
+ continue;
601
+ }
602
+ const wanted = schemaName(declaration.name);
603
+ const name = uniqueName(wanted, used, 'enum', project.warnings);
604
+ project.enums.push({ name, values: captured });
605
+ project.enumNames.set(declaration, name);
606
+ }
607
+ }
608
+ function buildSchema(project) {
609
+ const declarations = project.declarations;
610
+ // Classify everything first: a declaration can depend on one that comes later in the file.
611
+ for (const declaration of declarations) {
612
+ classify(project, declaration);
613
+ }
614
+ registerEnums(project);
615
+ const usedModelNames = new Set();
616
+ const modelDeclarations = [];
617
+ for (const declaration of declarations) {
618
+ const kind = classify(project, declaration);
619
+ if (kind === 'object' || kind === 'derived') {
620
+ project.modelNames.set(declaration, uniqueName(schemaName(declaration.name), usedModelNames, 'schema', project.warnings));
621
+ modelDeclarations.push(declaration);
622
+ }
623
+ }
624
+ const models = [];
625
+ const pending = new Map();
626
+ for (const declaration of modelDeclarations) {
627
+ const built = buildModel(project, declaration);
628
+ if (built !== undefined) {
629
+ models.push(built.model);
630
+ pending.set(built.model, built.relations);
631
+ }
632
+ }
633
+ linkRelations(project, models, pending);
634
+ return {
635
+ models,
636
+ enums: project.enums,
637
+ warnings: project.warnings,
638
+ };
639
+ }
640
+ function buildModel(project, declaration) {
641
+ const name = project.modelNames.get(declaration) ?? declaration.name;
642
+ const shape = shapeOf(project, declaration);
643
+ if (shape === undefined) {
644
+ project.warnings.push(`${name}: the schema refers to itself through its own properties and could not be read; it was skipped.`);
645
+ return undefined;
646
+ }
647
+ for (const problem of shape.problems) {
648
+ project.warnings.push(`${name}: ${problem}`);
649
+ }
650
+ const dropped = [];
651
+ if (shape.dropped.length > 0) {
652
+ dropped.push(`the schema itself (${shape.dropped.join(', ')})`);
653
+ }
654
+ const fields = [];
655
+ const relations = [];
656
+ for (const entry of shape.entries) {
657
+ const scope = {
658
+ host: hostFor(project, entry.file),
659
+ file: entry.file,
660
+ model: name,
661
+ key: entry.key,
662
+ };
663
+ const resolved = resolveFieldNode(scope, entry.node);
664
+ if (entry.partial) {
665
+ resolved.optional = true;
666
+ }
667
+ if (entry.required) {
668
+ resolved.optional = false;
669
+ }
670
+ for (const problem of resolved.problems) {
671
+ project.warnings.push(`${name}.${entry.key}: ${problem}`);
672
+ }
673
+ if (resolved.dropped.length > 0) {
674
+ dropped.push(`${entry.key} (${[...new Set(resolved.dropped)].join(', ')})`);
675
+ }
676
+ const nullable = resolved.nullable || (resolved.optional && !resolved.hasDefault);
677
+ if (resolved.relation !== undefined) {
678
+ relations.push({
679
+ key: entry.key,
680
+ target: resolved.relation.model,
681
+ many: resolved.relation.many,
682
+ nullable,
683
+ });
684
+ continue;
685
+ }
686
+ fields.push(toField(entry.key, resolved, nullable));
687
+ }
688
+ if (dropped.length > 0) {
689
+ project.warnings.push(`${name}: Zod rules the shared model has no place for were ignored: ${dropped.join('; ')}.`);
690
+ }
691
+ const model = {
692
+ name,
693
+ tableName: name,
694
+ appLabel: project.options.appLabel,
695
+ fields,
696
+ relations: [],
697
+ indexes: [],
698
+ };
699
+ markPrimaryKey(project, model);
700
+ return { model, relations };
701
+ }
702
+ function toField(key, resolved, nullable) {
703
+ const type = resolved.type === 'text' && resolved.maxLength !== undefined
704
+ ? 'string'
705
+ : resolved.type;
706
+ const field = {
707
+ name: key,
708
+ columnName: key,
709
+ type,
710
+ isPrimaryKey: false,
711
+ isUnique: false,
712
+ isNullable: nullable,
713
+ isAutoUpdated: false,
714
+ };
715
+ if (resolved.default !== undefined) {
716
+ field.default = resolved.default;
717
+ }
718
+ if (resolved.maxLength !== undefined) {
719
+ field.maxLength = resolved.maxLength;
720
+ }
721
+ if (resolved.maxDigits !== undefined) {
722
+ field.maxDigits = resolved.maxDigits;
723
+ }
724
+ if (resolved.decimalPlaces !== undefined) {
725
+ field.decimalPlaces = resolved.decimalPlaces;
726
+ }
727
+ if (resolved.enumName !== undefined) {
728
+ field.enumName = resolved.enumName;
729
+ }
730
+ if (resolved.arrayDepth > 0) {
731
+ field.arrayDepth = resolved.arrayDepth;
732
+ }
733
+ return field;
734
+ }
735
+ /** Zod has no keys: the property named `id` (in any case) is the primary key. */
736
+ function markPrimaryKey(project, model) {
737
+ const key = model.fields.find((field) => field.name.toLowerCase() === 'id');
738
+ if (key === undefined) {
739
+ project.warnings.push(`${model.name}: Zod has no primary keys and no property is named "id", so the model has no primary key. Add an "id" property.`);
740
+ return;
741
+ }
742
+ key.isPrimaryKey = true;
743
+ key.isNullable = false;
744
+ }
745
+ /**
746
+ * Turns the properties that hold another object schema into relations. A single schema is a foreign key
747
+ * (the closest thing in the shared model to an embedded object). A list of schemas is the reverse side of a
748
+ * foreign key when the other model points back, and a many-to-many otherwise.
749
+ */
750
+ function linkRelations(project, models, pending) {
751
+ const byName = new Map(models.map((model) => [model.name, model]));
752
+ for (const model of models) {
753
+ for (const relation of pending.get(model) ?? []) {
754
+ if (relation.many || !byName.has(relation.target)) {
755
+ continue;
756
+ }
757
+ model.relations.push({
758
+ name: relation.key,
759
+ kind: 'foreignKey',
760
+ targetModel: relation.target,
761
+ columnName: `${toSnakeCase(relation.key)}_id`,
762
+ isNullable: relation.nullable,
763
+ onDelete: 'noAction',
764
+ });
765
+ project.warnings.push(`${model.name}.${relation.key}: the property holds the schema of "${relation.target}", which was read as a foreign key to that model (column "${toSnakeCase(relation.key)}_id", no delete rule).`);
766
+ }
767
+ }
768
+ for (const model of models) {
769
+ for (const relation of pending.get(model) ?? []) {
770
+ if (!relation.many) {
771
+ continue;
772
+ }
773
+ const target = byName.get(relation.target);
774
+ if (target === undefined) {
775
+ continue;
776
+ }
777
+ const pointsBack = target.relations.find((candidate) => candidate.kind === 'foreignKey' &&
778
+ candidate.targetModel === model.name &&
779
+ candidate.relatedName === undefined);
780
+ if (pointsBack !== undefined) {
781
+ pointsBack.relatedName = relation.key;
782
+ continue;
783
+ }
784
+ model.relations.push({
785
+ name: relation.key,
786
+ kind: 'manyToMany',
787
+ targetModel: relation.target,
788
+ columnName: `${toSnakeCase(relation.key)}_id`,
789
+ isNullable: false,
790
+ onDelete: 'cascade',
791
+ });
792
+ project.warnings.push(`${model.name}.${relation.key}: the property holds a list of "${relation.target}" schemas and "${relation.target}" has no property that points back, so it was read as a many-to-many relation.`);
793
+ }
794
+ }
795
+ }
796
+ //# sourceMappingURL=zod.js.map