ormbridge 0.2.0 → 0.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.
Files changed (60) hide show
  1. package/README.md +270 -70
  2. package/dist/cliRunner.js +22 -2
  3. package/dist/cliRunner.js.map +1 -1
  4. package/dist/config.d.ts +4 -0
  5. package/dist/config.js +17 -1
  6. package/dist/config.js.map +1 -1
  7. package/dist/emitters/drizzle.d.ts +19 -0
  8. package/dist/emitters/drizzle.js +1337 -0
  9. package/dist/emitters/drizzle.js.map +1 -0
  10. package/dist/emitters/gorm.d.ts +38 -0
  11. package/dist/emitters/gorm.js +1507 -0
  12. package/dist/emitters/gorm.js.map +1 -0
  13. package/dist/emitters/jsonSchema.d.ts +20 -0
  14. package/dist/emitters/jsonSchema.js +503 -0
  15. package/dist/emitters/jsonSchema.js.map +1 -0
  16. package/dist/emitters/sqlDdl.d.ts +29 -0
  17. package/dist/emitters/sqlDdl.js +1741 -0
  18. package/dist/emitters/sqlDdl.js.map +1 -0
  19. package/dist/emitters/typeorm.js +60 -3
  20. package/dist/emitters/typeorm.js.map +1 -1
  21. package/dist/emitters/zod.d.ts +40 -0
  22. package/dist/emitters/zod.js +498 -0
  23. package/dist/emitters/zod.js.map +1 -0
  24. package/dist/formats.d.ts +6 -1
  25. package/dist/formats.js +88 -0
  26. package/dist/formats.js.map +1 -1
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/io.js +69 -6
  31. package/dist/io.js.map +1 -1
  32. package/dist/parsers/doctrine.js +203 -1
  33. package/dist/parsers/doctrine.js.map +1 -1
  34. package/dist/parsers/drizzle.d.ts +12 -0
  35. package/dist/parsers/drizzle.js +2040 -0
  36. package/dist/parsers/drizzle.js.map +1 -0
  37. package/dist/parsers/goSyntax.d.ts +105 -0
  38. package/dist/parsers/goSyntax.js +435 -0
  39. package/dist/parsers/goSyntax.js.map +1 -0
  40. package/dist/parsers/gorm.d.ts +16 -0
  41. package/dist/parsers/gorm.js +1857 -0
  42. package/dist/parsers/gorm.js.map +1 -0
  43. package/dist/parsers/jsonSchema.d.ts +21 -0
  44. package/dist/parsers/jsonSchema.js +1432 -0
  45. package/dist/parsers/jsonSchema.js.map +1 -0
  46. package/dist/parsers/sqlDdl.d.ts +18 -0
  47. package/dist/parsers/sqlDdl.js +2302 -0
  48. package/dist/parsers/sqlDdl.js.map +1 -0
  49. package/dist/parsers/sqlSyntax.d.ts +79 -0
  50. package/dist/parsers/sqlSyntax.js +698 -0
  51. package/dist/parsers/sqlSyntax.js.map +1 -0
  52. package/dist/parsers/sqlTypes.d.ts +53 -0
  53. package/dist/parsers/sqlTypes.js +742 -0
  54. package/dist/parsers/sqlTypes.js.map +1 -0
  55. package/dist/parsers/typeorm.js +22 -7
  56. package/dist/parsers/typeorm.js.map +1 -1
  57. package/dist/prismaOnlyConstructs.d.ts +2 -2
  58. package/dist/prismaOnlyConstructs.js +5 -5
  59. package/dist/prismaOnlyConstructs.js.map +1 -1
  60. package/package.json +2 -1
