ormbridge 0.0.0-stage → 0.3.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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +459 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +20 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/cliRunner.d.ts +20 -0
  7. package/dist/cliRunner.js +363 -0
  8. package/dist/cliRunner.js.map +1 -0
  9. package/dist/config.d.ts +44 -0
  10. package/dist/config.js +220 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/convert.d.ts +20 -0
  13. package/dist/convert.js +61 -0
  14. package/dist/convert.js.map +1 -0
  15. package/dist/emitters/django.d.ts +4 -0
  16. package/dist/emitters/django.js +501 -0
  17. package/dist/emitters/django.js.map +1 -0
  18. package/dist/emitters/doctrine.d.ts +20 -0
  19. package/dist/emitters/doctrine.js +1134 -0
  20. package/dist/emitters/doctrine.js.map +1 -0
  21. package/dist/emitters/gorm.d.ts +38 -0
  22. package/dist/emitters/gorm.js +1507 -0
  23. package/dist/emitters/gorm.js.map +1 -0
  24. package/dist/emitters/graphene.d.ts +4 -0
  25. package/dist/emitters/graphene.js +431 -0
  26. package/dist/emitters/graphene.js.map +1 -0
  27. package/dist/emitters/laravel.d.ts +33 -0
  28. package/dist/emitters/laravel.js +1695 -0
  29. package/dist/emitters/laravel.js.map +1 -0
  30. package/dist/emitters/prisma.d.ts +21 -0
  31. package/dist/emitters/prisma.js +1058 -0
  32. package/dist/emitters/prisma.js.map +1 -0
  33. package/dist/emitters/typeorm.d.ts +9 -0
  34. package/dist/emitters/typeorm.js +838 -0
  35. package/dist/emitters/typeorm.js.map +1 -0
  36. package/dist/emitters/typescriptInterfaces.d.ts +15 -0
  37. package/dist/emitters/typescriptInterfaces.js +350 -0
  38. package/dist/emitters/typescriptInterfaces.js.map +1 -0
  39. package/dist/formats.d.ts +82 -0
  40. package/dist/formats.js +238 -0
  41. package/dist/formats.js.map +1 -0
  42. package/dist/index.d.ts +37 -0
  43. package/dist/index.js +20 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/io.d.ts +48 -0
  46. package/dist/io.js +343 -0
  47. package/dist/io.js.map +1 -0
  48. package/dist/ir.d.ts +197 -0
  49. package/dist/ir.js +10 -0
  50. package/dist/ir.js.map +1 -0
  51. package/dist/naming.d.ts +11 -0
  52. package/dist/naming.js +88 -0
  53. package/dist/naming.js.map +1 -0
  54. package/dist/parsers/django.d.ts +14 -0
  55. package/dist/parsers/django.js +1161 -0
  56. package/dist/parsers/django.js.map +1 -0
  57. package/dist/parsers/doctrine.d.ts +12 -0
  58. package/dist/parsers/doctrine.js +1984 -0
  59. package/dist/parsers/doctrine.js.map +1 -0
  60. package/dist/parsers/goSyntax.d.ts +105 -0
  61. package/dist/parsers/goSyntax.js +435 -0
  62. package/dist/parsers/goSyntax.js.map +1 -0
  63. package/dist/parsers/gorm.d.ts +16 -0
  64. package/dist/parsers/gorm.js +1857 -0
  65. package/dist/parsers/gorm.js.map +1 -0
  66. package/dist/parsers/laravel.d.ts +17 -0
  67. package/dist/parsers/laravel.js +2824 -0
  68. package/dist/parsers/laravel.js.map +1 -0
  69. package/dist/parsers/phpSyntax.d.ts +124 -0
  70. package/dist/parsers/phpSyntax.js +429 -0
  71. package/dist/parsers/phpSyntax.js.map +1 -0
  72. package/dist/parsers/prisma.d.ts +12 -0
  73. package/dist/parsers/prisma.js +964 -0
  74. package/dist/parsers/prisma.js.map +1 -0
  75. package/dist/parsers/pythonSyntax.d.ts +38 -0
  76. package/dist/parsers/pythonSyntax.js +120 -0
  77. package/dist/parsers/pythonSyntax.js.map +1 -0
  78. package/dist/parsers/typeorm.d.ts +12 -0
  79. package/dist/parsers/typeorm.js +1614 -0
  80. package/dist/parsers/typeorm.js.map +1 -0
  81. package/dist/parsers/typescriptSyntax.d.ts +57 -0
  82. package/dist/parsers/typescriptSyntax.js +200 -0
  83. package/dist/parsers/typescriptSyntax.js.map +1 -0
  84. package/dist/prismaOnlyConstructs.d.ts +7 -0
  85. package/dist/prismaOnlyConstructs.js +76 -0
  86. package/dist/prismaOnlyConstructs.js.map +1 -0
  87. package/dist/result.d.ts +20 -0
  88. package/dist/result.js +18 -0
  89. package/dist/result.js.map +1 -0
  90. package/dist/transforms.d.ts +13 -0
  91. package/dist/transforms.js +142 -0
  92. package/dist/transforms.js.map +1 -0
  93. package/package.json +83 -4