@@ -0,0 +1,1507 @@
1
+ import { findModel } from '../ir.js';
2
+ import { splitWords, toSnakeCase } from '../naming.js';
3
+ import { prismaOnlyWarnings } from '../prismaOnlyConstructs.js';
4
+ export const DEFAULT_GO_PACKAGE = 'models';
5
+ const GO_PACKAGE_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
+ const INDENT = '\t';
7
+ const GO_KEYWORDS = new Set(('break default func interface select case defer go map struct chan else ' +
8
+ 'goto package switch const fallthrough if range type continue for import ' +
9
+ 'return var').split(' '));
10
+ /** True when `value` can be used as a Go package name: an identifier that is not `_` or a keyword. */
11
+ export function isValidGoPackageName(value) {
12
+ return (GO_PACKAGE_PATTERN.test(value) && value !== '_' && !GO_KEYWORDS.has(value));
13
+ }
14
+ const ON_DELETE_NAMES = {
15
+ cascade: 'CASCADE',
16
+ setNull: 'SET NULL',
17
+ restrict: 'RESTRICT',
18
+ noAction: 'NO ACTION',
19
+ setDefault: 'SET DEFAULT',
20
+ };
21
+ /** Methods GORM looks for on a model; a field with the same name would not compile. */
22
+ const RESERVED_MEMBER_NAMES = ['TableName', 'BeforeCreate'];
23
+ /** The columns `gorm.Model` adds, as Go field names. */
24
+ const GORM_MODEL_MEMBERS = [
25
+ 'Model',
26
+ 'ID',
27
+ 'CreatedAt',
28
+ 'UpdatedAt',
29
+ 'DeletedAt',
30
+ ];
31
+ /** Operating systems and architectures whose names in `name_<suffix>.go` make Go skip the file elsewhere. */
32
+ const GO_FILE_SUFFIXES = new Set(('test aix android darwin dragonfly freebsd hurd illumos ios js linux ' +
33
+ 'nacl netbsd openbsd plan9 solaris wasip1 windows zos 386 amd64 arm arm64 ' +
34
+ 'loong64 mips mipsle mips64 mips64le ppc64 ppc64le riscv64 s390x sparc64 wasm').split(' '));
35
+ const IMPORT_PATHS = {
36
+ time: 'time',
37
+ uuid: 'github.com/google/uuid',
38
+ decimal: 'github.com/shopspring/decimal',
39
+ datatypes: 'gorm.io/datatypes',
40
+ gorm: 'gorm.io/gorm',
41
+ };
42
+ export function emitGorm(schema, options) {
43
+ const prepared = options.naming === 'normalize' ? normalizeGormSchema(schema) : schema;
44
+ const warnings = prismaOnlyWarnings(prepared);
45
+ let packageName = DEFAULT_GO_PACKAGE;
46
+ if (options.goPackage !== undefined) {
47
+ if (isValidGoPackageName(options.goPackage)) {
48
+ packageName = options.goPackage;
49
+ }
50
+ else {
51
+ warnings.push(`The Go package name "${options.goPackage}" is not valid; "${DEFAULT_GO_PACKAGE}" was used instead.`);
52
+ }
53
+ }
54
+ const context = {
55
+ schema: prepared,
56
+ options,
57
+ dialect: dialectOf(options.provider),
58
+ warnings,
59
+ packageName,
60
+ typeNames: new Set(),
61
+ modelTypes: new Map(),
62
+ enumTypes: new Map(),
63
+ plans: new Map(),
64
+ valueEdges: new Map(),
65
+ };
66
+ if (options.provider === 'mongodb') {
67
+ warnings.push('Provider "mongodb": GORM is relational (it has no MongoDB driver); relational models were written.');
68
+ }
69
+ allocateTypeNames(context);
70
+ for (const model of prepared.models) {
71
+ planStruct(context, model);
72
+ }
73
+ for (const model of prepared.models) {
74
+ planOwnedRelations(context, model);
75
+ }
76
+ for (const model of prepared.models) {
77
+ planInverseSides(context, model);
78
+ }
79
+ for (const model of prepared.models) {
80
+ planIndexes(context, model);
81
+ }
82
+ for (const plan of context.plans.values()) {
83
+ if (gormTableName(plan.typeName) !== plan.model.tableName) {
84
+ plan.tableName = plan.model.tableName;
85
+ }
86
+ }
87
+ removeHandledWarnings(context);
88
+ const files = {};
89
+ const fileStems = new Set();
90
+ for (const enumDefinition of prepared.enums) {
91
+ const typeName = context.enumTypes.get(enumDefinition.name) ?? '';
92
+ const stem = uniqueFileStem(typeName, fileStems);
93
+ files[`${packageName}/${stem}.go`] = renderEnum(context, enumDefinition, typeName);
94
+ }
95
+ for (const model of prepared.models) {
96
+ const plan = context.plans.get(model.name);
97
+ if (plan !== undefined) {
98
+ plan.fileStem = uniqueFileStem(plan.typeName, fileStems);
99
+ files[`${packageName}/${plan.fileStem}.go`] = renderStruct(context, plan);
100
+ }
101
+ }
102
+ return { files, warnings };
103
+ }
104
+ // ---------------------------------------------------------------------------
105
+ // GORM naming conventions
106
+ // ---------------------------------------------------------------------------
107
+ /** The initialisms GORM's default naming strategy keeps together (`ID` becomes `id`, not `i_d`). */
108
+ const GORM_INITIALISMS = [
109
+ 'API',
110
+ 'ASCII',
111
+ 'CPU',
112
+ 'CSS',
113
+ 'DNS',
114
+ 'EOF',
115
+ 'GUID',
116
+ 'HTML',
117
+ 'HTTP',
118
+ 'HTTPS',
119
+ 'ID',
120
+ 'IP',
121
+ 'JSON',
122
+ 'LHS',
123
+ 'QPS',
124
+ 'RAM',
125
+ 'RHS',
126
+ 'RPC',
127
+ 'SLA',
128
+ 'SMTP',
129
+ 'SSH',
130
+ 'TLS',
131
+ 'TTL',
132
+ 'UID',
133
+ 'UI',
134
+ 'UUID',
135
+ 'URI',
136
+ 'URL',
137
+ 'UTF8',
138
+ 'VM',
139
+ 'XML',
140
+ 'XSRF',
141
+ 'XSS',
142
+ ];
143
+ /** Initialisms written in capitals in Go names (GORM's list plus a few the Go linters add). */
144
+ const GO_INITIALISMS = new Set([
145
+ ...GORM_INITIALISMS,
146
+ 'ACL',
147
+ 'SQL',
148
+ 'TCP',
149
+ 'UDP',
150
+ 'XMPP',
151
+ ]);
152
+ function isAsciiUpper(char) {
153
+ return char !== undefined && char >= 'A' && char <= 'Z';
154
+ }
155
+ function isAsciiDigit(char) {
156
+ return char !== undefined && char >= '0' && char <= '9';
157
+ }
158
+ /** Go's `strings.Title` for an initialism that is all capitals: first letter up, the rest down. */
159
+ function titleCase(word) {
160
+ return `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`;
161
+ }
162
+ /**
163
+ * Replaces the common initialisms the way GORM's `strings.NewReplacer` does:
164
+ * left to right, the first initialism in list order that matches wins.
165
+ */
166
+ function replaceInitialisms(name) {
167
+ let result = '';
168
+ let position = 0;
169
+ while (position < name.length) {
170
+ const initialism = GORM_INITIALISMS.find((candidate) => name.startsWith(candidate, position));
171
+ if (initialism === undefined) {
172
+ result += name.charAt(position);
173
+ position += 1;
174
+ }
175
+ else {
176
+ result += titleCase(initialism);
177
+ position += initialism.length;
178
+ }
179
+ }
180
+ return result;
181
+ }
182
+ /**
183
+ * The column name GORM derives from a Go field name (`schema.NamingStrategy.ColumnName`,
184
+ * gorm v1.31): `PublicID` becomes `public_id`, `HTTPServer` becomes `http_server`.
185
+ */
186
+ export function gormColumnName(goName) {
187
+ if (goName === '') {
188
+ return '';
189
+ }
190
+ const value = replaceInitialisms(goName);
191
+ let result = '';
192
+ let lastCase = false;
193
+ let currentCase = isAsciiUpper(value.charAt(0));
194
+ for (let index = 0; index < value.length - 1; index += 1) {
195
+ const char = value.charAt(index);
196
+ const next = value.charAt(index + 1);
197
+ const nextCase = isAsciiUpper(next);
198
+ const nextNumber = isAsciiDigit(next);
199
+ if (currentCase) {
200
+ if (lastCase && (nextCase || nextNumber)) {
201
+ result += char.toLowerCase();
202
+ }
203
+ else {
204
+ if (index > 0 && value.charAt(index - 1) !== '_' && next !== '_') {
205
+ result += '_';
206
+ }
207
+ result += char.toLowerCase();
208
+ }
209
+ }
210
+ else {
211
+ result += char;
212
+ }
213
+ lastCase = currentCase;
214
+ currentCase = nextCase;
215
+ }
216
+ const last = value.charAt(value.length - 1);
217
+ if (currentCase) {
218
+ if (!lastCase && value.length > 1) {
219
+ result += '_';
220
+ }
221
+ result += last.toLowerCase();
222
+ }
223
+ else {
224
+ result += last;
225
+ }
226
+ return result;
227
+ }
228
+ const UNCOUNTABLE_WORDS = [
229
+ 'equipment',
230
+ 'information',
231
+ 'rice',
232
+ 'money',
233
+ 'species',
234
+ 'series',
235
+ 'fish',
236
+ 'sheep',
237
+ 'jeans',
238
+ 'police',
239
+ ];
240
+ const IRREGULAR_PLURALS = [
241
+ ['person', 'people'],
242
+ ['man', 'men'],
243
+ ['child', 'children'],
244
+ ['sex', 'sexes'],
245
+ ['move', 'moves'],
246
+ ['mombie', 'mombies'],
247
+ ];
248
+ /** The regular rules of github.com/jinzhu/inflection; the last one that matches wins. */
249
+ const REGULAR_PLURALS = [
250
+ ['([a-z])$', '$1s'],
251
+ ['s$', 's'],
252
+ ['^(ax|test)is$', '$1es'],
253
+ ['(octop|vir)us$', '$1i'],
254
+ ['(octop|vir)i$', '$1i'],
255
+ ['(alias|status)$', '$1es'],
256
+ ['(bu)s$', '$1ses'],
257
+ ['(buffal|tomat)o$', '$1oes'],
258
+ ['([ti])um$', '$1a'],
259
+ ['([ti])a$', '$1a'],
260
+ ['sis$', 'ses'],
261
+ ['(?:([^f])fe|([lr])f)$', '$1$2ves'],
262
+ ['(hive)$', '$1s'],
263
+ ['([^aeiouy]|qu)y$', '$1ies'],
264
+ ['(x|ch|ss|sh)$', '$1es'],
265
+ ['(matr|vert|ind)(?:ix|ex)$', '$1ices'],
266
+ ['^(m|l)ouse$', '$1ice'],
267
+ ['^(m|l)ice$', '$1ice'],
268
+ ['^(ox)$', '$1en'],
269
+ ['^(oxen)$', '$1'],
270
+ ['(quiz)$', '$1zes'],
271
+ ];
272
+ /** The rule list in the order github.com/jinzhu/inflection tries it. */
273
+ function buildPluralRules() {
274
+ const rules = [];
275
+ for (const word of UNCOUNTABLE_WORDS) {
276
+ rules.push([new RegExp(`^(${word})$`, 'i'), '$1']);
277
+ }
278
+ for (const [singular, plural] of IRREGULAR_PLURALS) {
279
+ rules.push([new RegExp(`${singular.toUpperCase()}$`), plural.toUpperCase()], [new RegExp(`${titleCase(singular)}$`), titleCase(plural)], [new RegExp(`${singular}$`), plural]);
280
+ }
281
+ for (let index = REGULAR_PLURALS.length - 1; index >= 0; index -= 1) {
282
+ const [find, replacement] = REGULAR_PLURALS[index] ?? ['', ''];
283
+ rules.push([new RegExp(find.toUpperCase()), replacement.toUpperCase()], [new RegExp(find), replacement], [new RegExp(find, 'i'), replacement]);
284
+ }
285
+ return rules;
286
+ }
287
+ const PLURAL_RULES = buildPluralRules();
288
+ /** Pluralizes a lower-case snake_case name the way GORM does for table names. */
289
+ export function gormPluralize(snake) {
290
+ for (const [pattern, replacement] of PLURAL_RULES) {
291
+ if (pattern.test(snake)) {
292
+ return snake.replace(pattern, replacement);
293
+ }
294
+ }
295
+ return snake;
296
+ }
297
+ /** The table name GORM derives from a Go struct name: `BlogPost` becomes `blog_posts`. */
298
+ export function gormTableName(goTypeName) {
299
+ return gormPluralize(gormColumnName(goTypeName));
300
+ }
301
+ /**
302
+ * Applies GORM's conventions to a schema: plural snake_case table names (join
303
+ * tables keep theirs), snake_case columns, and `created_at` / `updated_at`
304
+ * where a model has none (GORM fills both in by name). Primary keys keep their
305
+ * type.
306
+ */
307
+ export function normalizeGormSchema(schema) {
308
+ return {
309
+ ...schema,
310
+ models: schema.models.map((model) => normalizeGormModel(model)),
311
+ };
312
+ }
313
+ const CREATED_FIELD_NAMES = new Set([
314
+ 'created_at',
315
+ 'created',
316
+ 'createdat',
317
+ 'date_created',
318
+ ]);
319
+ const UPDATED_FIELD_NAMES = new Set([
320
+ 'updated_at',
321
+ 'updated',
322
+ 'updatedat',
323
+ 'modified',
324
+ 'modified_at',
325
+ 'date_updated',
326
+ ]);
327
+ function normalizeGormModel(model) {
328
+ const fields = model.fields.map((field) => ({
329
+ ...field,
330
+ columnName: toSnakeCase(field.columnName),
331
+ }));
332
+ if (model.isJoinTable !== true) {
333
+ const hasCreated = fields.some((field) => CREATED_FIELD_NAMES.has(field.name.toLowerCase()));
334
+ const hasUpdated = fields.some((field) => field.isAutoUpdated || UPDATED_FIELD_NAMES.has(field.name.toLowerCase()));
335
+ if (!hasCreated) {
336
+ fields.push({
337
+ name: 'created_at',
338
+ columnName: 'created_at',
339
+ type: 'dateTime',
340
+ isPrimaryKey: false,
341
+ isUnique: false,
342
+ isNullable: false,
343
+ isAutoUpdated: false,
344
+ default: { kind: 'now' },
345
+ });
346
+ }
347
+ if (!hasUpdated) {
348
+ fields.push({
349
+ name: 'updated_at',
350
+ columnName: 'updated_at',
351
+ type: 'dateTime',
352
+ isPrimaryKey: false,
353
+ isUnique: false,
354
+ isNullable: false,
355
+ isAutoUpdated: true,
356
+ });
357
+ }
358
+ }
359
+ return {
360
+ ...model,
361
+ tableName: model.isJoinTable === true
362
+ ? model.tableName
363
+ : gormTableName(goName(model.name)),
364
+ fields,
365
+ relations: model.relations.map((relation) => ({
366
+ ...relation,
367
+ columnName: relation.columnName === '' ? '' : toSnakeCase(relation.columnName),
368
+ })),
369
+ };
370
+ }
371
+ // ---------------------------------------------------------------------------
372
+ // Small helpers
373
+ // ---------------------------------------------------------------------------
374
+ function dialectOf(provider) {
375
+ switch (provider) {
376
+ case 'mysql':
377
+ return 'mysql';
378
+ case 'sqlite':
379
+ return 'sqlite';
380
+ case 'sqlserver':
381
+ return 'sqlserver';
382
+ default:
383
+ return 'postgres';
384
+ }
385
+ }
386
+ /** A name written the way Go programmers write it: `public_id` becomes `PublicID`. */
387
+ function goName(raw) {
388
+ const words = splitWords(raw).map((word) => {
389
+ const upper = word.toUpperCase();
390
+ if (GO_INITIALISMS.has(upper)) {
391
+ return upper;
392
+ }
393
+ if (word === upper && word.length > 1 && /[A-Z]/.test(word)) {
394
+ return titleCase(word);
395
+ }
396
+ return `${word.charAt(0).toUpperCase()}${word.slice(1)}`;
397
+ });
398
+ return words.join('');
399
+ }
400
+ /** An exported Go identifier for `raw`, warning when characters had to be dropped or added. */
401
+ function exportedName(context, owner, raw, fallback) {
402
+ let name = goName(raw);
403
+ let changed = /[^A-Za-z0-9_]/.test(raw);
404
+ if (name === '') {
405
+ name = fallback;
406
+ changed = true;
407
+ }
408
+ else if (/^[0-9]/.test(name)) {
409
+ name = `X${name}`;
410
+ changed = true;
411
+ }
412
+ if (changed) {
413
+ context.warnings.push(`${owner}: "${raw}" is not a usable Go identifier; it was written as "${name}".`);
414
+ }
415
+ return name;
416
+ }
417
+ function uniqueName(baseName, used) {
418
+ let candidate = baseName;
419
+ let suffix = 2;
420
+ while (used.has(candidate)) {
421
+ candidate = `${baseName}${suffix}`;
422
+ suffix += 1;
423
+ }
424
+ used.add(candidate);
425
+ return candidate;
426
+ }
427
+ /** A file name stem that is unique ignoring case and that Go does not treat as a test or platform file. */
428
+ function uniqueFileStem(typeName, used) {
429
+ let stem = toSnakeCase(typeName);
430
+ if (stem === '') {
431
+ stem = 'model';
432
+ }
433
+ const lastWord = stem.slice(stem.lastIndexOf('_') + 1);
434
+ if (stem.includes('_') && GO_FILE_SUFFIXES.has(lastWord)) {
435
+ stem = `${stem}_model`;
436
+ }
437
+ let candidate = stem;
438
+ let suffix = 2;
439
+ while (used.has(candidate.toLowerCase())) {
440
+ candidate = `${stem}_${suffix}`;
441
+ suffix += 1;
442
+ }
443
+ used.add(candidate.toLowerCase());
444
+ return candidate;
445
+ }
446
+ /** Escapes a value for a gorm tag: `;` separates entries, so a literal one is written `\;`. */
447
+ function tagValue(value) {
448
+ return value.replace(/;/g, '\\;');
449
+ }
450
+ /** A SQL string literal, for defaults GORM passes to the database as written. */
451
+ function sqlString(value) {
452
+ return `'${value.replace(/'/g, "''")}'`;
453
+ }
454
+ /** The struct tag literal for the entries of a `gorm:"..."` tag. */
455
+ function tagLiteral(entries) {
456
+ const body = entries
457
+ .join(';')
458
+ .replace(/\\/g, '\\\\')
459
+ .replace(/"/g, '\\"');
460
+ const tag = `gorm:"${body}"`;
461
+ // eslint-disable-next-line no-control-regex
462
+ if (!tag.includes('`') && !/[\u0000-\u001f\u007f]/.test(tag)) {
463
+ return `\`${tag}\``;
464
+ }
465
+ return JSON.stringify(tag);
466
+ }
467
+ /** A Go string literal. */
468
+ function goString(value) {
469
+ return JSON.stringify(value);
470
+ }
471
+ /**
472
+ * Lays out rows the way gofmt's tabwriter does: within a run of consecutive
473
+ * rows that all have a cell in a column, that column is as wide as its widest
474
+ * cell plus one space. A row's last cell is never padded.
475
+ */
476
+ function alignRows(rows) {
477
+ const widths = rows.map((row) => row.map(() => undefined));
478
+ const columnCount = Math.max(0, ...rows.map((row) => row.length));
479
+ for (let column = 0; column < columnCount - 1; column += 1) {
480
+ let start = 0;
481
+ while (start < rows.length) {
482
+ if ((rows[start]?.length ?? 0) <= column + 1) {
483
+ start += 1;
484
+ continue;
485
+ }
486
+ let end = start;
487
+ let width = 0;
488
+ while (end < rows.length && (rows[end]?.length ?? 0) > column + 1) {
489
+ width = Math.max(width, [...(rows[end]?.[column] ?? '')].length);
490
+ end += 1;
491
+ }
492
+ for (let row = start; row < end; row += 1) {
493
+ const rowWidths = widths[row];
494
+ if (rowWidths !== undefined) {
495
+ rowWidths[column] = width + 1;
496
+ }
497
+ }
498
+ start = end;
499
+ }
500
+ }
501
+ return rows.map((row, rowIndex) => {
502
+ return row
503
+ .map((cell, column) => {
504
+ const width = widths[rowIndex]?.[column];
505
+ return width === undefined
506
+ ? cell
507
+ : cell + ' '.repeat(width - [...cell].length);
508
+ })
509
+ .join('');
510
+ });
511
+ }
512
+ // ---------------------------------------------------------------------------
513
+ // Names
514
+ // ---------------------------------------------------------------------------
515
+ function allocateTypeNames(context) {
516
+ for (const model of context.schema.models) {
517
+ const name = exportedName(context, model.name, model.name, 'Model');
518
+ context.modelTypes.set(model.name, uniqueName(name, context.typeNames));
519
+ }
520
+ for (const enumDefinition of context.schema.enums) {
521
+ const name = exportedName(context, `enum ${enumDefinition.name}`, enumDefinition.name, 'Enum');
522
+ context.enumTypes.set(enumDefinition.name, uniqueName(name, context.typeNames));
523
+ }
524
+ }
525
+ /** Constant names of an enum, claimed from the package-level names so they never clash with a type. */
526
+ function enumConstantNames(context, enumDefinition, typeName) {
527
+ return enumDefinition.values.map((value) => {
528
+ const suffix = goName(value.name);
529
+ return uniqueName(`${typeName}${suffix === '' ? 'Value' : suffix}`, context.typeNames);
530
+ });
531
+ }
532
+ function findEnum(context, name) {
533
+ return context.schema.enums.find((candidate) => candidate.name === name);
534
+ }
535
+ // ---------------------------------------------------------------------------
536
+ // Column types
537
+ // ---------------------------------------------------------------------------
538
+ function spec(goType, kind, typeEntries = [], imports = [], extra = {}) {
539
+ return {
540
+ goType,
541
+ typeEntries,
542
+ kind,
543
+ imports,
544
+ nilable: false,
545
+ temporal: false,
546
+ ...extra,
547
+ };
548
+ }
549
+ /** The `type:` entry for a UUID column; SQLite and anything unknown keep GORM's text column. */
550
+ function uuidTypeEntries(dialect) {
551
+ switch (dialect) {
552
+ case 'postgres':
553
+ return ['type:uuid'];
554
+ case 'mysql':
555
+ return ['type:char(36)'];
556
+ case 'sqlserver':
557
+ return ['type:uniqueidentifier'];
558
+ default:
559
+ return [];
560
+ }
561
+ }
562
+ function textTypeEntries(dialect) {
563
+ switch (dialect) {
564
+ case 'mysql':
565
+ return ['type:longtext'];
566
+ case 'sqlserver':
567
+ return ['type:nvarchar(max)'];
568
+ default:
569
+ return ['type:text'];
570
+ }
571
+ }
572
+ /**
573
+ * Works out the Go type and the type entries of a column. `report` is false
574
+ * when the same field is looked at again (for example as a referenced key), so
575
+ * its warnings are not repeated.
576
+ */
577
+ function scalarSpec(context, label, field, report) {
578
+ const warn = (message) => {
579
+ if (report) {
580
+ context.warnings.push(`${label}: ${message}`);
581
+ }
582
+ };
583
+ const base = baseSpec(context, field, warn);
584
+ const depth = field.arrayDepth ?? 0;
585
+ if (depth === 0) {
586
+ return base;
587
+ }
588
+ warn('GORM has no portable array column type; the array was written as a JSON column (datatypes.JSONSlice).');
589
+ return spec(`datatypes.JSONSlice[${'[]'.repeat(depth - 1)}${base.goType}]`, 'json', [], ['datatypes', ...base.imports], { nilable: true });
590
+ }
591
+ function baseSpec(context, field, warn) {
592
+ const dialect = context.dialect;
593
+ const sizeEntry = field.maxLength === undefined ? [] : [`size:${field.maxLength}`];
594
+ if (field.enumName !== undefined) {
595
+ const enumDefinition = findEnum(context, field.enumName);
596
+ if (enumDefinition === undefined) {
597
+ warn(`enum "${field.enumName}" does not exist in the schema; the column was written as a plain string.`);
598
+ }
599
+ else {
600
+ return spec(context.enumTypes.get(enumDefinition.name) ?? field.enumName, 'enum', sizeEntry, [], { enumDefinition });
601
+ }
602
+ }
603
+ switch (field.type) {
604
+ case 'string':
605
+ return spec('string', 'string', sizeEntry);
606
+ case 'text':
607
+ return spec('string', 'string', textTypeEntries(dialect));
608
+ case 'uuid':
609
+ return spec('uuid.UUID', 'uuid', uuidTypeEntries(dialect), ['uuid']);
610
+ case 'int':
611
+ return spec('int32', 'int');
612
+ case 'bigInt':
613
+ return spec('int64', 'int');
614
+ case 'float':
615
+ return spec('float64', 'float', dialect === 'postgres' ? ['type:double precision'] : []);
616
+ case 'decimal': {
617
+ if (field.maxDigits !== undefined && field.decimalPlaces !== undefined) {
618
+ return spec('decimal.Decimal', 'decimal', [`type:decimal(${field.maxDigits},${field.decimalPlaces})`], ['decimal']);
619
+ }
620
+ if (dialect === 'mysql' || dialect === 'sqlserver') {
621
+ warn('a decimal column without precision and scale gets the database default (no decimal places on this provider).');
622
+ }
623
+ return spec('decimal.Decimal', 'decimal', ['type:decimal'], ['decimal']);
624
+ }
625
+ case 'boolean':
626
+ return spec('bool', 'bool');
627
+ case 'dateTime':
628
+ return spec('time.Time', 'time', [], ['time'], { temporal: true });
629
+ case 'date':
630
+ return spec('time.Time', 'time', ['type:date'], ['time'], {
631
+ temporal: true,
632
+ });
633
+ case 'time':
634
+ return spec('time.Time', 'time', ['type:time(6)'], ['time']);
635
+ case 'json':
636
+ return spec('datatypes.JSON', 'json', [], ['datatypes'], {
637
+ nilable: true,
638
+ });
639
+ case 'bytes':
640
+ return spec('[]byte', 'bytes', [], [], { nilable: true });
641
+ case 'duration':
642
+ warn('GORM has no interval column; the duration was written as a time.Duration, stored as nanoseconds in a bigint column.');
643
+ return spec('time.Duration', 'duration', [], ['time']);
644
+ case 'ipAddress':
645
+ return spec('string', 'string', ['size:45']);
646
+ case 'hstore':
647
+ warn('GORM has no hstore type; the field was written as a JSON column (datatypes.JSON).');
648
+ return spec('datatypes.JSON', 'json', [], ['datatypes'], {
649
+ nilable: true,
650
+ });
651
+ case 'range':
652
+ warn('GORM has no range column type; the field was written as a string column holding the range text.');
653
+ return spec('string', 'string');
654
+ case 'unsupported':
655
+ // Without a type name the shared Prisma-only warning already says everything.
656
+ if (field.unsupportedType !== undefined) {
657
+ warn(`the database type "${field.unsupportedType}" has no Go type here; the column was written as a string with type:${field.unsupportedType}.`);
658
+ }
659
+ return spec('string', 'string', field.unsupportedType === undefined
660
+ ? []
661
+ : [`type:${tagValue(field.unsupportedType)}`]);
662
+ default:
663
+ warn(`unknown field type "${String(field.type)}"; it was written as a string column.`);
664
+ return spec('string', 'string');
665
+ }
666
+ }
667
+ /** The Go type of a column: a pointer when the column is nullable, unless the type holds nil itself. */
668
+ function memberType(columnSpec, nullable) {
669
+ return nullable && !columnSpec.nilable
670
+ ? `*${columnSpec.goType}`
671
+ : columnSpec.goType;
672
+ }
673
+ function enumValueFor(columnSpec, raw) {
674
+ const values = columnSpec.enumDefinition?.values ?? [];
675
+ return (values.find((candidate) => candidate.name === raw) ??
676
+ values.find((candidate) => candidate.dbValue === raw));
677
+ }
678
+ /** Works out the tag entries (and generated-UUID hook) that implement a field's default. */
679
+ function defaultOf(context, label, field, columnSpec) {
680
+ const defaultValue = field.default;
681
+ if (defaultValue === undefined) {
682
+ return { entries: [] };
683
+ }
684
+ switch (defaultValue.kind) {
685
+ case 'autoIncrement':
686
+ return { entries: [] };
687
+ case 'now':
688
+ if (columnSpec.temporal) {
689
+ return {
690
+ entries: [
691
+ 'autoCreateTime',
692
+ ...(field.isDbDefault === true
693
+ ? ['default:CURRENT_TIMESTAMP']
694
+ : []),
695
+ ],
696
+ };
697
+ }
698
+ if (columnSpec.kind === 'string' || columnSpec.kind === 'time') {
699
+ return { entries: ['default:CURRENT_TIMESTAMP'] };
700
+ }
701
+ context.warnings.push(`${label}: a "now" default needs a date or time column; it was dropped.`);
702
+ return { entries: [] };
703
+ case 'uuid': {
704
+ if (columnSpec.kind !== 'uuid' && columnSpec.kind !== 'string') {
705
+ context.warnings.push(`${label}: a UUID default needs a uuid or string column; it was dropped.`);
706
+ return { entries: [] };
707
+ }
708
+ const version = defaultValue.version;
709
+ if (version !== undefined && version !== 4 && version !== 7) {
710
+ context.warnings.push(`${label}: a version ${version} UUID default was written as a random (version 4) UUID.`);
711
+ }
712
+ const style = columnSpec.kind === 'string'
713
+ ? 'string'
714
+ : field.isNullable
715
+ ? 'pointer'
716
+ : 'value';
717
+ if (context.dialect === 'postgres' && version !== 7) {
718
+ return { entries: ['default:gen_random_uuid()'] };
719
+ }
720
+ return {
721
+ entries: [],
722
+ hook: { style, version7: version === 7 },
723
+ };
724
+ }
725
+ case 'enumValue': {
726
+ const found = enumValueFor(columnSpec, defaultValue.value);
727
+ if (found === undefined) {
728
+ context.warnings.push(`${label}: the enum default "${defaultValue.value}" does not match a value of the enum; it was dropped.`);
729
+ return { entries: [] };
730
+ }
731
+ return { entries: [`default:${tagValue(found.dbValue)}`] };
732
+ }
733
+ case 'literal':
734
+ return literalDefault(context, label, field, columnSpec, defaultValue.value);
735
+ case 'dbExpression':
736
+ return defaultValue.isFunction === true
737
+ ? { entries: [] }
738
+ : { entries: [`default:${tagValue(defaultValue.expression)}`] };
739
+ default:
740
+ // clientGenerated defaults are reported by prismaOnlyWarnings.
741
+ return { entries: [] };
742
+ }
743
+ }
744
+ function literalDefault(context, label, field, columnSpec, value) {
745
+ const text = String(value);
746
+ switch (columnSpec.kind) {
747
+ case 'enum': {
748
+ const found = enumValueFor(columnSpec, text);
749
+ if (found === undefined) {
750
+ context.warnings.push(`${label}: the enum default "${text}" does not match a value of the enum; it was dropped.`);
751
+ return { entries: [] };
752
+ }
753
+ return { entries: [`default:${tagValue(found.dbValue)}`] };
754
+ }
755
+ case 'string':
756
+ return { entries: [`default:${tagValue(text)}`] };
757
+ case 'bool':
758
+ return {
759
+ entries: [`default:${value === true || value === 'true'}`],
760
+ };
761
+ case 'int':
762
+ return { entries: [`default:${Math.trunc(Number(value))}`] };
763
+ case 'float':
764
+ return { entries: [`default:${Number(value)}`] };
765
+ case 'decimal':
766
+ return {
767
+ entries: [
768
+ `default:${Number.isNaN(Number(text)) || text.trim() === ''
769
+ ? tagValue(sqlString(text))
770
+ : tagValue(text)}`,
771
+ ],
772
+ };
773
+ case 'json':
774
+ case 'uuid':
775
+ case 'time':
776
+ return { entries: [`default:${tagValue(sqlString(text))}`] };
777
+ default:
778
+ context.warnings.push(`${label}: a literal default on a ${field.type} column cannot be represented; it was dropped.`);
779
+ return { entries: [] };
780
+ }
781
+ }
782
+ function isPlainTimestamp(field) {
783
+ return (field.type === 'dateTime' &&
784
+ !field.isUnique &&
785
+ field.enumName === undefined &&
786
+ (field.arrayDepth ?? 0) === 0 &&
787
+ field.generated === undefined);
788
+ }
789
+ /**
790
+ * Finds the four columns of `gorm.Model` when the model has exactly those:
791
+ * a big-integer auto-increment `id` key, date-time `created_at` and
792
+ * `updated_at` (the latter refreshed on update), and a nullable, indexed
793
+ * `deleted_at`.
794
+ */
795
+ function findGormModelFields(model) {
796
+ if ((model.compositePrimaryKey?.length ?? 0) > 0 ||
797
+ model.relations.some((relation) => relation.isPrimaryKey)) {
798
+ return undefined;
799
+ }
800
+ const byColumn = (column) => model.fields.find((field) => field.columnName === column);
801
+ const id = byColumn('id');
802
+ const createdAt = byColumn('created_at');
803
+ const updatedAt = byColumn('updated_at');
804
+ const deletedAt = byColumn('deleted_at');
805
+ if (id === undefined ||
806
+ createdAt === undefined ||
807
+ updatedAt === undefined ||
808
+ deletedAt === undefined) {
809
+ return undefined;
810
+ }
811
+ const onlyKey = model.fields.filter((field) => field.isPrimaryKey).length === 1;
812
+ const idMatches = onlyKey &&
813
+ id.isPrimaryKey &&
814
+ id.type === 'bigInt' &&
815
+ id.default?.kind === 'autoIncrement' &&
816
+ !id.isNullable &&
817
+ id.enumName === undefined &&
818
+ (id.arrayDepth ?? 0) === 0;
819
+ const createdMatches = isPlainTimestamp(createdAt) &&
820
+ !createdAt.isPrimaryKey &&
821
+ (createdAt.default === undefined || createdAt.default.kind === 'now');
822
+ const updatedMatches = isPlainTimestamp(updatedAt) &&
823
+ !updatedAt.isPrimaryKey &&
824
+ updatedAt.isAutoUpdated &&
825
+ updatedAt.default === undefined;
826
+ const deletedMatches = isPlainTimestamp(deletedAt) &&
827
+ !deletedAt.isPrimaryKey &&
828
+ deletedAt.isNullable &&
829
+ deletedAt.default === undefined;
830
+ const deletedIndexed = model.indexes.some((index) => index.fields.length === 1 &&
831
+ index.fields[0] === deletedAt.name &&
832
+ !index.isUnique &&
833
+ index.name === undefined &&
834
+ index.kind === undefined &&
835
+ index.method === undefined);
836
+ if (idMatches &&
837
+ createdMatches &&
838
+ updatedMatches &&
839
+ deletedMatches &&
840
+ deletedIndexed) {
841
+ return { id, createdAt, updatedAt, deletedAt };
842
+ }
843
+ return undefined;
844
+ }
845
+ /** True for a nullable `deleted_at` timestamp, which GORM calls soft delete. */
846
+ function isSoftDeleteField(field) {
847
+ return (isPlainTimestamp(field) &&
848
+ field.isNullable &&
849
+ field.default === undefined &&
850
+ toSnakeCase(field.columnName) === 'deleted_at');
851
+ }
852
+ function planStruct(context, model) {
853
+ const typeName = context.modelTypes.get(model.name) ?? model.name;
854
+ const plan = {
855
+ model,
856
+ typeName,
857
+ fileStem: '',
858
+ embedsModel: false,
859
+ members: [],
860
+ names: new Set(RESERVED_MEMBER_NAMES),
861
+ imports: new Set(),
862
+ fieldNames: new Map(),
863
+ byKey: new Map(),
864
+ owned: new Map(),
865
+ manyToMany: new Set(),
866
+ uuidHooks: [],
867
+ };
868
+ context.plans.set(model.name, plan);
869
+ const embedded = findGormModelFields(model);
870
+ const embeddedFields = new Set();
871
+ if (embedded !== undefined) {
872
+ plan.embedsModel = true;
873
+ plan.imports.add('gorm');
874
+ for (const name of GORM_MODEL_MEMBERS) {
875
+ plan.names.add(name);
876
+ }
877
+ plan.members.push({
878
+ name: '',
879
+ type: 'gorm.Model',
880
+ entries: [],
881
+ role: 'column',
882
+ });
883
+ const pairs = [
884
+ [embedded.id, 'ID'],
885
+ [embedded.createdAt, 'CreatedAt'],
886
+ [embedded.updatedAt, 'UpdatedAt'],
887
+ [embedded.deletedAt, 'DeletedAt'],
888
+ ];
889
+ for (const [field, name] of pairs) {
890
+ embeddedFields.add(field);
891
+ plan.fieldNames.set(field.name, name);
892
+ }
893
+ }
894
+ const hasPrimaryKey = model.fields.some((field) => field.isPrimaryKey) ||
895
+ model.relations.some((relation) => relation.isPrimaryKey) ||
896
+ (model.compositePrimaryKey?.length ?? 0) > 0;
897
+ if (!hasPrimaryKey) {
898
+ context.warnings.push(`${model.name}: the model has no primary key; GORM can still read and create rows, but updates and deletes by key need one.`);
899
+ }
900
+ for (const field of model.fields) {
901
+ if (!embeddedFields.has(field)) {
902
+ planField(context, plan, field);
903
+ }
904
+ }
905
+ }
906
+ function planField(context, plan, field) {
907
+ const model = plan.model;
908
+ const label = `${model.name}.${field.name}`;
909
+ const name = uniqueName(exportedName(context, label, field.name, 'Field'), plan.names);
910
+ plan.fieldNames.set(field.name, name);
911
+ const columnSpec = scalarSpec(context, label, field, true);
912
+ if (field.generated !== undefined) {
913
+ context.warnings.push(`${label}: the generated column expression ${field.generated.expression} is not SQL GORM can run; the field was written as a regular column.`);
914
+ }
915
+ const inComposite = model.compositePrimaryKey?.includes(field.name) ?? false;
916
+ const isKey = field.isPrimaryKey || inComposite;
917
+ const entries = [];
918
+ if (gormColumnName(name) !== field.columnName) {
919
+ entries.push(`column:${tagValue(field.columnName)}`);
920
+ }
921
+ const softDelete = !isKey && isSoftDeleteField(field);
922
+ let type = memberType(columnSpec, field.isNullable);
923
+ if (!softDelete) {
924
+ for (const imported of columnSpec.imports) {
925
+ plan.imports.add(imported);
926
+ }
927
+ }
928
+ if (softDelete) {
929
+ type = 'gorm.DeletedAt';
930
+ plan.imports.add('gorm');
931
+ context.warnings.push(`${label}: a nullable deleted_at timestamp was written as gorm.DeletedAt, so GORM treats ${model.name} as soft-deleted: queries skip rows where it is set and Delete only sets it.`);
932
+ }
933
+ else {
934
+ entries.push(...columnSpec.typeEntries);
935
+ }
936
+ const incrementing = field.default?.kind === 'autoIncrement' && columnSpec.kind === 'int';
937
+ if (field.default?.kind === 'autoIncrement' && !incrementing) {
938
+ context.warnings.push(`${label}: an auto-increment default is only supported on integer columns; it was dropped.`);
939
+ }
940
+ if (isKey) {
941
+ entries.push('primaryKey');
942
+ }
943
+ if (incrementing) {
944
+ entries.push('autoIncrement');
945
+ }
946
+ else if (isKey &&
947
+ !inComposite &&
948
+ columnSpec.kind === 'int' &&
949
+ model.fields.filter((candidate) => candidate.isPrimaryKey)
950
+ .length === 1) {
951
+ // GORM makes a lone integer key auto-increment unless told otherwise.
952
+ entries.push('autoIncrement:false');
953
+ }
954
+ if (!isKey && !field.isNullable && !softDelete) {
955
+ entries.push('not null');
956
+ }
957
+ if (!isKey && field.isUnique) {
958
+ entries.push(field.uniqueName === undefined
959
+ ? 'unique'
960
+ : `uniqueIndex:${tagValue(field.uniqueName)}`);
961
+ }
962
+ if (!softDelete) {
963
+ const defaults = defaultOf(context, label, field, columnSpec);
964
+ entries.push(...defaults.entries);
965
+ if (defaults.hook !== undefined) {
966
+ plan.uuidHooks.push({ member: name, ...defaults.hook });
967
+ plan.imports.add('uuid');
968
+ plan.imports.add('gorm');
969
+ }
970
+ }
971
+ if (field.isAutoUpdated) {
972
+ if (columnSpec.temporal) {
973
+ entries.push('autoUpdateTime');
974
+ }
975
+ else {
976
+ context.warnings.push(`${label}: an auto-updated field must be a date or time column; it was written as a plain column.`);
977
+ }
978
+ }
979
+ const member = { name, type, entries, role: 'column' };
980
+ plan.members.push(member);
981
+ plan.byKey.set(`f:${field.name}`, member);
982
+ }
983
+ /** The single-column key a relation points at: an explicit `toField`, otherwise the primary key. */
984
+ function referencedKey(context, target, toField, depth = 0) {
985
+ const targetPlan = context.plans.get(target.name);
986
+ if (depth > 5 || targetPlan === undefined) {
987
+ return undefined;
988
+ }
989
+ const label = `${target.name}`;
990
+ const explicit = toField === undefined
991
+ ? undefined
992
+ : target.fields.find((field) => field.name === toField);
993
+ if (explicit !== undefined) {
994
+ return {
995
+ goName: targetPlan.fieldNames.get(explicit.name) ?? explicit.name,
996
+ spec: scalarSpec(context, label, explicit, false),
997
+ isPrimaryKey: explicit.isPrimaryKey,
998
+ };
999
+ }
1000
+ const keys = target.fields.filter((field) => field.isPrimaryKey);
1001
+ const compositeLength = target.compositePrimaryKey?.length ?? 0;
1002
+ const keyedRelations = target.relations.filter((relation) => relation.isPrimaryKey === true);
1003
+ if (keys.length === 1 &&
1004
+ compositeLength === 0 &&
1005
+ keyedRelations.length === 0) {
1006
+ const key = keys[0];
1007
+ return {
1008
+ goName: targetPlan.fieldNames.get(key.name) ?? key.name,
1009
+ spec: scalarSpec(context, label, key, false),
1010
+ isPrimaryKey: true,
1011
+ };
1012
+ }
1013
+ if (keys.length === 0 &&
1014
+ compositeLength === 0 &&
1015
+ keyedRelations.length === 1) {
1016
+ // Multi-table inheritance: the key is itself a foreign key to the parent.
1017
+ const relation = keyedRelations[0];
1018
+ const parent = findModel(context.schema, relation.targetModel);
1019
+ const inherited = parent === undefined
1020
+ ? undefined
1021
+ : referencedKey(context, parent, relation.toField, depth + 1);
1022
+ return inherited === undefined
1023
+ ? undefined
1024
+ : { ...inherited, goName: 'ID', isPrimaryKey: true };
1025
+ }
1026
+ return undefined;
1027
+ }
1028
+ function isKeyedRelation(model, relation) {
1029
+ return (relation.isPrimaryKey === true ||
1030
+ (model.compositePrimaryKey?.includes(relation.name) ?? false));
1031
+ }
1032
+ /** True when `from` can reach `to` by holding structs by value. */
1033
+ function reachesByValue(context, from, to, seen = new Set()) {
1034
+ if (from === to) {
1035
+ return true;
1036
+ }
1037
+ if (seen.has(from)) {
1038
+ return false;
1039
+ }
1040
+ seen.add(from);
1041
+ for (const next of context.valueEdges.get(from) ?? []) {
1042
+ if (reachesByValue(context, next, to, seen)) {
1043
+ return true;
1044
+ }
1045
+ }
1046
+ return false;
1047
+ }
1048
+ /** The `constraint:` entry for the referential actions of a relation, or undefined when GORM's defaults apply. */
1049
+ function constraintEntry(relation) {
1050
+ const actions = [];
1051
+ if (relation.onUpdate !== undefined && relation.onUpdate !== 'noAction') {
1052
+ actions.push(`OnUpdate:${ON_DELETE_NAMES[relation.onUpdate]}`);
1053
+ }
1054
+ if (relation.onDelete !== 'noAction') {
1055
+ actions.push(`OnDelete:${ON_DELETE_NAMES[relation.onDelete]}`);
1056
+ }
1057
+ if (actions.length === 0) {
1058
+ return undefined;
1059
+ }
1060
+ // A name is only recognized by GORM when it is letters, underscores and hyphens.
1061
+ const named = relation.constraintName !== undefined &&
1062
+ /^[A-Za-z_-]+$/.test(relation.constraintName);
1063
+ return `constraint:${named ? `${relation.constraintName ?? ''},` : ''}${actions.join(',')}`;
1064
+ }
1065
+ function planOwnedRelations(context, model) {
1066
+ const plan = context.plans.get(model.name);
1067
+ if (plan === undefined) {
1068
+ return;
1069
+ }
1070
+ for (const relation of model.relations) {
1071
+ const target = findModel(context.schema, relation.targetModel);
1072
+ const label = `${model.name}.${relation.name}`;
1073
+ if (target === undefined) {
1074
+ context.warnings.push(`${label}: target model "${relation.targetModel}" does not exist in the schema; the relation was skipped.`);
1075
+ continue;
1076
+ }
1077
+ if (relation.kind === 'manyToMany') {
1078
+ planManyToMany(context, plan, relation, target);
1079
+ }
1080
+ else {
1081
+ planToOne(context, plan, relation, target);
1082
+ }
1083
+ }
1084
+ }
1085
+ function planToOne(context, plan, relation, target) {
1086
+ const model = plan.model;
1087
+ const label = `${model.name}.${relation.name}`;
1088
+ const key = referencedKey(context, target, relation.toField);
1089
+ const targetPlan = context.plans.get(target.name);
1090
+ if (key === undefined || targetPlan === undefined) {
1091
+ context.warnings.push(`${label}: target model "${target.name}" has no single-column primary key to reference; the relation was skipped.`);
1092
+ return;
1093
+ }
1094
+ if (relation.onDelete === 'setNull' && !relation.isNullable) {
1095
+ context.warnings.push(`${label}: onDelete SET NULL on a required relation will fail at the database level; review the relation.`);
1096
+ }
1097
+ const keyed = isKeyedRelation(model, relation);
1098
+ const nullable = relation.isNullable && !keyed;
1099
+ const baseName = exportedName(context, label, relation.name, 'Rel');
1100
+ const associationName = uniqueName(baseName, plan.names);
1101
+ const foreignKeyName = uniqueName(`${baseName}${key.goName}`, plan.names);
1102
+ for (const imported of key.spec.imports) {
1103
+ plan.imports.add(imported);
1104
+ }
1105
+ const column = relation.columnName === ''
1106
+ ? `${toSnakeCase(relation.name)}_id`
1107
+ : relation.columnName;
1108
+ const foreignKeyEntries = [];
1109
+ if (gormColumnName(foreignKeyName) !== column) {
1110
+ foreignKeyEntries.push(`column:${tagValue(column)}`);
1111
+ }
1112
+ foreignKeyEntries.push(...key.spec.typeEntries);
1113
+ if (keyed) {
1114
+ foreignKeyEntries.push('primaryKey');
1115
+ if (key.spec.kind === 'int' &&
1116
+ (model.compositePrimaryKey?.length ?? 0) === 0) {
1117
+ foreignKeyEntries.push('autoIncrement:false');
1118
+ }
1119
+ }
1120
+ else {
1121
+ if (!nullable) {
1122
+ foreignKeyEntries.push('not null');
1123
+ }
1124
+ if (relation.kind === 'oneToOne') {
1125
+ foreignKeyEntries.push('unique');
1126
+ }
1127
+ }
1128
+ const foreignKey = {
1129
+ name: foreignKeyName,
1130
+ type: memberType(key.spec, nullable),
1131
+ entries: foreignKeyEntries,
1132
+ role: 'column',
1133
+ };
1134
+ // A struct holds a required, non-recursive parent by value; anything else is
1135
+ // a pointer so the generated types stay finite.
1136
+ const byValue = !nullable &&
1137
+ target.name !== model.name &&
1138
+ !reachesByValue(context, target.name, model.name);
1139
+ if (byValue) {
1140
+ const edges = context.valueEdges.get(model.name) ?? new Set();
1141
+ edges.add(target.name);
1142
+ context.valueEdges.set(model.name, edges);
1143
+ }
1144
+ const constraint = constraintEntry(relation);
1145
+ const references = relation.toField !== undefined && !key.isPrimaryKey
1146
+ ? key.goName
1147
+ : undefined;
1148
+ const association = {
1149
+ name: associationName,
1150
+ type: `${byValue ? '' : '*'}${targetPlan.typeName}`,
1151
+ entries: [
1152
+ `foreignKey:${foreignKeyName}`,
1153
+ ...(references === undefined ? [] : [`references:${references}`]),
1154
+ ...(constraint === undefined ? [] : [constraint]),
1155
+ ],
1156
+ role: 'association',
1157
+ };
1158
+ plan.members.push(foreignKey, association);
1159
+ plan.byKey.set(`r:${relation.name}`, foreignKey);
1160
+ plan.owned.set(relation.name, {
1161
+ relation,
1162
+ target,
1163
+ foreignKey,
1164
+ association,
1165
+ ...(references === undefined ? {} : { references }),
1166
+ ...(constraint === undefined ? {} : { constraint }),
1167
+ });
1168
+ }
1169
+ function joinPlanOf(model, relation) {
1170
+ const isSelfReference = model.name === relation.targetModel;
1171
+ return {
1172
+ table: `${model.tableName}_${relation.name}`,
1173
+ ownerColumn: `${isSelfReference ? 'from_' : ''}${toSnakeCase(model.name)}_id`,
1174
+ targetColumn: `${isSelfReference ? 'to_' : ''}${toSnakeCase(relation.targetModel)}_id`,
1175
+ };
1176
+ }
1177
+ /**
1178
+ * The tag entries of one side of a many-to-many relation. GORM names the join
1179
+ * columns `<Struct><Key>` and `<Target><Key>`; `joinForeignKey` and
1180
+ * `joinReferences` are only written when the columns differ from that.
1181
+ */
1182
+ function manyToManyEntries(join, ownColumn, otherColumn, defaultOwnColumn, defaultOtherColumn, isSelfReference) {
1183
+ // Both sides carry the actions: GORM builds the join table from whichever struct it migrates first.
1184
+ const entries = [`many2many:${tagValue(join.table)}`];
1185
+ if (isSelfReference || ownColumn !== defaultOwnColumn) {
1186
+ entries.push(`joinForeignKey:${tagValue(ownColumn)}`);
1187
+ }
1188
+ if (isSelfReference || otherColumn !== defaultOtherColumn) {
1189
+ entries.push(`joinReferences:${tagValue(otherColumn)}`);
1190
+ }
1191
+ entries.push('constraint:OnDelete:CASCADE');
1192
+ return entries;
1193
+ }
1194
+ function planManyToMany(context, plan, relation, target) {
1195
+ const model = plan.model;
1196
+ const label = `${model.name}.${relation.name}`;
1197
+ const targetPlan = context.plans.get(target.name);
1198
+ const ownerKey = referencedKey(context, model, undefined);
1199
+ const targetKey = referencedKey(context, target, undefined);
1200
+ if (targetPlan === undefined ||
1201
+ ownerKey === undefined ||
1202
+ targetKey === undefined) {
1203
+ context.warnings.push(`${label}: ${ownerKey === undefined ? model.name : target.name} has no single-column primary key to reference from a join table; the relation was skipped.`);
1204
+ return;
1205
+ }
1206
+ const join = joinPlanOf(model, relation);
1207
+ if (join.table !== join.table.toLowerCase()) {
1208
+ context.warnings.push(`${label}: GORM lower-cases and pluralizes a join table name that has capital letters, so "${join.table}" will not be used as written.`);
1209
+ }
1210
+ const isSelfReference = model.name === target.name;
1211
+ const name = uniqueName(exportedName(context, label, relation.name, 'Rel'), plan.names);
1212
+ plan.members.push({
1213
+ name,
1214
+ type: `[]${targetPlan.typeName}`,
1215
+ entries: manyToManyEntries(join, join.ownerColumn, join.targetColumn, gormColumnName(`${plan.typeName}${ownerKey.goName}`), gormColumnName(`${targetPlan.typeName}${targetKey.goName}`), isSelfReference),
1216
+ role: 'association',
1217
+ });
1218
+ plan.manyToMany.add(relation.name);
1219
+ }
1220
+ /** The name an unnamed reverse relation gets: the model's plural (`Posts`), or its name for a one-to-one. */
1221
+ function defaultInverseName(typeName, plural) {
1222
+ return plural ? goName(gormTableName(typeName)) : typeName;
1223
+ }
1224
+ function planInverseSides(context, model) {
1225
+ const plan = context.plans.get(model.name);
1226
+ if (plan === undefined) {
1227
+ return;
1228
+ }
1229
+ for (const relation of model.relations) {
1230
+ const target = findModel(context.schema, relation.targetModel);
1231
+ const targetPlan = target === undefined ? undefined : context.plans.get(target.name);
1232
+ if (target === undefined || targetPlan === undefined) {
1233
+ continue;
1234
+ }
1235
+ const label = `${model.name}.${relation.name}`;
1236
+ if (relation.kind === 'manyToMany') {
1237
+ planInverseManyToMany(context, plan, relation, target, targetPlan);
1238
+ continue;
1239
+ }
1240
+ const owned = plan.owned.get(relation.name);
1241
+ if (owned === undefined) {
1242
+ continue;
1243
+ }
1244
+ const many = relation.kind !== 'oneToOne';
1245
+ const raw = relation.relatedName ?? defaultInverseName(plan.typeName, many);
1246
+ const name = uniqueName(exportedName(context, label, raw, 'Related'), targetPlan.names);
1247
+ targetPlan.members.push({
1248
+ name,
1249
+ type: many ? `[]${plan.typeName}` : `*${plan.typeName}`,
1250
+ entries: [
1251
+ `foreignKey:${owned.foreignKey.name}`,
1252
+ ...(owned.references === undefined
1253
+ ? []
1254
+ : [`references:${owned.references}`]),
1255
+ ...(owned.constraint === undefined ? [] : [owned.constraint]),
1256
+ ],
1257
+ role: 'association',
1258
+ });
1259
+ }
1260
+ }
1261
+ function planInverseManyToMany(context, plan, relation, target, targetPlan) {
1262
+ const model = plan.model;
1263
+ const ownerKey = referencedKey(context, model, undefined);
1264
+ const targetKey = referencedKey(context, target, undefined);
1265
+ if (ownerKey === undefined ||
1266
+ targetKey === undefined ||
1267
+ !plan.manyToMany.has(relation.name)) {
1268
+ return;
1269
+ }
1270
+ const label = `${model.name}.${relation.name}`;
1271
+ const join = joinPlanOf(model, relation);
1272
+ const raw = relation.relatedName ?? defaultInverseName(plan.typeName, true);
1273
+ const name = uniqueName(exportedName(context, label, raw, 'Related'), targetPlan.names);
1274
+ targetPlan.members.push({
1275
+ name,
1276
+ type: `[]${plan.typeName}`,
1277
+ entries: manyToManyEntries(join, join.targetColumn, join.ownerColumn, gormColumnName(`${targetPlan.typeName}${targetKey.goName}`), gormColumnName(`${plan.typeName}${ownerKey.goName}`), model.name === target.name),
1278
+ role: 'association',
1279
+ });
1280
+ }
1281
+ // ---------------------------------------------------------------------------
1282
+ // Planning: indexes
1283
+ // ---------------------------------------------------------------------------
1284
+ function planIndexes(context, model) {
1285
+ const plan = context.plans.get(model.name);
1286
+ if (plan === undefined) {
1287
+ return;
1288
+ }
1289
+ for (const index of model.indexes) {
1290
+ const label = `${model.name} index (${index.fields.join(', ')})`;
1291
+ if (plan.embedsModel && isEmbeddedSoftDeleteIndex(plan, index)) {
1292
+ continue;
1293
+ }
1294
+ const members = [];
1295
+ const columns = [];
1296
+ let resolved = true;
1297
+ for (const fieldName of index.fields) {
1298
+ const member = plan.byKey.get(`r:${fieldName}`) ?? plan.byKey.get(`f:${fieldName}`);
1299
+ const byColumn = model.fields.find((candidate) => candidate.columnName === fieldName);
1300
+ const resolvedMember = member ??
1301
+ (byColumn === undefined
1302
+ ? undefined
1303
+ : plan.byKey.get(`f:${byColumn.name}`));
1304
+ if (resolvedMember === undefined) {
1305
+ context.warnings.push(`${label}: "${fieldName}" is not a column of the model (or belongs to gorm.Model); the index was skipped.`);
1306
+ resolved = false;
1307
+ break;
1308
+ }
1309
+ members.push(resolvedMember);
1310
+ columns.push(columnOf(resolvedMember, model));
1311
+ }
1312
+ if (!resolved || members.length === 0) {
1313
+ continue;
1314
+ }
1315
+ addIndexEntries(model, index, members, columns);
1316
+ }
1317
+ }
1318
+ /** True for the plain `deleted_at` index that `gorm.Model` already declares. */
1319
+ function isEmbeddedSoftDeleteIndex(plan, index) {
1320
+ return (index.fields.length === 1 &&
1321
+ plan.fieldNames.get(index.fields[0] ?? '') === 'DeletedAt' &&
1322
+ !index.isUnique &&
1323
+ index.name === undefined);
1324
+ }
1325
+ /** The column name of a member, read back from its tag or derived from its Go name. */
1326
+ function columnOf(member, model) {
1327
+ const explicit = member.entries
1328
+ .find((entry) => entry.startsWith('column:'))
1329
+ ?.slice('column:'.length);
1330
+ if (explicit !== undefined) {
1331
+ return explicit;
1332
+ }
1333
+ const derived = gormColumnName(member.name);
1334
+ return derived === '' ? model.tableName : derived;
1335
+ }
1336
+ function addIndexEntries(model, index, members, columns) {
1337
+ const keyword = index.isUnique ? 'uniqueIndex' : 'index';
1338
+ if (members.length === 1) {
1339
+ const member = members[0];
1340
+ const alreadyUnique = member.entries.some((entry) => entry === 'unique' || entry.startsWith('uniqueIndex'));
1341
+ if (index.isUnique && alreadyUnique && index.name === undefined) {
1342
+ return;
1343
+ }
1344
+ member.entries.push(index.name === undefined ? keyword : `${keyword}:${tagValue(index.name)}`);
1345
+ return;
1346
+ }
1347
+ // GORM only joins columns into one index through a shared index name.
1348
+ const name = index.name ??
1349
+ `${index.isUnique ? 'uni' : 'idx'}_${model.tableName}_${columns.join('_')}`;
1350
+ members.forEach((member, position) => {
1351
+ member.entries.push(`${keyword}:${tagValue(name)},priority:${position + 1}`);
1352
+ });
1353
+ }
1354
+ // ---------------------------------------------------------------------------
1355
+ // Warnings
1356
+ // ---------------------------------------------------------------------------
1357
+ /** Drops prismaOnlyWarnings lines for things this emitter does write (onUpdate actions, database default expressions). */
1358
+ function removeHandledWarnings(context) {
1359
+ const handled = new Set();
1360
+ for (const model of context.schema.models) {
1361
+ for (const relation of model.relations) {
1362
+ if (relation.onUpdate !== undefined) {
1363
+ handled.add(`${model.name}.${relation.name}: the onUpdate action "${relation.onUpdate}" is not supported by this format and was ignored.`);
1364
+ }
1365
+ }
1366
+ for (const field of model.fields) {
1367
+ if (field.type === 'unsupported' && field.unsupportedType !== undefined) {
1368
+ handled.add(`${model.name}.${field.name}: the database type "${field.unsupportedType}" (Prisma Unsupported) has no equivalent here; the column was written as a plain string.`);
1369
+ }
1370
+ if (field.default?.kind === 'dbExpression' &&
1371
+ field.default.isFunction !== true) {
1372
+ handled.add(`${model.name}.${field.name}: the database default expression ${field.default.expression} was dropped.`);
1373
+ }
1374
+ }
1375
+ }
1376
+ const kept = context.warnings.filter((warning) => !handled.has(warning));
1377
+ context.warnings.splice(0, context.warnings.length, ...kept);
1378
+ }
1379
+ // ---------------------------------------------------------------------------
1380
+ // Rendering
1381
+ // ---------------------------------------------------------------------------
1382
+ function renderHeader(context, imports) {
1383
+ const paths = [...imports].map((name) => IMPORT_PATHS[name]);
1384
+ // gofmt keeps the import groups apart and sorts each: the standard library first.
1385
+ const standard = paths
1386
+ .filter((path) => !path.includes('.'))
1387
+ .sort();
1388
+ const external = paths
1389
+ .filter((path) => path.includes('.'))
1390
+ .sort();
1391
+ const lines = [`package ${context.packageName}`, ''];
1392
+ if (paths.length === 1) {
1393
+ lines.push(`import ${goString(paths[0] ?? '')}`, '');
1394
+ }
1395
+ else if (paths.length > 1) {
1396
+ lines.push('import (');
1397
+ for (const path of standard) {
1398
+ lines.push(`${INDENT}${goString(path)}`);
1399
+ }
1400
+ if (standard.length > 0 && external.length > 0) {
1401
+ lines.push('');
1402
+ }
1403
+ for (const path of external) {
1404
+ lines.push(`${INDENT}${goString(path)}`);
1405
+ }
1406
+ lines.push(')', '');
1407
+ }
1408
+ return lines;
1409
+ }
1410
+ function renderEnum(context, enumDefinition, typeName) {
1411
+ const lines = [
1412
+ ...renderHeader(context, []),
1413
+ `// ${typeName} is a database enum, stored as text.`,
1414
+ `type ${typeName} string`,
1415
+ ];
1416
+ if (enumDefinition.values.length > 0) {
1417
+ const names = enumConstantNames(context, enumDefinition, typeName);
1418
+ const rows = enumDefinition.values.map((value, position) => [
1419
+ names[position] ?? '',
1420
+ typeName,
1421
+ `= ${goString(value.dbValue)}`,
1422
+ ...(value.label === undefined || value.label === ''
1423
+ ? []
1424
+ : [`// ${value.label.replace(/\s+/g, ' ')}`]),
1425
+ ]);
1426
+ lines.push('', 'const (', ...alignRows(rows).map((line) => `${INDENT}${line}`), ')');
1427
+ }
1428
+ lines.push('');
1429
+ return lines.join('\n');
1430
+ }
1431
+ function memberRow(member) {
1432
+ if (member.name === '') {
1433
+ return [member.type];
1434
+ }
1435
+ const row = [member.name, member.type];
1436
+ if (member.entries.length > 0) {
1437
+ row.push(tagLiteral(member.entries));
1438
+ }
1439
+ return row;
1440
+ }
1441
+ /** Struct lines: columns first, then associations; each group is aligned on its own. */
1442
+ function renderMembers(plan) {
1443
+ const columns = plan.members.filter((member) => member.role === 'column');
1444
+ const associations = plan.members.filter((member) => member.role === 'association');
1445
+ const groups = [columns, associations]
1446
+ .filter((group) => group.length > 0)
1447
+ .map((group) => alignRows(group.map(memberRow)).map((line) => `${INDENT}${line}`));
1448
+ return groups.flatMap((group, position) => position === 0 ? group : ['', ...group]);
1449
+ }
1450
+ function receiverName(typeName) {
1451
+ return typeName.charAt(0).toLowerCase();
1452
+ }
1453
+ function renderUuidHook(plan) {
1454
+ const receiver = receiverName(plan.typeName);
1455
+ const body = [];
1456
+ for (const hook of plan.uuidHooks) {
1457
+ const field = `${receiver}.${hook.member}`;
1458
+ const empty = hook.style === 'string'
1459
+ ? `${field} == ""`
1460
+ : hook.style === 'pointer'
1461
+ ? `${field} == nil`
1462
+ : `${field} == uuid.Nil`;
1463
+ body.push(`${INDENT}if ${empty} {`);
1464
+ if (hook.version7) {
1465
+ body.push(`${INDENT}${INDENT}id, err := uuid.NewV7()`, `${INDENT}${INDENT}if err != nil {`, `${INDENT}${INDENT}${INDENT}return err`, `${INDENT}${INDENT}}`);
1466
+ body.push(`${INDENT}${INDENT}${field} = ${hook.style === 'string' ? 'id.String()' : hook.style === 'pointer' ? '&id' : 'id'}`);
1467
+ }
1468
+ else if (hook.style === 'string') {
1469
+ body.push(`${INDENT}${INDENT}${field} = uuid.NewString()`);
1470
+ }
1471
+ else if (hook.style === 'pointer') {
1472
+ body.push(`${INDENT}${INDENT}id := uuid.New()`, `${INDENT}${INDENT}${field} = &id`);
1473
+ }
1474
+ else {
1475
+ body.push(`${INDENT}${INDENT}${field} = uuid.New()`);
1476
+ }
1477
+ body.push(`${INDENT}}`);
1478
+ }
1479
+ return [
1480
+ '// BeforeCreate fills in generated UUIDs that were left empty.',
1481
+ `func (${receiver} *${plan.typeName}) BeforeCreate(tx *gorm.DB) error {`,
1482
+ ...body,
1483
+ `${INDENT}return nil`,
1484
+ '}',
1485
+ ];
1486
+ }
1487
+ function renderStruct(context, plan) {
1488
+ const members = renderMembers(plan);
1489
+ const lines = [
1490
+ ...renderHeader(context, plan.imports),
1491
+ `// ${plan.typeName} maps to the ${goString(plan.model.tableName)} table.`,
1492
+ members.length === 0
1493
+ ? `type ${plan.typeName} struct{}`
1494
+ : `type ${plan.typeName} struct {`,
1495
+ ...members,
1496
+ ...(members.length === 0 ? [] : ['}']),
1497
+ ];
1498
+ if (plan.tableName !== undefined) {
1499
+ lines.push('', `// TableName overrides GORM's default table name (${gormTableName(plan.typeName)}).`, `func (${plan.typeName}) TableName() string {`, `${INDENT}return ${goString(plan.tableName)}`, '}');
1500
+ }
1501
+ if (plan.uuidHooks.length > 0) {
1502
+ lines.push('', ...renderUuidHook(plan));
1503
+ }
1504
+ lines.push('');
1505
+ return lines.join('\n');
1506
+ }
1507
+ //# sourceMappingURL=gorm.js.map