@@ -0,0 +1,1857 @@
1
+ import { singularize, toCamelCase, toSnakeCase } from '../naming.js';
2
+ import { describeThrown, err, ok } from '../result.js';
3
+ import { defaultImportName, describeGoType, getGoParser, lookupStructTag, readGoFile, } from './goSyntax.js';
4
+ // ---------------------------------------------------------------------------
5
+ // GORM naming conventions (gorm.io/gorm/schema NamingStrategy)
6
+ // ---------------------------------------------------------------------------
7
+ /** Initialisms GORM keeps together when it converts a Go name to snake_case, in GORM's priority order. */
8
+ const COMMON_INITIALISMS = [
9
+ 'API',
10
+ 'ASCII',
11
+ 'CPU',
12
+ 'CSS',
13
+ 'DNS',
14
+ 'EOF',
15
+ 'GUID',
16
+ 'HTML',
17
+ 'HTTP',
18
+ 'HTTPS',
19
+ 'ID',
20
+ 'IP',
21
+ 'JSON',
22
+ 'LHS',
23
+ 'QPS',
24
+ 'RAM',
25
+ 'RHS',
26
+ 'RPC',
27
+ 'SLA',
28
+ 'SMTP',
29
+ 'SSH',
30
+ 'TLS',
31
+ 'TTL',
32
+ 'UID',
33
+ 'UI',
34
+ 'UUID',
35
+ 'URI',
36
+ 'URL',
37
+ 'UTF8',
38
+ 'VM',
39
+ 'XML',
40
+ 'XSRF',
41
+ 'XSS',
42
+ ];
43
+ const INITIALISM_PATTERN = new RegExp(COMMON_INITIALISMS.join('|'), 'g');
44
+ function isUpperAscii(character) {
45
+ return character !== undefined && character >= 'A' && character <= 'Z';
46
+ }
47
+ function isDigit(character) {
48
+ return character !== undefined && character >= '0' && character <= '9';
49
+ }
50
+ /** GORM's default column name for a Go field name: `PublicID` becomes `public_id`. */
51
+ export function gormColumnName(goName) {
52
+ if (goName === '') {
53
+ return '';
54
+ }
55
+ const value = goName.replace(INITIALISM_PATTERN, (initialism) => initialism.charAt(0) + initialism.slice(1).toLowerCase());
56
+ let result = '';
57
+ let lastIsUpper = false;
58
+ let currentIsUpper = isUpperAscii(value.charAt(0));
59
+ for (let index = 0; index < value.length - 1; index += 1) {
60
+ const character = value.charAt(index);
61
+ const nextIsUpper = isUpperAscii(value.charAt(index + 1));
62
+ const nextIsDigit = isDigit(value.charAt(index + 1));
63
+ if (currentIsUpper) {
64
+ if (lastIsUpper && (nextIsUpper || nextIsDigit)) {
65
+ result += character.toLowerCase();
66
+ }
67
+ else {
68
+ if (index > 0 &&
69
+ value.charAt(index - 1) !== '_' &&
70
+ value.charAt(index + 1) !== '_') {
71
+ result += '_';
72
+ }
73
+ result += character.toLowerCase();
74
+ }
75
+ }
76
+ else {
77
+ result += character;
78
+ }
79
+ lastIsUpper = currentIsUpper;
80
+ currentIsUpper = nextIsUpper;
81
+ }
82
+ const lastCharacter = value.charAt(value.length - 1);
83
+ if (currentIsUpper) {
84
+ if (!lastIsUpper && value.length > 1) {
85
+ result += '_';
86
+ }
87
+ result += lastCharacter.toLowerCase();
88
+ }
89
+ else {
90
+ result += lastCharacter;
91
+ }
92
+ return result;
93
+ }
94
+ const UNCOUNTABLE_WORDS = [
95
+ 'equipment',
96
+ 'information',
97
+ 'rice',
98
+ 'money',
99
+ 'species',
100
+ 'series',
101
+ 'fish',
102
+ 'sheep',
103
+ 'jeans',
104
+ 'police',
105
+ ];
106
+ const IRREGULAR_PLURALS = [
107
+ ['person', 'people'],
108
+ ['man', 'men'],
109
+ ['child', 'children'],
110
+ ['sex', 'sexes'],
111
+ ['move', 'moves'],
112
+ ['mombie', 'mombies'],
113
+ ];
114
+ /** The plural rules of github.com/jinzhu/inflection (which GORM uses), in the order they are tried. */
115
+ const PLURAL_RULES = [
116
+ [/(quiz)$/, '$1zes'],
117
+ [/^(oxen)$/, '$1'],
118
+ [/^(ox)$/, '$1en'],
119
+ [/^(m|l)ice$/, '$1ice'],
120
+ [/^(m|l)ouse$/, '$1ice'],
121
+ [/(matr|vert|ind)(?:ix|ex)$/, '$1ices'],
122
+ [/(x|ch|ss|sh)$/, '$1es'],
123
+ [/([^aeiouy]|qu)y$/, '$1ies'],
124
+ [/(hive)$/, '$1s'],
125
+ [/(?:([^f])fe|([lr])f)$/, '$1$2ves'],
126
+ [/sis$/, 'ses'],
127
+ [/([ti])a$/, '$1a'],
128
+ [/([ti])um$/, '$1a'],
129
+ [/(buffal|tomat)o$/, '$1oes'],
130
+ [/(bu)s$/, '$1ses'],
131
+ [/(alias|status)$/, '$1es'],
132
+ [/(octop|vir)i$/, '$1i'],
133
+ [/(octop|vir)us$/, '$1i'],
134
+ [/^(ax|test)is$/, '$1es'],
135
+ [/s$/, 's'],
136
+ [/([a-z])$/, '$1s'],
137
+ ];
138
+ function pluralize(word) {
139
+ if (UNCOUNTABLE_WORDS.includes(word.toLowerCase())) {
140
+ return word;
141
+ }
142
+ for (const [singular, plural] of IRREGULAR_PLURALS) {
143
+ if (word.endsWith(singular)) {
144
+ return word.slice(0, word.length - singular.length) + plural;
145
+ }
146
+ }
147
+ for (const [pattern, replacement] of PLURAL_RULES) {
148
+ if (pattern.test(word)) {
149
+ return word.replace(pattern, replacement);
150
+ }
151
+ }
152
+ return word;
153
+ }
154
+ /** GORM's default table name for a struct: `PostTag` becomes `post_tags`. */
155
+ export function gormTableName(structName) {
156
+ return pluralize(gormColumnName(structName));
157
+ }
158
+ function upperFirst(text) {
159
+ return text.charAt(0).toUpperCase() + text.slice(1);
160
+ }
161
+ /** The IR name of a Go field: `PublicID` becomes `publicId`. */
162
+ function irFieldName(goName) {
163
+ return toCamelCase(goName);
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Static tables
167
+ // ---------------------------------------------------------------------------
168
+ const ON_DELETE_MAP = {
169
+ CASCADE: 'cascade',
170
+ 'SET NULL': 'setNull',
171
+ RESTRICT: 'restrict',
172
+ 'NO ACTION': 'noAction',
173
+ 'SET DEFAULT': 'setDefault',
174
+ };
175
+ const NOW_SQL = /^(now\(\)|current_timestamp(\(\d*\))?|getdate\(\)|sysdate|systimestamp|localtimestamp(\(\d*\))?|datetime\('now'\)|\(datetime\('now'\)\))$/i;
176
+ const UUID_SQL = /^(uuid_generate_v[14]\(\)|gen_random_uuid\(\)|uuid\(\)|\(uuid\(\)\)|newid\(\)|newsequentialid\(\)|sys_guid\(\))$/i;
177
+ /** Library types by `<package>.<Name>`, where the package is the import's last path element. */
178
+ const KNOWN_TYPES = {
179
+ 'time.Time': { type: 'dateTime', goKind: 'time' },
180
+ 'time.Duration': { type: 'bigInt', goKind: 'int' },
181
+ 'gorm.DeletedAt': { type: 'dateTime', goKind: 'time', nullable: true },
182
+ 'sql.NullString': { type: 'text', goKind: 'string', nullable: true },
183
+ 'sql.NullInt64': { type: 'bigInt', goKind: 'int', nullable: true },
184
+ 'sql.NullInt32': { type: 'int', goKind: 'int', nullable: true },
185
+ 'sql.NullInt16': { type: 'int', goKind: 'int', nullable: true },
186
+ 'sql.NullByte': { type: 'int', goKind: 'int', nullable: true },
187
+ 'sql.NullFloat64': { type: 'float', goKind: 'float', nullable: true },
188
+ 'sql.NullBool': { type: 'boolean', goKind: 'bool', nullable: true },
189
+ 'sql.NullTime': { type: 'dateTime', goKind: 'time', nullable: true },
190
+ 'sql.RawBytes': { type: 'bytes', goKind: 'other' },
191
+ 'uuid.UUID': { type: 'uuid', goKind: 'other' },
192
+ 'uuid.NullUUID': { type: 'uuid', goKind: 'other', nullable: true },
193
+ 'decimal.Decimal': { type: 'decimal', goKind: 'float' },
194
+ 'decimal.NullDecimal': { type: 'decimal', goKind: 'float', nullable: true },
195
+ 'datatypes.JSON': { type: 'json', goKind: 'other' },
196
+ 'datatypes.JSONMap': { type: 'json', goKind: 'other' },
197
+ 'datatypes.Date': { type: 'date', goKind: 'time' },
198
+ 'datatypes.Time': { type: 'time', goKind: 'time' },
199
+ 'datatypes.UUID': { type: 'uuid', goKind: 'other' },
200
+ 'datatypes.BinUUID': { type: 'uuid', goKind: 'other' },
201
+ 'json.RawMessage': { type: 'json', goKind: 'other' },
202
+ 'net.IP': { type: 'ipAddress', goKind: 'other' },
203
+ 'netip.Addr': { type: 'ipAddress', goKind: 'other' },
204
+ 'pq.StringArray': { type: 'text', goKind: 'string', arrayDepth: 1 },
205
+ 'pq.Int64Array': { type: 'bigInt', goKind: 'int', arrayDepth: 1 },
206
+ 'pq.Int32Array': { type: 'int', goKind: 'int', arrayDepth: 1 },
207
+ 'pq.Float64Array': { type: 'float', goKind: 'float', arrayDepth: 1 },
208
+ 'pq.BoolArray': { type: 'boolean', goKind: 'bool', arrayDepth: 1 },
209
+ 'pq.ByteaArray': { type: 'bytes', goKind: 'other', arrayDepth: 1 },
210
+ 'null.String': { type: 'text', goKind: 'string', nullable: true },
211
+ 'null.Int': { type: 'bigInt', goKind: 'int', nullable: true },
212
+ 'null.Int32': { type: 'int', goKind: 'int', nullable: true },
213
+ 'null.Float': { type: 'float', goKind: 'float', nullable: true },
214
+ 'null.Bool': { type: 'boolean', goKind: 'bool', nullable: true },
215
+ 'null.Time': { type: 'dateTime', goKind: 'time', nullable: true },
216
+ };
217
+ /** Generic library types whose single type argument is the stored value. */
218
+ const GENERIC_JSON_TYPES = new Set([
219
+ 'datatypes.JSONType',
220
+ 'datatypes.JSONSlice',
221
+ ]);
222
+ const INTEGER_KINDS = {
223
+ int: { bits: 64, unsigned: false },
224
+ int8: { bits: 8, unsigned: false },
225
+ int16: { bits: 16, unsigned: false },
226
+ int32: { bits: 32, unsigned: false },
227
+ int64: { bits: 64, unsigned: false },
228
+ rune: { bits: 32, unsigned: false },
229
+ uint: { bits: 64, unsigned: true },
230
+ uint8: { bits: 8, unsigned: true },
231
+ byte: { bits: 8, unsigned: true },
232
+ uint16: { bits: 16, unsigned: true },
233
+ uint32: { bits: 32, unsigned: true },
234
+ uint64: { bits: 64, unsigned: true },
235
+ uintptr: { bits: 64, unsigned: true },
236
+ };
237
+ /** An integer column is `int` up to 32 bits and `bigInt` above; unsigned types need one more bit, as in GORM's PostgreSQL dialect. */
238
+ function integerType(bits, unsigned) {
239
+ return bits + (unsigned ? 1 : 0) <= 32 ? 'int' : 'bigInt';
240
+ }
241
+ /**
242
+ * Splits a tag into its settings the way GORM's `ParseTagSetting` does: `;` (or `separator`)
243
+ * separates settings, `\;` escapes it, and the first `:` separates key from value.
244
+ */
245
+ function parseSettings(text, separator) {
246
+ const entries = [];
247
+ const names = text.split(separator);
248
+ for (let position = 0; position < names.length; position += 1) {
249
+ let current = names[position] ?? '';
250
+ while (current.endsWith('\\') && position + 1 < names.length) {
251
+ position += 1;
252
+ current = current.slice(0, -1) + separator + (names[position] ?? '');
253
+ }
254
+ const parts = current.split(':');
255
+ const key = (parts[0] ?? '').trim().toUpperCase();
256
+ if (parts.length >= 2) {
257
+ entries.push({ key, value: parts.slice(1).join(':') });
258
+ }
259
+ else if (key !== '') {
260
+ entries.push({ key, value: key });
261
+ }
262
+ }
263
+ return entries;
264
+ }
265
+ function gormTagOf(field) {
266
+ return field.tag === undefined
267
+ ? ''
268
+ : (lookupStructTag(field.tag, 'gorm') ?? '');
269
+ }
270
+ /** The last value given for any of the keys (GORM keeps the last duplicate). */
271
+ function entryValue(entries, ...keys) {
272
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
273
+ const entry = entries[index];
274
+ if (entry !== undefined && keys.includes(entry.key)) {
275
+ return entry.value;
276
+ }
277
+ }
278
+ return undefined;
279
+ }
280
+ /** True/false when a flag setting is present (`false` turns it off), undefined when absent. */
281
+ function entryFlag(entries, ...keys) {
282
+ const value = entryValue(entries, ...keys);
283
+ if (value === undefined) {
284
+ return undefined;
285
+ }
286
+ return value !== '' && value.toLowerCase() !== 'false';
287
+ }
288
+ function entryNumber(entries, key) {
289
+ const value = entryValue(entries, key);
290
+ if (value === undefined) {
291
+ return undefined;
292
+ }
293
+ const parsed = Number.parseInt(value, 10);
294
+ return Number.isNaN(parsed) ? undefined : parsed;
295
+ }
296
+ function splitList(value) {
297
+ return value === undefined
298
+ ? []
299
+ : value
300
+ .split(',')
301
+ .map((item) => item.trim())
302
+ .filter((item) => item !== '');
303
+ }
304
+ /** `gorm:"-"`, `-:all` and `-:migration` leave the field out of the schema. */
305
+ function isIgnored(entries) {
306
+ const value = entryValue(entries, '-');
307
+ return (value !== undefined &&
308
+ ['-', 'all', 'migration'].includes(value.toLowerCase()));
309
+ }
310
+ /** The `gorm.Model` struct that GORM provides for embedding. */
311
+ const GORM_MODEL = {
312
+ name: 'Model',
313
+ path: 'gorm.io/gorm',
314
+ fields: [
315
+ {
316
+ name: 'ID',
317
+ type: { kind: 'name', name: 'uint', args: [] },
318
+ tag: 'gorm:"primarykey"',
319
+ },
320
+ {
321
+ name: 'CreatedAt',
322
+ type: {
323
+ kind: 'name',
324
+ name: 'Time',
325
+ qualifier: 'time',
326
+ importPath: 'time',
327
+ args: [],
328
+ },
329
+ },
330
+ {
331
+ name: 'UpdatedAt',
332
+ type: {
333
+ kind: 'name',
334
+ name: 'Time',
335
+ qualifier: 'time',
336
+ importPath: 'time',
337
+ args: [],
338
+ },
339
+ },
340
+ {
341
+ name: 'DeletedAt',
342
+ type: {
343
+ kind: 'name',
344
+ name: 'DeletedAt',
345
+ qualifier: 'gorm',
346
+ importPath: 'gorm.io/gorm',
347
+ args: [],
348
+ },
349
+ tag: 'gorm:"index"',
350
+ },
351
+ ],
352
+ };
353
+ // ---------------------------------------------------------------------------
354
+ // Public entry point
355
+ // ---------------------------------------------------------------------------
356
+ /** Parses GORM model structs (Go source with `gorm:"..."` tags) into the shared IR using tree-sitter. */
357
+ export async function parseGorm(sources, options) {
358
+ const parserResult = await getGoParser();
359
+ if (!parserResult.ok) {
360
+ return parserResult;
361
+ }
362
+ try {
363
+ return readGorm(parserResult.value, sources, options);
364
+ }
365
+ catch (thrown) {
366
+ return err('PARSE_FAILED', `Failed to read GORM models from ${sources.map((source) => source.path).join(', ')}. ` +
367
+ `Underlying error: ${describeThrown(thrown)}`);
368
+ }
369
+ }
370
+ function readGorm(parser, sources, options) {
371
+ const warnings = [];
372
+ const ctx = {
373
+ warnings,
374
+ structs: new Map(),
375
+ namedTypes: new Map(),
376
+ enums: new Map(),
377
+ methods: new Map(),
378
+ consts: new Map(),
379
+ models: new Set(),
380
+ };
381
+ const structOrder = [];
382
+ const constList = [];
383
+ for (const source of sources) {
384
+ // The Go grammar needs the final statement to end in a newline, which Go inserts as a semicolon.
385
+ const tree = parser.parse(source.text.endsWith('\n') ? source.text : `${source.text}\n`);
386
+ const file = readGoFile(tree.rootNode, source.path);
387
+ if (file.hasErrors) {
388
+ warnings.push(`${source.path}: the file contains Go syntax errors; some structs or fields may be missing from the output.`);
389
+ }
390
+ for (const goStruct of file.structs) {
391
+ if (ctx.structs.has(goStruct.name)) {
392
+ warnings.push(`Duplicate struct name "${goStruct.name}" (${source.path}); only the first definition was converted.`);
393
+ continue;
394
+ }
395
+ ctx.structs.set(goStruct.name, goStruct);
396
+ structOrder.push(goStruct);
397
+ }
398
+ for (const namedType of file.namedTypes) {
399
+ if (!ctx.namedTypes.has(namedType.name)) {
400
+ ctx.namedTypes.set(namedType.name, namedType);
401
+ }
402
+ }
403
+ for (const goConst of file.consts) {
404
+ constList.push(goConst);
405
+ if (!ctx.consts.has(goConst.name)) {
406
+ ctx.consts.set(goConst.name, goConst);
407
+ }
408
+ }
409
+ for (const method of file.methods) {
410
+ const byName = ctx.methods.get(method.receiver) ?? new Map();
411
+ byName.set(method.name, method);
412
+ ctx.methods.set(method.receiver, byName);
413
+ }
414
+ }
415
+ buildEnums(ctx, constList);
416
+ ctx.models = detectModels(structOrder, ctx);
417
+ const rawModels = structOrder
418
+ .filter((goStruct) => ctx.models.has(goStruct.name))
419
+ .map((goStruct) => buildRawModel(goStruct, ctx));
420
+ if (rawModels.length === 0) {
421
+ const checkedPaths = sources
422
+ .map((source) => source.path)
423
+ .join(', ');
424
+ return err('NO_MODELS_FOUND', `No GORM models were found in: ${checkedPaths}. A model is a struct that embeds gorm.Model, has a field with a ` +
425
+ `gorm:"..." tag, has a field called ID, has a TableName() method, or is the target of an association from another model.`);
426
+ }
427
+ const schema = buildSchema(rawModels, ctx, options);
428
+ return ok(schema);
429
+ }
430
+ // ---------------------------------------------------------------------------
431
+ // Enums
432
+ // ---------------------------------------------------------------------------
433
+ /** `StatusDraft` of type `Status` is the enum member `Draft`. */
434
+ function enumMemberName(constName, typeName) {
435
+ if (constName.startsWith(typeName) && constName.length > typeName.length) {
436
+ const rest = constName.slice(typeName.length).replace(/^_/, '');
437
+ if (/^[A-Z0-9]/.test(rest)) {
438
+ return rest;
439
+ }
440
+ }
441
+ return constName;
442
+ }
443
+ /** Named string and integer types with typed constants become enums. */
444
+ function buildEnums(ctx, constList) {
445
+ for (const namedType of ctx.namedTypes.values()) {
446
+ const underlying = namedType.underlying;
447
+ if (underlying.kind !== 'name' || underlying.qualifier !== undefined) {
448
+ continue;
449
+ }
450
+ const members = constList.filter((candidate) => candidate.typeName === namedType.name && candidate.value !== undefined);
451
+ if (members.length === 0) {
452
+ continue;
453
+ }
454
+ if (underlying.name === 'string') {
455
+ const values = [];
456
+ for (const member of members) {
457
+ if (member.value?.kind === 'string') {
458
+ values.push({
459
+ name: enumMemberName(member.name, namedType.name),
460
+ dbValue: member.value.value,
461
+ });
462
+ }
463
+ }
464
+ if (values.length > 0) {
465
+ ctx.enums.set(namedType.name, {
466
+ name: namedType.name,
467
+ backing: 'string',
468
+ values,
469
+ });
470
+ }
471
+ }
472
+ else if (underlying.name in INTEGER_KINDS) {
473
+ ctx.enums.set(namedType.name, {
474
+ name: namedType.name,
475
+ backing: 'int',
476
+ values: [],
477
+ });
478
+ }
479
+ }
480
+ }
481
+ // ---------------------------------------------------------------------------
482
+ // Which structs are models
483
+ // ---------------------------------------------------------------------------
484
+ /** The struct an embedded field refers to, including GORM's own `gorm.Model`. */
485
+ function embeddedStruct(type, ctx) {
486
+ let current = type;
487
+ while (current.kind === 'pointer') {
488
+ current = current.elem;
489
+ }
490
+ if (current.kind !== 'name') {
491
+ return undefined;
492
+ }
493
+ if (current.name === 'Model' && libraryName(current) === 'gorm') {
494
+ return GORM_MODEL;
495
+ }
496
+ return ctx.structs.get(current.name);
497
+ }
498
+ /** The name of the package a qualified type comes from: the last import path element, or the qualifier as written. */
499
+ function libraryName(type) {
500
+ if (type.kind !== 'name' || type.qualifier === undefined) {
501
+ return '';
502
+ }
503
+ return type.importPath === undefined
504
+ ? type.qualifier
505
+ : defaultImportName(type.importPath);
506
+ }
507
+ function isEmbeddedField(field, entries) {
508
+ return field.name === undefined || entryFlag(entries, 'EMBEDDED') === true;
509
+ }
510
+ /** Every field of a struct including those promoted from embedded structs. */
511
+ function promotedFields(goStruct, ctx, visited = new Set()) {
512
+ if (visited.has(goStruct.name)) {
513
+ return [];
514
+ }
515
+ visited.add(goStruct.name);
516
+ const fields = [];
517
+ for (const field of goStruct.fields) {
518
+ const entries = parseSettings(gormTagOf(field), ';');
519
+ if (isEmbeddedField(field, entries)) {
520
+ const inner = embeddedStruct(field.type, ctx);
521
+ if (inner !== undefined) {
522
+ fields.push(...promotedFields(inner, ctx, visited));
523
+ }
524
+ }
525
+ else {
526
+ fields.push(field);
527
+ }
528
+ }
529
+ return fields;
530
+ }
531
+ function hasGormMarkers(goStruct, ctx, visited = new Set()) {
532
+ if (visited.has(goStruct.name)) {
533
+ return false;
534
+ }
535
+ visited.add(goStruct.name);
536
+ for (const field of goStruct.fields) {
537
+ const entries = parseSettings(gormTagOf(field), ';');
538
+ if (entries.length > 0 && !isIgnored(entries)) {
539
+ return true;
540
+ }
541
+ if (field.name === undefined) {
542
+ const inner = embeddedStruct(field.type, ctx);
543
+ if (inner === GORM_MODEL) {
544
+ return true;
545
+ }
546
+ if (inner !== undefined && hasGormMarkers(inner, ctx, visited)) {
547
+ return true;
548
+ }
549
+ }
550
+ }
551
+ return false;
552
+ }
553
+ /** GORM treats a field called ID as the primary key, so a struct that has one is shaped like a model. */
554
+ function hasIdField(goStruct, ctx) {
555
+ return promotedFields(goStruct, ctx).some((field) => field.name === 'ID' || field.name === 'Id');
556
+ }
557
+ function implementsScanner(name, ctx) {
558
+ const methods = ctx.methods.get(name);
559
+ return methods?.has('Scan') === true && methods.has('Value');
560
+ }
561
+ /** The struct a field's type refers to through pointers and slices, when it is one of the input structs. */
562
+ function referencedStruct(type, ctx) {
563
+ let current = type;
564
+ while (current.kind === 'pointer' ||
565
+ current.kind === 'slice' ||
566
+ current.kind === 'array') {
567
+ current = current.elem;
568
+ }
569
+ if (current.kind !== 'name') {
570
+ return undefined;
571
+ }
572
+ if (current.qualifier !== undefined &&
573
+ `${libraryName(current)}.${current.name}` in KNOWN_TYPES) {
574
+ return undefined;
575
+ }
576
+ const found = ctx.structs.get(current.name);
577
+ return found === undefined || implementsScanner(found.name, ctx)
578
+ ? undefined
579
+ : found;
580
+ }
581
+ /**
582
+ * GORM has no marker for a model, so a struct counts as one when it embeds gorm.Model, has a
583
+ * `gorm:"..."` tag, has a field called ID, has a TableName() method, or is the target of an
584
+ * association from a model. A struct that is only embedded in others (a shared base) is not a
585
+ * model of its own.
586
+ */
587
+ function detectModels(structs, ctx) {
588
+ const embeddedNames = new Set();
589
+ for (const goStruct of structs) {
590
+ for (const field of goStruct.fields) {
591
+ const entries = parseSettings(gormTagOf(field), ';');
592
+ if (isEmbeddedField(field, entries)) {
593
+ const inner = embeddedStruct(field.type, ctx);
594
+ if (inner !== undefined) {
595
+ embeddedNames.add(inner.name);
596
+ }
597
+ }
598
+ }
599
+ }
600
+ const models = new Set();
601
+ for (const goStruct of structs) {
602
+ const hasTableName = ctx.methods.get(goStruct.name)?.has('TableName') === true;
603
+ const isMarked = hasTableName ||
604
+ hasIdField(goStruct, ctx) ||
605
+ hasGormMarkers(goStruct, ctx);
606
+ if (isMarked && (hasTableName || !embeddedNames.has(goStruct.name))) {
607
+ models.add(goStruct.name);
608
+ }
609
+ }
610
+ // Association targets are models too, even without any marker of their own.
611
+ let changed = true;
612
+ while (changed) {
613
+ changed = false;
614
+ for (const name of [...models]) {
615
+ const goStruct = ctx.structs.get(name);
616
+ if (goStruct === undefined) {
617
+ continue;
618
+ }
619
+ for (const field of promotedFields(goStruct, ctx)) {
620
+ const entries = parseSettings(gormTagOf(field), ';');
621
+ if (field.name === undefined ||
622
+ isIgnored(entries) ||
623
+ entryValue(entries, 'TYPE', 'SERIALIZER') !== undefined) {
624
+ continue;
625
+ }
626
+ const target = referencedStruct(field.type, ctx);
627
+ if (target !== undefined && !models.has(target.name)) {
628
+ models.add(target.name);
629
+ changed = true;
630
+ }
631
+ }
632
+ }
633
+ }
634
+ return models;
635
+ }
636
+ // ---------------------------------------------------------------------------
637
+ // Go types to column types
638
+ // ---------------------------------------------------------------------------
639
+ function classifyType(type, ctx, depth = 0) {
640
+ let nullable = false;
641
+ let current = type;
642
+ while (current.kind === 'pointer') {
643
+ nullable = true;
644
+ current = current.elem;
645
+ }
646
+ switch (current.kind) {
647
+ case 'slice':
648
+ case 'array': {
649
+ let element = current.elem;
650
+ while (element.kind === 'pointer') {
651
+ element = element.elem;
652
+ }
653
+ if (element.kind === 'name' &&
654
+ element.qualifier === undefined &&
655
+ (element.name === 'byte' || element.name === 'uint8')) {
656
+ return { kind: 'scalar', type: 'bytes', goKind: 'other', nullable };
657
+ }
658
+ const inner = classifyType(current.elem, ctx, depth + 1);
659
+ if (inner.kind === 'association') {
660
+ return { ...inner, many: true };
661
+ }
662
+ if (inner.kind === 'skip') {
663
+ return inner;
664
+ }
665
+ return {
666
+ ...inner,
667
+ nullable,
668
+ arrayDepth: (inner.arrayDepth ?? 0) + 1,
669
+ needsHint: 'slice',
670
+ };
671
+ }
672
+ case 'map':
673
+ return {
674
+ kind: 'scalar',
675
+ type: 'json',
676
+ goKind: 'other',
677
+ nullable,
678
+ needsHint: 'map',
679
+ };
680
+ case 'struct':
681
+ return {
682
+ kind: 'skip',
683
+ message: 'an inline struct type has no column mapping (declare a named struct and embed it)',
684
+ };
685
+ case 'other':
686
+ return { kind: 'skip' };
687
+ case 'name':
688
+ return classifyNamed(current, nullable, ctx, depth);
689
+ default:
690
+ return { kind: 'skip' };
691
+ }
692
+ }
693
+ function classifyNamed(type, nullable, ctx, depth) {
694
+ const qualified = type.qualifier !== undefined && type.qualifier !== '';
695
+ if (qualified) {
696
+ const key = `${libraryName(type)}.${type.name}`;
697
+ const known = KNOWN_TYPES[key];
698
+ if (known !== undefined) {
699
+ return {
700
+ kind: 'scalar',
701
+ type: known.type,
702
+ goKind: known.goKind,
703
+ nullable: nullable || known.nullable === true,
704
+ ...(known.arrayDepth === undefined
705
+ ? {}
706
+ : { arrayDepth: known.arrayDepth }),
707
+ ...(key === 'gorm.DeletedAt' ? { isSoftDelete: true } : {}),
708
+ };
709
+ }
710
+ if (GENERIC_JSON_TYPES.has(key)) {
711
+ return { kind: 'scalar', type: 'json', goKind: 'other', nullable };
712
+ }
713
+ if (key === 'sql.Null') {
714
+ const argument = type.args[0];
715
+ const inner = argument === undefined
716
+ ? { kind: 'skip' }
717
+ : classifyType(argument, ctx, depth + 1);
718
+ return inner.kind === 'scalar' ? { ...inner, nullable: true } : inner;
719
+ }
720
+ // A type from another package of the same project (models.Post) is looked up by name.
721
+ if (!ctx.structs.has(type.name) && !ctx.namedTypes.has(type.name)) {
722
+ return {
723
+ kind: 'scalar',
724
+ type: 'unsupported',
725
+ goKind: 'other',
726
+ nullable,
727
+ unsupportedType: describeGoType(type),
728
+ };
729
+ }
730
+ }
731
+ if (!qualified) {
732
+ if (type.name === 'string') {
733
+ return { kind: 'scalar', type: 'text', goKind: 'string', nullable };
734
+ }
735
+ if (type.name === 'bool') {
736
+ return { kind: 'scalar', type: 'boolean', goKind: 'bool', nullable };
737
+ }
738
+ if (type.name === 'float32' || type.name === 'float64') {
739
+ return { kind: 'scalar', type: 'float', goKind: 'float', nullable };
740
+ }
741
+ const integer = INTEGER_KINDS[type.name];
742
+ if (integer !== undefined) {
743
+ return {
744
+ kind: 'scalar',
745
+ type: integerType(integer.bits, integer.unsigned),
746
+ goKind: integer.unsigned ? 'uint' : 'int',
747
+ nullable,
748
+ bits: integer.bits,
749
+ unsigned: integer.unsigned,
750
+ };
751
+ }
752
+ if (type.name === 'any' || type.name === 'error') {
753
+ return { kind: 'skip' };
754
+ }
755
+ }
756
+ const namedType = ctx.namedTypes.get(type.name);
757
+ if (namedType !== undefined && depth < 8) {
758
+ const underlying = classifyType(namedType.underlying, ctx, depth + 1);
759
+ if (underlying.kind !== 'scalar') {
760
+ return underlying;
761
+ }
762
+ const enumInfo = ctx.enums.get(type.name);
763
+ return {
764
+ ...underlying,
765
+ nullable: nullable || underlying.nullable,
766
+ ...(enumInfo?.backing === 'string' ? { enumInfo } : {}),
767
+ ...(enumInfo?.backing === 'int' ? { intEnumName: enumInfo.name } : {}),
768
+ };
769
+ }
770
+ const goStruct = ctx.structs.get(type.name);
771
+ if (goStruct !== undefined) {
772
+ if (implementsScanner(goStruct.name, ctx)) {
773
+ return {
774
+ kind: 'scalar',
775
+ type: 'unsupported',
776
+ goKind: 'other',
777
+ nullable,
778
+ unsupportedType: goStruct.name,
779
+ needsHint: 'scanner',
780
+ };
781
+ }
782
+ return { kind: 'association', target: goStruct.name, many: false };
783
+ }
784
+ return {
785
+ kind: 'scalar',
786
+ type: 'unsupported',
787
+ goKind: 'other',
788
+ nullable,
789
+ unsupportedType: describeGoType(type),
790
+ };
791
+ }
792
+ /** Maps a `type:` tag (a SQL type such as `varchar(100)` or `numeric(10,2)`) to a column type. */
793
+ function readDbType(raw) {
794
+ let text = raw.trim().toLowerCase().replace(/\s+/g, ' ');
795
+ let arrayDepth = 0;
796
+ while (text.endsWith('[]')) {
797
+ arrayDepth += 1;
798
+ text = text.slice(0, -2).trim();
799
+ }
800
+ const argumentMatch = text.match(/\(([^)]*)\)/);
801
+ const args = (argumentMatch?.[1] ?? '')
802
+ .split(',')
803
+ .map((item) => item.trim())
804
+ .filter((item) => item !== '');
805
+ const base = text
806
+ .replace(/\([^)]*\)/g, '')
807
+ .replace(/\s+/g, ' ')
808
+ .trim();
809
+ const first = args[0] !== undefined && /^\d+$/.test(args[0])
810
+ ? Number.parseInt(args[0], 10)
811
+ : undefined;
812
+ const second = args[1] !== undefined && /^\d+$/.test(args[1])
813
+ ? Number.parseInt(args[1], 10)
814
+ : undefined;
815
+ const withArray = (result) => arrayDepth > 0 ? { ...result, arrayDepth } : result;
816
+ if (/^(varchar|character varying|nvarchar|varchar2|nvarchar2|char|character|nchar|bpchar)\b/.test(base)) {
817
+ return withArray({
818
+ type: 'string',
819
+ ...(first === undefined ? {} : { maxLength: first }),
820
+ });
821
+ }
822
+ if (/^(text|tinytext|mediumtext|longtext|ntext|clob|citext)$/.test(base)) {
823
+ return withArray({ type: 'text' });
824
+ }
825
+ if (/^(bool|boolean)$/.test(base) || (base === 'tinyint' && first === 1)) {
826
+ return withArray({ type: 'boolean' });
827
+ }
828
+ if (/^bit$/.test(base) && (first === undefined || first === 1)) {
829
+ return withArray({ type: 'boolean' });
830
+ }
831
+ if (/^(bigint|int8|bigserial|serial8)( unsigned)?$/.test(base)) {
832
+ return withArray({
833
+ type: 'bigInt',
834
+ ...(base.includes('serial') ? { isSerial: true } : {}),
835
+ });
836
+ }
837
+ if (/^(smallint|int2|tinyint|mediumint|integer|int|int4|serial|serial4|smallserial|serial2)( unsigned)?$/.test(base)) {
838
+ return withArray({
839
+ type: 'int',
840
+ ...(base.includes('serial') ? { isSerial: true } : {}),
841
+ });
842
+ }
843
+ if (/^(numeric|decimal|dec|number|money)$/.test(base)) {
844
+ return withArray({
845
+ type: 'decimal',
846
+ ...(first === undefined ? {} : { maxDigits: first }),
847
+ ...(second === undefined ? {} : { decimalPlaces: second }),
848
+ });
849
+ }
850
+ if (/^(real|float|float4|float8|double precision|double|binary_float|binary_double)$/.test(base)) {
851
+ return withArray({ type: 'float' });
852
+ }
853
+ if (/^(timestamp|timestamptz|datetime|datetime2|smalldatetime|datetimeoffset)\b/.test(base)) {
854
+ return withArray({ type: 'dateTime' });
855
+ }
856
+ if (base === 'date') {
857
+ return withArray({ type: 'date' });
858
+ }
859
+ if (/^time\b/.test(base) || base === 'timetz') {
860
+ return withArray({ type: 'time' });
861
+ }
862
+ if (base === 'interval') {
863
+ return withArray({ type: 'duration' });
864
+ }
865
+ if (base === 'uuid' || base === 'uniqueidentifier') {
866
+ return withArray({ type: 'uuid' });
867
+ }
868
+ if (base === 'json' || base === 'jsonb') {
869
+ return withArray({ type: 'json' });
870
+ }
871
+ if (/^(bytea|blob|tinyblob|mediumblob|longblob|varbinary|binary|image|raw)$/.test(base)) {
872
+ return withArray({ type: 'bytes' });
873
+ }
874
+ if (base === 'inet' || base === 'cidr') {
875
+ return withArray({ type: 'ipAddress' });
876
+ }
877
+ if (base === 'hstore') {
878
+ return withArray({ type: 'hstore' });
879
+ }
880
+ return withArray({ type: 'unsupported', unsupportedType: raw.trim() });
881
+ }
882
+ // ---------------------------------------------------------------------------
883
+ // Columns
884
+ // ---------------------------------------------------------------------------
885
+ function parseIndexTags(tagText) {
886
+ const indexTags = [];
887
+ for (const piece of tagText.split(';')) {
888
+ if (piece === '') {
889
+ continue;
890
+ }
891
+ const parts = piece.split(':');
892
+ const key = (parts[0] ?? '').trim().toUpperCase();
893
+ if (key !== 'INDEX' && key !== 'UNIQUEINDEX') {
894
+ continue;
895
+ }
896
+ const tag = parts.slice(1).join(':');
897
+ const comma = tag.indexOf(',');
898
+ const name = comma === -1 ? tag : tag.slice(0, comma);
899
+ const settings = parseSettings(comma === -1 ? '' : tag.slice(comma + 1), ',');
900
+ const className = (entryValue(settings, 'CLASS') ?? '').toUpperCase();
901
+ const priority = entryNumber(settings, 'PRIORITY') ?? 10;
902
+ const sort = (entryValue(settings, 'SORT') ?? '').toLowerCase();
903
+ const method = entryValue(settings, 'TYPE') ?? '';
904
+ const composite = entryValue(settings, 'COMPOSITE');
905
+ indexTags.push({
906
+ isUnique: key === 'UNIQUEINDEX' ||
907
+ entryValue(settings, 'UNIQUE') !== undefined ||
908
+ className === 'UNIQUE',
909
+ name: name === '' ? undefined : name,
910
+ composite,
911
+ priority,
912
+ sort: sort === 'desc' ? 'desc' : sort === 'asc' ? 'asc' : undefined,
913
+ length: entryNumber(settings, 'LENGTH'),
914
+ kind: className === 'FULLTEXT' ? 'fulltext' : undefined,
915
+ method: method === '' ? undefined : method,
916
+ hasWhere: entryValue(settings, 'WHERE') !== undefined,
917
+ hasExpression: entryValue(settings, 'EXPRESSION') !== undefined,
918
+ });
919
+ }
920
+ return indexTags;
921
+ }
922
+ const INDEX_METHODS = {
923
+ btree: 'BTree',
924
+ hash: 'Hash',
925
+ gin: 'Gin',
926
+ gist: 'Gist',
927
+ spgist: 'SpGist',
928
+ brin: 'Brin',
929
+ };
930
+ function stripQuotes(value) {
931
+ const trimmed = value.trim();
932
+ if (trimmed.length >= 2 &&
933
+ ((trimmed.startsWith("'") && trimmed.endsWith("'")) ||
934
+ (trimmed.startsWith('"') && trimmed.endsWith('"')))) {
935
+ return trimmed.slice(1, -1).replace(/''/g, "'");
936
+ }
937
+ return trimmed;
938
+ }
939
+ function isQuoted(value) {
940
+ return stripQuotes(value) !== value.trim();
941
+ }
942
+ /** Converts the value of a `default:` tag. Returns undefined when the column has no default. */
943
+ function convertDefault(raw, field, enumInfo, location, ctx) {
944
+ const trimmed = raw.trim();
945
+ if (trimmed === '' || trimmed.toLowerCase() === 'null' || trimmed === '(-)') {
946
+ return undefined;
947
+ }
948
+ if (enumInfo !== undefined) {
949
+ const text = stripQuotes(trimmed);
950
+ const member = enumInfo.values.find((candidate) => candidate.dbValue === text);
951
+ if (member !== undefined) {
952
+ return { kind: 'enumValue', value: member.name };
953
+ }
954
+ ctx.warnings.push(`${location}: the default "${text}" is not a value of the enum "${enumInfo.name}"; it was kept as a literal.`);
955
+ return { kind: 'literal', value: text };
956
+ }
957
+ // A function call (or any expression with parentheses) is passed to the database as written.
958
+ if (!isQuoted(trimmed) && trimmed.includes('(') && trimmed.includes(')')) {
959
+ if (UUID_SQL.test(trimmed)) {
960
+ return { kind: 'uuid' };
961
+ }
962
+ if (NOW_SQL.test(trimmed)) {
963
+ return { kind: 'now' };
964
+ }
965
+ return { kind: 'dbExpression', expression: trimmed };
966
+ }
967
+ const unquoted = stripQuotes(trimmed);
968
+ switch (field.type) {
969
+ case 'boolean':
970
+ if (/^(1|t|true)$/i.test(unquoted)) {
971
+ return { kind: 'literal', value: true };
972
+ }
973
+ if (/^(0|f|false)$/i.test(unquoted)) {
974
+ return { kind: 'literal', value: false };
975
+ }
976
+ break;
977
+ case 'int':
978
+ case 'bigInt':
979
+ case 'float':
980
+ case 'decimal':
981
+ if (/^-?\d+(\.\d+)?$/.test(unquoted)) {
982
+ return { kind: 'literal', value: Number(unquoted) };
983
+ }
984
+ return { kind: 'dbExpression', expression: trimmed };
985
+ case 'dateTime':
986
+ case 'date':
987
+ case 'time':
988
+ if (NOW_SQL.test(unquoted) || /^current_(date|time)$/i.test(unquoted)) {
989
+ return { kind: 'now' };
990
+ }
991
+ break;
992
+ case 'uuid':
993
+ if (UUID_SQL.test(unquoted)) {
994
+ return { kind: 'uuid' };
995
+ }
996
+ break;
997
+ default:
998
+ break;
999
+ }
1000
+ return { kind: 'literal', value: unquoted };
1001
+ }
1002
+ /** Builds the column for a scalar field, or undefined when the field is not stored. */
1003
+ function buildColumn(structName, goField, entries, scalar, scope) {
1004
+ const ctx = scope.ctx;
1005
+ const location = `${structName}.${goField.name}`;
1006
+ const warnings = ctx.warnings;
1007
+ let type = scalar.type;
1008
+ let maxLength;
1009
+ let maxDigits;
1010
+ let decimalPlaces;
1011
+ let arrayDepth = scalar.arrayDepth;
1012
+ let unsupportedType = scalar.unsupportedType;
1013
+ let enumInfo = scalar.enumInfo;
1014
+ let isSerial = false;
1015
+ let goKind = scalar.goKind;
1016
+ const typeTag = entryValue(entries, 'TYPE');
1017
+ const serializer = entryValue(entries, 'SERIALIZER')?.toLowerCase();
1018
+ if (typeTag !== undefined) {
1019
+ const dbType = readDbType(typeTag);
1020
+ type = dbType.type;
1021
+ maxLength = dbType.maxLength;
1022
+ maxDigits = dbType.maxDigits;
1023
+ decimalPlaces = dbType.decimalPlaces;
1024
+ arrayDepth = dbType.arrayDepth;
1025
+ unsupportedType = dbType.unsupportedType;
1026
+ isSerial = dbType.isSerial === true;
1027
+ if (dbType.type === 'unsupported') {
1028
+ warnings.push(`${location}: the column type "${typeTag}" has no equivalent in the shared model; it was kept as an unsupported column.`);
1029
+ }
1030
+ if (enumInfo !== undefined && type !== 'string' && type !== 'text') {
1031
+ warnings.push(`${location}: the column type "${typeTag}" is not a string type, so the enum "${enumInfo.name}" was not applied.`);
1032
+ enumInfo = undefined;
1033
+ }
1034
+ }
1035
+ else if (serializer !== undefined) {
1036
+ arrayDepth = undefined;
1037
+ unsupportedType = undefined;
1038
+ enumInfo = undefined;
1039
+ if (serializer === 'json') {
1040
+ type = 'json';
1041
+ }
1042
+ else if (serializer === 'gob') {
1043
+ type = 'bytes';
1044
+ }
1045
+ else if (serializer === 'unixtime') {
1046
+ type = 'bigInt';
1047
+ }
1048
+ else {
1049
+ type = 'unsupported';
1050
+ unsupportedType = `serializer:${serializer}`;
1051
+ warnings.push(`${location}: the serializer "${serializer}" is custom; the column was kept as an unsupported column.`);
1052
+ }
1053
+ }
1054
+ else if (scalar.needsHint !== undefined) {
1055
+ if (scalar.needsHint === 'scanner') {
1056
+ warnings.push(`${location}: "${scalar.unsupportedType ?? ''}" implements Scanner and Valuer, so GORM stores it as a column of a type ` +
1057
+ `only the database knows; add a type:... tag to set it. The column was kept as an unsupported column.`);
1058
+ }
1059
+ else {
1060
+ warnings.push(`${location}: a ${scalar.needsHint} column needs a type:... or serializer:... tag for GORM to migrate it; ` +
1061
+ `it was read as ${scalar.needsHint === 'map' ? 'a json column' : `an array of ${type}`}.`);
1062
+ }
1063
+ }
1064
+ else if (type === 'unsupported') {
1065
+ warnings.push(`${location}: the Go type "${unsupportedType ?? ''}" has no column mapping; add a type:... tag to choose one. ` +
1066
+ `It was kept as an unsupported column.`);
1067
+ }
1068
+ if (scalar.intEnumName !== undefined) {
1069
+ warnings.push(`${location}: the type "${scalar.intEnumName}" has integer constants, which the shared model cannot represent as an enum; ` +
1070
+ `the column was converted to a plain integer.`);
1071
+ }
1072
+ // size, precision and scale tags -----------------------------------------
1073
+ const size = entryNumber(entries, 'SIZE');
1074
+ const precision = entryNumber(entries, 'PRECISION');
1075
+ const scale = entryNumber(entries, 'SCALE');
1076
+ if (typeTag === undefined && serializer === undefined) {
1077
+ if (size !== undefined &&
1078
+ size > 0 &&
1079
+ (type === 'text' || type === 'string') &&
1080
+ goKind === 'string') {
1081
+ type = 'string';
1082
+ maxLength = size;
1083
+ }
1084
+ else if (size !== undefined &&
1085
+ size > 0 &&
1086
+ (goKind === 'int' || goKind === 'uint') &&
1087
+ (type === 'int' || type === 'bigInt')) {
1088
+ type = integerType(size, goKind === 'uint');
1089
+ }
1090
+ }
1091
+ if (precision !== undefined &&
1092
+ precision > 0 &&
1093
+ (type === 'decimal' || type === 'float')) {
1094
+ // GORM stores a float with a precision as numeric(precision, scale).
1095
+ type = 'decimal';
1096
+ maxDigits = precision;
1097
+ decimalPlaces = scale;
1098
+ goKind = 'float';
1099
+ }
1100
+ else if (type === 'decimal' && scale !== undefined) {
1101
+ decimalPlaces = scale;
1102
+ }
1103
+ if (enumInfo !== undefined && type === 'text') {
1104
+ type = 'string';
1105
+ }
1106
+ const columnName = scope.columnPrefix +
1107
+ (entryValue(entries, 'COLUMN') ?? gormColumnName(goField.name));
1108
+ const irName = irFieldName(scope.namePrefix + goField.name);
1109
+ // Primary key, nullability, uniqueness -----------------------------------
1110
+ const isPrimary = (entryFlag(entries, 'PRIMARYKEY', 'PRIMARY_KEY') ?? false) === true;
1111
+ const notNull = entryFlag(entries, 'NOT NULL', 'NOTNULL') === true;
1112
+ const isNullable = scalar.nullable && !notNull && !isPrimary;
1113
+ // Timestamps GORM maintains ----------------------------------------------
1114
+ const isTimeLike = goKind === 'time';
1115
+ const isIntegerLike = goKind === 'int' || goKind === 'uint';
1116
+ const createTag = entryFlag(entries, 'AUTOCREATETIME');
1117
+ const updateTag = entryFlag(entries, 'AUTOUPDATETIME');
1118
+ const autoCreate = createTag ??
1119
+ (goField.name === 'CreatedAt' && (isTimeLike || isIntegerLike));
1120
+ const autoUpdate = updateTag ??
1121
+ (goField.name === 'UpdatedAt' && (isTimeLike || isIntegerLike));
1122
+ if ((autoCreate || autoUpdate) &&
1123
+ isIntegerLike &&
1124
+ (createTag === true || updateTag === true)) {
1125
+ warnings.push(`${location}: ${autoCreate ? 'autoCreateTime' : 'autoUpdateTime'} on an integer field stores Unix time; ` +
1126
+ `it was read as a plain integer column.`);
1127
+ }
1128
+ const field = {
1129
+ name: irName,
1130
+ columnName,
1131
+ type,
1132
+ isPrimaryKey: isPrimary,
1133
+ isUnique: !isPrimary && entryFlag(entries, 'UNIQUE') === true,
1134
+ isNullable,
1135
+ isAutoUpdated: autoUpdate && isTimeLike,
1136
+ ...(maxLength === undefined || type !== 'string' ? {} : { maxLength }),
1137
+ ...(type === 'decimal' && maxDigits !== undefined ? { maxDigits } : {}),
1138
+ ...(type === 'decimal' && decimalPlaces !== undefined
1139
+ ? { decimalPlaces }
1140
+ : {}),
1141
+ ...(arrayDepth === undefined || arrayDepth === 0 ? {} : { arrayDepth }),
1142
+ ...(enumInfo === undefined ? {} : { enumName: enumInfo.name }),
1143
+ ...(type === 'unsupported' && unsupportedType !== undefined
1144
+ ? { unsupportedType }
1145
+ : {}),
1146
+ };
1147
+ // Defaults ---------------------------------------------------------------
1148
+ const defaultTag = entryValue(entries, 'DEFAULT');
1149
+ const autoIncrementTag = entryFlag(entries, 'AUTOINCREMENT');
1150
+ if (defaultTag !== undefined) {
1151
+ const converted = convertDefault(defaultTag, field, enumInfo, location, ctx);
1152
+ if (converted !== undefined) {
1153
+ field.default = converted;
1154
+ }
1155
+ }
1156
+ else if (autoCreate && isTimeLike) {
1157
+ field.default = { kind: 'now' };
1158
+ }
1159
+ return {
1160
+ kind: 'column',
1161
+ goName: goField.name,
1162
+ field,
1163
+ entries,
1164
+ indexTags: parseIndexTags(gormTagOf(goField)),
1165
+ isPrimary,
1166
+ autoIncrementTag: isSerial ? (autoIncrementTag ?? true) : autoIncrementTag,
1167
+ goKind,
1168
+ depth: scope.depth,
1169
+ };
1170
+ }
1171
+ // ---------------------------------------------------------------------------
1172
+ // Structs to raw models
1173
+ // ---------------------------------------------------------------------------
1174
+ function collectMembers(goStruct, structName, scope, trail, soft) {
1175
+ const ctx = scope.ctx;
1176
+ const members = [];
1177
+ for (const field of goStruct.fields) {
1178
+ const entries = parseSettings(gormTagOf(field), ';');
1179
+ if (isIgnored(entries)) {
1180
+ continue;
1181
+ }
1182
+ if (isEmbeddedField(field, entries)) {
1183
+ const inner = embeddedStruct(field.type, ctx);
1184
+ const label = `${structName}.${field.name ?? describeGoType(field.type)}`;
1185
+ if (inner === undefined) {
1186
+ // An embedded scalar-like type (for example time.Time) is a column; only structs are expanded.
1187
+ if (field.name === undefined) {
1188
+ ctx.warnings.push(`${label}: the embedded type is not declared in the input files, so its columns are missing. ` +
1189
+ `Add the file that defines it to --input.`);
1190
+ continue;
1191
+ }
1192
+ }
1193
+ else {
1194
+ if (trail.has(inner.name)) {
1195
+ ctx.warnings.push(`${label}: the embedded struct "${inner.name}" contains itself; the recursion was cut.`);
1196
+ continue;
1197
+ }
1198
+ const prefix = entryValue(entries, 'EMBEDDEDPREFIX') ?? '';
1199
+ members.push(...collectMembers(inner, structName, {
1200
+ ctx,
1201
+ columnPrefix: scope.columnPrefix + prefix,
1202
+ namePrefix: scope.namePrefix + (prefix === '' ? '' : toCamelCase(prefix)),
1203
+ depth: scope.depth + 1,
1204
+ }, new Set([...trail, inner.name]), soft));
1205
+ continue;
1206
+ }
1207
+ }
1208
+ const goName = field.name;
1209
+ if (goName === undefined || !isUpperAscii(goName.charAt(0))) {
1210
+ // Unexported fields are not part of the schema.
1211
+ continue;
1212
+ }
1213
+ const location = `${structName}.${goName}`;
1214
+ const hasColumnTag = entryValue(entries, 'TYPE', 'SERIALIZER') !== undefined;
1215
+ const classified = classifyType(field.type, ctx);
1216
+ if (classified.kind === 'skip') {
1217
+ if (classified.message !== undefined) {
1218
+ ctx.warnings.push(`${location}: ${classified.message}; it was skipped.`);
1219
+ }
1220
+ continue;
1221
+ }
1222
+ if (classified.kind === 'association') {
1223
+ if (hasColumnTag) {
1224
+ // An explicit column type makes the field a column, not an association.
1225
+ const column = buildColumn(structName, { ...field, name: goName }, entries, {
1226
+ kind: 'scalar',
1227
+ type: 'json',
1228
+ goKind: 'other',
1229
+ nullable: field.type.kind === 'pointer',
1230
+ }, scope);
1231
+ members.push(column);
1232
+ continue;
1233
+ }
1234
+ members.push({
1235
+ kind: 'association',
1236
+ goName,
1237
+ irName: irFieldName(scope.namePrefix + goName),
1238
+ target: classified.target,
1239
+ many: classified.many,
1240
+ entries,
1241
+ depth: scope.depth,
1242
+ });
1243
+ continue;
1244
+ }
1245
+ if (classified.isSoftDelete === true && soft.field === undefined) {
1246
+ soft.field = goName;
1247
+ }
1248
+ members.push(buildColumn(structName, { ...field, name: goName }, entries, classified, scope));
1249
+ }
1250
+ return members;
1251
+ }
1252
+ /** Table name from a TableName() method, or GORM's default. */
1253
+ function resolveTableName(goStruct, ctx) {
1254
+ const method = ctx.methods
1255
+ .get(goStruct.name)
1256
+ ?.get('TableName');
1257
+ const fallback = gormTableName(goStruct.name);
1258
+ if (method === undefined) {
1259
+ return fallback;
1260
+ }
1261
+ if (method.returns?.kind === 'string') {
1262
+ return method.returns.value;
1263
+ }
1264
+ if (method.returns?.kind === 'identifier') {
1265
+ const constant = ctx.consts.get(method.returns.name);
1266
+ if (constant?.value?.kind === 'string') {
1267
+ return constant.value.value;
1268
+ }
1269
+ }
1270
+ ctx.warnings.push(`${goStruct.name}: TableName() does not return a string literal or constant, so its value cannot be read statically; ` +
1271
+ `GORM's default table name "${fallback}" was used.`);
1272
+ return fallback;
1273
+ }
1274
+ function buildRawModel(goStruct, ctx) {
1275
+ const soft = { field: undefined };
1276
+ const collected = collectMembers(goStruct, goStruct.name, { ctx, columnPrefix: '', namePrefix: '', depth: 0 }, new Set([goStruct.name]), soft);
1277
+ // A field of the model itself shadows a promoted field of the same name.
1278
+ const members = collected.filter((member, index) => {
1279
+ const name = member.kind === 'column' ? member.field.name : member.irName;
1280
+ return !collected.some((other, otherIndex) => otherIndex !== index &&
1281
+ (other.kind === 'column' ? other.field.name : other.irName) ===
1282
+ name &&
1283
+ (other.depth < member.depth ||
1284
+ (other.depth === member.depth && otherIndex < index)));
1285
+ });
1286
+ const columns = members.filter((member) => member.kind === 'column');
1287
+ let primaryMembers = columns.filter((column) => column.isPrimary);
1288
+ if (primaryMembers.length === 0) {
1289
+ // GORM treats a field called ID as the primary key when none is declared.
1290
+ const implicit = columns.find((column) => column.field.columnName === 'id');
1291
+ if (implicit !== undefined) {
1292
+ implicit.isPrimary = true;
1293
+ implicit.field.isPrimaryKey = true;
1294
+ implicit.field.isNullable = false;
1295
+ implicit.field.isUnique = false;
1296
+ primaryMembers = [implicit];
1297
+ }
1298
+ }
1299
+ return {
1300
+ name: goStruct.name,
1301
+ struct: goStruct,
1302
+ tableName: resolveTableName(goStruct, ctx),
1303
+ members,
1304
+ primaryMembers,
1305
+ softDeleteField: soft.field,
1306
+ };
1307
+ }
1308
+ function readConstraint(member, location, ctx) {
1309
+ const text = entryValue(member.entries, 'CONSTRAINT');
1310
+ if (text === undefined) {
1311
+ return {};
1312
+ }
1313
+ const result = {};
1314
+ const comma = text.indexOf(',');
1315
+ if (comma !== -1 && /^[\w-]+$/.test(text.slice(0, comma))) {
1316
+ result.name = text.slice(0, comma);
1317
+ }
1318
+ const settings = parseSettings(text, ',');
1319
+ for (const [key, target] of [
1320
+ ['ONDELETE', 'onDelete'],
1321
+ ['ONUPDATE', 'onUpdate'],
1322
+ ]) {
1323
+ const value = entryValue(settings, key);
1324
+ if (value === undefined || value === '') {
1325
+ continue;
1326
+ }
1327
+ const mapped = ON_DELETE_MAP[value.toUpperCase()];
1328
+ if (mapped === undefined) {
1329
+ ctx.warnings.push(`${location}: ${target === 'onDelete' ? 'OnDelete' : 'OnUpdate'} "${value}" has no equivalent and was converted to NoAction.`);
1330
+ result[target] = 'noAction';
1331
+ }
1332
+ else {
1333
+ result[target] = mapped;
1334
+ }
1335
+ }
1336
+ return result;
1337
+ }
1338
+ /** GORM looks a field up by column name first, then by Go name. */
1339
+ function lookupMember(model, name) {
1340
+ const columns = model.members.filter((member) => member.kind === 'column');
1341
+ return (columns.find((column) => column.field.columnName === name) ??
1342
+ columns.find((column) => column.goName === name));
1343
+ }
1344
+ /**
1345
+ * Finds the foreign key of an association the way GORM does. For `has`, the key lives on the
1346
+ * target and refers to the declaring model; for `belongs`, it lives on the declaring model.
1347
+ */
1348
+ function guessKeys(mode, declaring, target, member) {
1349
+ const primary = mode === 'has' ? declaring : target;
1350
+ const foreign = mode === 'has' ? target : declaring;
1351
+ const foreignNames = splitList(entryValue(member.entries, 'FOREIGNKEY'));
1352
+ const referenceNames = splitList(entryValue(member.entries, 'REFERENCES'));
1353
+ let primaryFields = primary.primaryMembers;
1354
+ if (referenceNames.length > 0) {
1355
+ const resolved = referenceNames.map((name) => lookupMember(primary, name));
1356
+ if (resolved.some((item) => item === undefined)) {
1357
+ return undefined;
1358
+ }
1359
+ primaryFields = resolved.filter((item) => item !== undefined);
1360
+ }
1361
+ if (foreignNames.length > 0) {
1362
+ const resolved = foreignNames.map((name) => lookupMember(foreign, name));
1363
+ const found = resolved.filter((item) => item !== undefined);
1364
+ if (found.length !== foreignNames.length ||
1365
+ found.length !== primaryFields.length) {
1366
+ return undefined;
1367
+ }
1368
+ return { foreign: found, references: primaryFields };
1369
+ }
1370
+ const foreignFields = [];
1371
+ for (const primaryField of primaryFields) {
1372
+ const base = (mode === 'belongs' ? member.goName : primary.name) + primaryField.goName;
1373
+ const names = [base];
1374
+ if (primaryFields.length === 1) {
1375
+ const stem = base.slice(0, base.length - primaryField.goName.length);
1376
+ names.push(`${stem}ID`, `${stem}Id`, gormColumnName(`${stem}ID`));
1377
+ }
1378
+ let found;
1379
+ for (const name of names) {
1380
+ found = lookupMember(foreign, name);
1381
+ if (found !== undefined) {
1382
+ break;
1383
+ }
1384
+ }
1385
+ if (found === undefined) {
1386
+ return undefined;
1387
+ }
1388
+ foreignFields.push(found);
1389
+ }
1390
+ return foreignFields.length === 0
1391
+ ? undefined
1392
+ : { foreign: foreignFields, references: primaryFields };
1393
+ }
1394
+ /**
1395
+ * The constraint settings that GORM applies to a foreign key. When both the belongs-to field and
1396
+ * the has-one / has-many field describe the key, GORM ignores the belongs-to field's settings.
1397
+ */
1398
+ function effectiveConstraint(link, ctx) {
1399
+ if (link.has === undefined) {
1400
+ return link.belongsConstraint;
1401
+ }
1402
+ const ignored = link.belongsConstraint;
1403
+ if (link.belongsTo !== undefined &&
1404
+ (ignored.onDelete !== undefined ||
1405
+ ignored.onUpdate !== undefined ||
1406
+ ignored.name !== undefined)) {
1407
+ ctx.warnings.push(`${link.belongsTo.model.name}.${link.belongsTo.member.goName}: GORM ignores the constraint:... tag of a belongs-to field when ` +
1408
+ `${link.has.model.name}.${link.has.member.goName} describes the same foreign key; set the constraint on that field instead.`);
1409
+ }
1410
+ return link.hasConstraint;
1411
+ }
1412
+ /** Resolves every association tag into foreign-key links and many-to-many uses. */
1413
+ function resolveAssociations(models, ctx) {
1414
+ const byName = new Map(models.map((model) => [model.name, model]));
1415
+ const links = new Map();
1416
+ const manyToMany = [];
1417
+ for (const model of models) {
1418
+ for (const member of model.members) {
1419
+ if (member.kind !== 'association') {
1420
+ continue;
1421
+ }
1422
+ const location = `${model.name}.${member.goName}`;
1423
+ const target = byName.get(member.target);
1424
+ if (target === undefined) {
1425
+ ctx.warnings.push(`${location}: the target "${member.target}" is not a model in the input; the association was skipped.`);
1426
+ continue;
1427
+ }
1428
+ const polymorphic = entryValue(member.entries, 'POLYMORPHIC');
1429
+ if (polymorphic !== undefined) {
1430
+ ctx.warnings.push(`${location}: the polymorphic association "${polymorphic}" (the ${polymorphic}ID and ${polymorphic}Type columns of "${target.name}") ` +
1431
+ `points at several models, which the shared model cannot represent; the association was dropped and the columns stay ordinary fields.`);
1432
+ continue;
1433
+ }
1434
+ const joinTable = entryValue(member.entries, 'MANY2MANY');
1435
+ if (joinTable !== undefined) {
1436
+ const use = resolveManyToMany(model, member, target, joinTable, location, ctx);
1437
+ if (use !== undefined) {
1438
+ manyToMany.push(use);
1439
+ }
1440
+ continue;
1441
+ }
1442
+ const modes = member.many
1443
+ ? ['has']
1444
+ : model === target
1445
+ ? ['belongs', 'has']
1446
+ : ['has', 'belongs'];
1447
+ let resolvedMode;
1448
+ let keys;
1449
+ for (const mode of modes) {
1450
+ keys = guessKeys(mode, model, target, member);
1451
+ if (keys !== undefined) {
1452
+ resolvedMode = mode;
1453
+ break;
1454
+ }
1455
+ }
1456
+ if (keys === undefined || resolvedMode === undefined) {
1457
+ ctx.warnings.push(`${location}: no foreign key was found for the association to "${target.name}". Add a foreignKey:... tag or a ` +
1458
+ `${member.many ? `${model.name}ID` : `${member.goName}ID`} field; the association was skipped.`);
1459
+ continue;
1460
+ }
1461
+ const owner = resolvedMode === 'has' ? target : model;
1462
+ const referenced = resolvedMode === 'has' ? model : target;
1463
+ const key = `${owner.name}|${keys.foreign
1464
+ .map((column) => column.field.columnName)
1465
+ .join(',')}|${referenced.name}`;
1466
+ const link = links.get(key) ?? {
1467
+ owner,
1468
+ target: referenced,
1469
+ keys,
1470
+ belongsConstraint: {},
1471
+ hasConstraint: {},
1472
+ };
1473
+ links.set(key, link);
1474
+ const constraint = readConstraint(member, location, ctx);
1475
+ if (resolvedMode === 'belongs') {
1476
+ if (link.belongsTo === undefined) {
1477
+ link.belongsTo = { model, member };
1478
+ }
1479
+ }
1480
+ else if (link.has === undefined) {
1481
+ link.has = { model, member };
1482
+ }
1483
+ else {
1484
+ ctx.warnings.push(`${location}: "${link.has.model.name}.${link.has.member.goName}" already describes the same foreign key; this reverse accessor was ignored.`);
1485
+ }
1486
+ if (resolvedMode === 'belongs') {
1487
+ link.belongsConstraint = constraint;
1488
+ }
1489
+ else if (link.has?.member === member) {
1490
+ link.hasConstraint = constraint;
1491
+ }
1492
+ }
1493
+ }
1494
+ return { links: [...links.values()], manyToMany };
1495
+ }
1496
+ function resolveManyToMany(owner, member, target, joinTable, location, ctx) {
1497
+ if (joinTable === '' || joinTable === 'MANY2MANY') {
1498
+ ctx.warnings.push(`${location}: many2many needs a join table name (many2many:table_name); the association was skipped.`);
1499
+ return undefined;
1500
+ }
1501
+ const ownerKeyNames = splitList(entryValue(member.entries, 'FOREIGNKEY'));
1502
+ const targetKeyNames = splitList(entryValue(member.entries, 'REFERENCES'));
1503
+ const ownerKeys = ownerKeyNames.length === 0
1504
+ ? owner.primaryMembers
1505
+ : ownerKeyNames
1506
+ .map((name) => lookupMember(owner, name))
1507
+ .filter((item) => item !== undefined);
1508
+ const targetKeys = targetKeyNames.length === 0
1509
+ ? target.primaryMembers
1510
+ : targetKeyNames
1511
+ .map((name) => lookupMember(target, name))
1512
+ .filter((item) => item !== undefined);
1513
+ const ownerKey = ownerKeys[0];
1514
+ const targetKey = targetKeys[0];
1515
+ if (ownerKey === undefined || targetKey === undefined) {
1516
+ ctx.warnings.push(`${location}: the keys of the many2many association could not be determined (${owner.name} or ${target.name} has no primary key); the association was skipped.`);
1517
+ return undefined;
1518
+ }
1519
+ if (ownerKeys.length > 1 || targetKeys.length > 1) {
1520
+ ctx.warnings.push(`${location}: the many2many association uses composite keys; only the first column of each key is read and the join table "${joinTable}" is not preserved.`);
1521
+ }
1522
+ const joinForeignKey = splitList(entryValue(member.entries, 'JOINFOREIGNKEY'))[0];
1523
+ const joinReferences = splitList(entryValue(member.entries, 'JOINREFERENCES'))[0];
1524
+ const ownerField = joinForeignKey === undefined
1525
+ ? upperFirst(owner.name) + ownerKey.goName
1526
+ : upperFirst(joinForeignKey);
1527
+ let relatedField = joinReferences === undefined
1528
+ ? upperFirst(target.name) + targetKey.goName
1529
+ : upperFirst(joinReferences);
1530
+ if (relatedField === ownerField) {
1531
+ // A self-referencing association names the second column after the field: Friends -> FriendID.
1532
+ relatedField =
1533
+ member.goName !== target.name
1534
+ ? upperFirst(toPascalSingular(member.goName)) + targetKey.goName
1535
+ : `${target.name}Reference${targetKey.goName}`;
1536
+ }
1537
+ if ((ownerKeyNames.length > 0 && !ownerKey.isPrimary) ||
1538
+ (targetKeyNames.length > 0 && !targetKey.isPrimary)) {
1539
+ ctx.warnings.push(`${location}: the join table "${joinTable}" refers to columns that are not primary keys; the relation is read as a many-to-many on the primary keys.`);
1540
+ }
1541
+ return {
1542
+ owner,
1543
+ member,
1544
+ target,
1545
+ table: joinTable,
1546
+ ownerColumn: gormColumnName(ownerField),
1547
+ relatedColumn: gormColumnName(relatedField),
1548
+ consumed: false,
1549
+ };
1550
+ }
1551
+ /** `Friends` becomes `Friend` (the singular of the last word). */
1552
+ function toPascalSingular(goName) {
1553
+ const snake = toSnakeCase(goName);
1554
+ const singular = singularize(snake);
1555
+ return singular
1556
+ .split('_')
1557
+ .map((word) => upperFirst(word))
1558
+ .join('');
1559
+ }
1560
+ function buildIndexGroups(columns) {
1561
+ const groups = new Map();
1562
+ let order = 0;
1563
+ for (const column of columns) {
1564
+ for (const tag of column.indexTags) {
1565
+ const key = tag.name !== undefined
1566
+ ? `name:${tag.name}`
1567
+ : tag.composite !== undefined
1568
+ ? `composite:${tag.composite}`
1569
+ : `field:${column.field.name}:${order}`;
1570
+ const group = groups.get(key) ?? {
1571
+ name: tag.name,
1572
+ isUnique: false,
1573
+ kind: undefined,
1574
+ method: undefined,
1575
+ uses: [],
1576
+ hasWhere: false,
1577
+ hasExpression: false,
1578
+ };
1579
+ group.isUnique = group.isUnique || tag.isUnique;
1580
+ group.kind = group.kind ?? tag.kind;
1581
+ group.method = group.method ?? tag.method;
1582
+ group.hasWhere = group.hasWhere || tag.hasWhere;
1583
+ group.hasExpression = group.hasExpression || tag.hasExpression;
1584
+ group.uses.push({ member: column, tag, order });
1585
+ order += 1;
1586
+ groups.set(key, group);
1587
+ }
1588
+ }
1589
+ return [...groups.values()];
1590
+ }
1591
+ function defaultRelationName(column, taken) {
1592
+ const base = column.field.name;
1593
+ const stripped = /Id$/.test(base) && base.length > 2 ? base.slice(0, -2) : base;
1594
+ return taken.has(stripped) ? `${stripped}Ref` : stripped;
1595
+ }
1596
+ function buildSchema(rawModels, ctx, options) {
1597
+ const warnings = ctx.warnings;
1598
+ const { links, manyToMany } = resolveAssociations(rawModels, ctx);
1599
+ const usedEnums = new Set();
1600
+ const models = [];
1601
+ for (const raw of rawModels) {
1602
+ const columns = raw.members.filter((member) => member.kind === 'column');
1603
+ const takenNames = new Set(raw.members.map((member) => member.kind === 'column' ? member.field.name : member.irName));
1604
+ // Foreign keys: the key columns turn into relations --------------------
1605
+ const consumed = new Map();
1606
+ const orderedRelations = [];
1607
+ const compositeKeys = [];
1608
+ let synthesized = 0;
1609
+ for (const link of links) {
1610
+ if (link.owner !== raw) {
1611
+ continue;
1612
+ }
1613
+ const order = link.belongsTo === undefined
1614
+ ? raw.members.length + synthesized++
1615
+ : raw.members.indexOf(link.belongsTo.member);
1616
+ const first = link.keys.foreign[0];
1617
+ if (first === undefined) {
1618
+ continue;
1619
+ }
1620
+ const constraint = effectiveConstraint(link, ctx);
1621
+ const name = link.belongsTo?.member.irName ?? defaultRelationName(first, takenNames);
1622
+ const isNullable = link.keys.foreign.some((column) => column.field.isNullable);
1623
+ const relatedName = link.has?.member.irName;
1624
+ const kind = link.has !== undefined && !link.has.member.many
1625
+ ? 'oneToOne'
1626
+ : 'foreignKey';
1627
+ if (link.keys.foreign.length > 1) {
1628
+ compositeKeys.push({
1629
+ name,
1630
+ targetModel: link.target.name,
1631
+ fields: link.keys.foreign.map((column) => column.field.name),
1632
+ references: link.keys.references.map((column) => column.field.name),
1633
+ kind,
1634
+ isNullable,
1635
+ onDelete: constraint.onDelete ?? 'noAction',
1636
+ ...(constraint.onUpdate === undefined
1637
+ ? {}
1638
+ : { onUpdate: constraint.onUpdate }),
1639
+ ...(relatedName === undefined ? {} : { relatedName }),
1640
+ ...(constraint.name === undefined
1641
+ ? {}
1642
+ : { constraintName: constraint.name }),
1643
+ });
1644
+ continue;
1645
+ }
1646
+ const reference = link.keys.references[0];
1647
+ const isTargetKey = reference !== undefined &&
1648
+ link.target.primaryMembers.length === 1 &&
1649
+ link.target.primaryMembers[0] === reference;
1650
+ const unique = first.field.isUnique ||
1651
+ first.indexTags.some((tag) => tag.isUnique &&
1652
+ tag.name === undefined &&
1653
+ tag.composite === undefined);
1654
+ const relation = {
1655
+ name,
1656
+ kind: kind === 'foreignKey' && unique ? 'oneToOne' : kind,
1657
+ targetModel: link.target.name,
1658
+ columnName: first.field.columnName,
1659
+ isNullable: first.isPrimary ? false : isNullable,
1660
+ onDelete: constraint.onDelete ?? 'noAction',
1661
+ ...(relatedName === undefined ? {} : { relatedName }),
1662
+ ...(reference === undefined || isTargetKey
1663
+ ? {}
1664
+ : { toField: reference.field.columnName }),
1665
+ ...(first.isPrimary ? { isPrimaryKey: true } : {}),
1666
+ ...(constraint.onUpdate === undefined
1667
+ ? {}
1668
+ : { onUpdate: constraint.onUpdate }),
1669
+ ...(constraint.name === undefined
1670
+ ? {}
1671
+ : { constraintName: constraint.name }),
1672
+ };
1673
+ consumed.set(first, name);
1674
+ orderedRelations.push({ order, relation });
1675
+ }
1676
+ // Many-to-many ----------------------------------------------------------
1677
+ for (const use of manyToMany) {
1678
+ if (use.owner !== raw || use.consumed) {
1679
+ continue;
1680
+ }
1681
+ use.consumed = true;
1682
+ const mirror = manyToMany.find((candidate) => !candidate.consumed &&
1683
+ candidate !== use &&
1684
+ candidate.owner === use.target &&
1685
+ candidate.target === use.owner &&
1686
+ candidate.table.toLowerCase() === use.table.toLowerCase());
1687
+ if (mirror !== undefined) {
1688
+ mirror.consumed = true;
1689
+ }
1690
+ const relation = {
1691
+ name: use.member.irName,
1692
+ kind: 'manyToMany',
1693
+ targetModel: use.target.name,
1694
+ columnName: `${toSnakeCase(use.member.irName)}_id`,
1695
+ isNullable: false,
1696
+ onDelete: 'cascade',
1697
+ ...(mirror === undefined ? {} : { relatedName: mirror.member.irName }),
1698
+ };
1699
+ orderedRelations.push({
1700
+ order: raw.members.indexOf(use.member),
1701
+ relation,
1702
+ });
1703
+ // Other formats derive the join table from the two models; say what does not survive.
1704
+ const isSelf = use.owner === use.target;
1705
+ const expectedTable = `${raw.tableName}_${use.member.irName}`;
1706
+ const expectedOwner = `${isSelf ? 'from_' : ''}${toSnakeCase(use.owner.name)}_id`;
1707
+ const expectedRelated = `${isSelf ? 'to_' : ''}${toSnakeCase(use.target.name)}_id`;
1708
+ if (use.table !== expectedTable ||
1709
+ use.ownerColumn !== expectedOwner ||
1710
+ use.relatedColumn !== expectedRelated) {
1711
+ warnings.push(`${raw.name}.${use.member.goName}: the join table "${use.table}" (columns ${use.ownerColumn}, ${use.relatedColumn}) is not preserved by name; ` +
1712
+ `formats that derive join tables will use "${expectedTable}" (columns ${expectedOwner}, ${expectedRelated}).`);
1713
+ }
1714
+ }
1715
+ const relations = orderedRelations
1716
+ .sort((first, second) => first.order - second.order)
1717
+ .map((entry) => entry.relation);
1718
+ // Fields -----------------------------------------------------------------
1719
+ const fields = columns
1720
+ .filter((column) => !consumed.has(column))
1721
+ .map((column) => ({ ...column.field }));
1722
+ const finalName = (column) => consumed.get(column) ?? column.field.name;
1723
+ const fieldFor = (column) => fields.find((field) => field.name === column.field.name);
1724
+ // Primary key ------------------------------------------------------------
1725
+ const keyColumns = raw.primaryMembers;
1726
+ let compositePrimaryKey;
1727
+ if (keyColumns.length === 0) {
1728
+ warnings.push(`${raw.name}: the struct has no primary key (GORM treats a field called ID, or a primaryKey tag, as one); ` +
1729
+ `other formats expect every model to have a key.`);
1730
+ }
1731
+ else if (keyColumns.length > 1) {
1732
+ compositePrimaryKey = keyColumns.map(finalName);
1733
+ for (const column of keyColumns) {
1734
+ const field = fieldFor(column);
1735
+ if (field !== undefined) {
1736
+ field.isPrimaryKey = false;
1737
+ field.isUnique = false;
1738
+ }
1739
+ }
1740
+ for (const relation of relations) {
1741
+ delete relation.isPrimaryKey;
1742
+ }
1743
+ }
1744
+ // The prioritized primary key of an integer type auto-increments unless told otherwise.
1745
+ for (const column of columns) {
1746
+ const field = fieldFor(column);
1747
+ if (field === undefined || field.default?.kind === 'autoIncrement') {
1748
+ continue;
1749
+ }
1750
+ const isInteger = field.type === 'int' || field.type === 'bigInt';
1751
+ const isOnlyKey = keyColumns.length === 1 && keyColumns[0] === column;
1752
+ if (isInteger &&
1753
+ field.arrayDepth === undefined &&
1754
+ (column.autoIncrementTag === true ||
1755
+ (isOnlyKey && column.autoIncrementTag === undefined)) &&
1756
+ field.default === undefined) {
1757
+ field.default = { kind: 'autoIncrement' };
1758
+ }
1759
+ }
1760
+ // Indexes ----------------------------------------------------------------
1761
+ const irIndexes = [];
1762
+ for (const group of buildIndexGroups(columns)) {
1763
+ const label = group.name === undefined ? '' : ` "${group.name}"`;
1764
+ const sorted = [...group.uses].sort((first, second) => first.tag.priority - second.tag.priority || first.order - second.order);
1765
+ if (group.hasExpression) {
1766
+ warnings.push(`${raw.name}: the index${label} on (${sorted.map((use) => use.member.goName).join(', ')}) uses an expression, which cannot be represented; it was skipped.`);
1767
+ continue;
1768
+ }
1769
+ if (group.hasWhere) {
1770
+ warnings.push(`${raw.name}: the index${label} has a where condition (a partial index), which is not preserved; it was read as a full index.`);
1771
+ }
1772
+ const names = sorted.map((use) => finalName(use.member));
1773
+ const only = sorted[0];
1774
+ if (names.length === 1 &&
1775
+ only !== undefined &&
1776
+ group.isUnique &&
1777
+ group.name === undefined &&
1778
+ group.kind === undefined &&
1779
+ only.tag.sort === undefined &&
1780
+ only.tag.length === undefined) {
1781
+ const field = fieldFor(only.member);
1782
+ if (field !== undefined) {
1783
+ if (!field.isPrimaryKey) {
1784
+ field.isUnique = true;
1785
+ }
1786
+ continue;
1787
+ }
1788
+ if (consumed.has(only.member)) {
1789
+ // The unique key is a foreign key: the relation already carries it (one-to-one).
1790
+ continue;
1791
+ }
1792
+ }
1793
+ const fieldOptions = {};
1794
+ for (const use of sorted) {
1795
+ const optionSet = {
1796
+ ...(use.tag.sort === undefined ? {} : { sort: use.tag.sort }),
1797
+ ...(use.tag.length === undefined ? {} : { length: use.tag.length }),
1798
+ };
1799
+ if (Object.keys(optionSet).length > 0) {
1800
+ fieldOptions[finalName(use.member)] = optionSet;
1801
+ }
1802
+ }
1803
+ const method = group.method === undefined
1804
+ ? undefined
1805
+ : INDEX_METHODS[group.method.toLowerCase()];
1806
+ if (group.method !== undefined && method === undefined) {
1807
+ warnings.push(`${raw.name}: the index method "${group.method}" has no equivalent and was ignored.`);
1808
+ }
1809
+ irIndexes.push({
1810
+ fields: names,
1811
+ isUnique: group.isUnique,
1812
+ ...(group.name === undefined ? {} : { name: group.name }),
1813
+ ...(group.kind === undefined ? {} : { kind: group.kind }),
1814
+ ...(method === undefined ? {} : { method }),
1815
+ ...(Object.keys(fieldOptions).length === 0 ? {} : { fieldOptions }),
1816
+ });
1817
+ }
1818
+ // Settings with no equivalent -----------------------------------------------
1819
+ for (const column of columns) {
1820
+ const check = entryValue(column.entries, 'CHECK');
1821
+ if (check !== undefined) {
1822
+ warnings.push(`${raw.name}.${column.goName}: the check constraint "${check}" has no equivalent in the shared model and was dropped.`);
1823
+ }
1824
+ const comment = entryValue(column.entries, 'COMMENT');
1825
+ if (comment !== undefined) {
1826
+ warnings.push(`${raw.name}.${column.goName}: the column comment is not preserved.`);
1827
+ }
1828
+ if (column.field.enumName !== undefined) {
1829
+ usedEnums.add(column.field.enumName);
1830
+ }
1831
+ }
1832
+ if (raw.softDeleteField !== undefined) {
1833
+ warnings.push(`${raw.name}.${raw.softDeleteField}: gorm.DeletedAt is read as a nullable timestamp column; the soft-delete behavior ` +
1834
+ `(GORM hides rows with a deletion time from queries) has no equivalent in the shared model.`);
1835
+ }
1836
+ models.push({
1837
+ name: raw.name,
1838
+ tableName: raw.tableName,
1839
+ appLabel: options.appLabel,
1840
+ fields,
1841
+ relations,
1842
+ indexes: irIndexes,
1843
+ ...(compositePrimaryKey === undefined ? {} : { compositePrimaryKey }),
1844
+ ...(compositeKeys.length === 0
1845
+ ? {}
1846
+ : { compositeForeignKeys: compositeKeys }),
1847
+ });
1848
+ }
1849
+ const enums = [...ctx.enums.values()]
1850
+ .filter((info) => info.backing === 'string' && usedEnums.has(info.name))
1851
+ .map((info) => ({
1852
+ name: info.name,
1853
+ values: info.values,
1854
+ }));
1855
+ return { models, enums, warnings };
1856
+ }
1857
+ //# sourceMappingURL=gorm.js.map