ormbridge 0.0.0-stage → 0.2.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +408 -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 +343 -0
  8. package/dist/cliRunner.js.map +1 -0
  9. package/dist/config.d.ts +40 -0
  10. package/dist/config.js +204 -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/graphene.d.ts +4 -0
  22. package/dist/emitters/graphene.js +431 -0
  23. package/dist/emitters/graphene.js.map +1 -0
  24. package/dist/emitters/laravel.d.ts +33 -0
  25. package/dist/emitters/laravel.js +1695 -0
  26. package/dist/emitters/laravel.js.map +1 -0
  27. package/dist/emitters/prisma.d.ts +21 -0
  28. package/dist/emitters/prisma.js +1058 -0
  29. package/dist/emitters/prisma.js.map +1 -0
  30. package/dist/emitters/typeorm.d.ts +9 -0
  31. package/dist/emitters/typeorm.js +833 -0
  32. package/dist/emitters/typeorm.js.map +1 -0
  33. package/dist/emitters/typescriptInterfaces.d.ts +15 -0
  34. package/dist/emitters/typescriptInterfaces.js +350 -0
  35. package/dist/emitters/typescriptInterfaces.js.map +1 -0
  36. package/dist/formats.d.ts +80 -0
  37. package/dist/formats.js +212 -0
  38. package/dist/formats.js.map +1 -0
  39. package/dist/index.d.ts +33 -0
  40. package/dist/index.js +18 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/io.d.ts +48 -0
  43. package/dist/io.js +328 -0
  44. package/dist/io.js.map +1 -0
  45. package/dist/ir.d.ts +197 -0
  46. package/dist/ir.js +10 -0
  47. package/dist/ir.js.map +1 -0
  48. package/dist/naming.d.ts +11 -0
  49. package/dist/naming.js +88 -0
  50. package/dist/naming.js.map +1 -0
  51. package/dist/parsers/django.d.ts +14 -0
  52. package/dist/parsers/django.js +1161 -0
  53. package/dist/parsers/django.js.map +1 -0
  54. package/dist/parsers/doctrine.d.ts +12 -0
  55. package/dist/parsers/doctrine.js +1782 -0
  56. package/dist/parsers/doctrine.js.map +1 -0
  57. package/dist/parsers/laravel.d.ts +17 -0
  58. package/dist/parsers/laravel.js +2824 -0
  59. package/dist/parsers/laravel.js.map +1 -0
  60. package/dist/parsers/phpSyntax.d.ts +124 -0
  61. package/dist/parsers/phpSyntax.js +429 -0
  62. package/dist/parsers/phpSyntax.js.map +1 -0
  63. package/dist/parsers/prisma.d.ts +12 -0
  64. package/dist/parsers/prisma.js +964 -0
  65. package/dist/parsers/prisma.js.map +1 -0
  66. package/dist/parsers/pythonSyntax.d.ts +38 -0
  67. package/dist/parsers/pythonSyntax.js +120 -0
  68. package/dist/parsers/pythonSyntax.js.map +1 -0
  69. package/dist/parsers/typeorm.d.ts +12 -0
  70. package/dist/parsers/typeorm.js +1614 -0
  71. package/dist/parsers/typeorm.js.map +1 -0
  72. package/dist/parsers/typescriptSyntax.d.ts +57 -0
  73. package/dist/parsers/typescriptSyntax.js +200 -0
  74. package/dist/parsers/typescriptSyntax.js.map +1 -0
  75. package/dist/prismaOnlyConstructs.d.ts +7 -0
  76. package/dist/prismaOnlyConstructs.js +76 -0
  77. package/dist/prismaOnlyConstructs.js.map +1 -0
  78. package/dist/result.d.ts +20 -0
  79. package/dist/result.js +18 -0
  80. package/dist/result.js.map +1 -0
  81. package/dist/transforms.d.ts +13 -0
  82. package/dist/transforms.js +142 -0
  83. package/dist/transforms.js.map +1 -0
  84. package/package.json +83 -4
@@ -0,0 +1,1161 @@
1
+ import { toPascalCase } from '../naming.js';
2
+ import { err, ok } from '../result.js';
3
+ import { evaluateNode, getPythonParser, lastSegment, } from './pythonSyntax.js';
4
+ const SCALAR_FIELDS = {
5
+ AutoField: { type: 'int', isAutoIncrement: true },
6
+ SmallAutoField: { type: 'int', isAutoIncrement: true },
7
+ BigAutoField: { type: 'bigInt', isAutoIncrement: true },
8
+ CharField: { type: 'string' },
9
+ SlugField: { type: 'string', defaultMaxLength: 50 },
10
+ EmailField: { type: 'string', defaultMaxLength: 254 },
11
+ URLField: { type: 'string', defaultMaxLength: 200 },
12
+ FileField: { type: 'string', defaultMaxLength: 100 },
13
+ ImageField: { type: 'string', defaultMaxLength: 100 },
14
+ FilePathField: { type: 'string', defaultMaxLength: 100 },
15
+ GenericIPAddressField: { type: 'ipAddress' },
16
+ IPAddressField: { type: 'ipAddress' },
17
+ CICharField: { type: 'string' },
18
+ CIEmailField: { type: 'string', defaultMaxLength: 254 },
19
+ CITextField: { type: 'text' },
20
+ TextField: { type: 'text' },
21
+ IntegerField: { type: 'int' },
22
+ SmallIntegerField: { type: 'int' },
23
+ PositiveIntegerField: { type: 'int' },
24
+ PositiveSmallIntegerField: { type: 'int' },
25
+ BigIntegerField: { type: 'bigInt' },
26
+ PositiveBigIntegerField: { type: 'bigInt' },
27
+ DurationField: { type: 'duration' },
28
+ FloatField: { type: 'float' },
29
+ DecimalField: { type: 'decimal' },
30
+ BooleanField: { type: 'boolean' },
31
+ NullBooleanField: { type: 'boolean' },
32
+ DateTimeField: { type: 'dateTime' },
33
+ DateField: { type: 'date' },
34
+ TimeField: { type: 'time' },
35
+ UUIDField: { type: 'uuid' },
36
+ JSONField: { type: 'json' },
37
+ BinaryField: { type: 'bytes' },
38
+ HStoreField: { type: 'hstore' },
39
+ IntegerRangeField: { type: 'range', rangeOf: 'int' },
40
+ BigIntegerRangeField: { type: 'range', rangeOf: 'bigInt' },
41
+ DecimalRangeField: { type: 'range', rangeOf: 'decimal' },
42
+ DateRangeField: { type: 'range', rangeOf: 'date' },
43
+ DateTimeRangeField: { type: 'range', rangeOf: 'dateTime' },
44
+ };
45
+ /** Virtual fields from django.contrib.contenttypes: they have no column of their own. */
46
+ const VIRTUAL_RELATION_FIELDS = new Set([
47
+ 'GenericForeignKey',
48
+ 'GenericRelation',
49
+ ]);
50
+ /**
51
+ * Placeholder relation targets for the user model. They are resolved once every
52
+ * file is read, because a model with Meta.swappable = "AUTH_USER_MODEL" defines it.
53
+ */
54
+ const AUTH_USER_SETTING = 'settings.AUTH_USER_MODEL';
55
+ const GET_USER_MODEL_CALL = 'get_user_model()';
56
+ const RELATION_FIELDS = new Set([
57
+ 'ForeignKey',
58
+ 'OneToOneField',
59
+ 'ManyToManyField',
60
+ ]);
61
+ const ON_DELETE_MAP = {
62
+ CASCADE: 'cascade',
63
+ SET_NULL: 'setNull',
64
+ PROTECT: 'restrict',
65
+ RESTRICT: 'restrict',
66
+ DO_NOTHING: 'noAction',
67
+ SET_DEFAULT: 'setDefault',
68
+ };
69
+ const NOW_CALLABLES = new Set([
70
+ 'now',
71
+ 'timezone.now',
72
+ 'datetime.now',
73
+ 'datetime.datetime.now',
74
+ ]);
75
+ /** Parses Django model files into the shared IR using tree-sitter (no Python required). */
76
+ export async function parseDjango(sources, options) {
77
+ const parserResult = await getPythonParser();
78
+ if (!parserResult.ok) {
79
+ return parserResult;
80
+ }
81
+ const parser = parserResult.value;
82
+ const parsedFiles = sources.map((source) => parseFile(parser, source));
83
+ const schema = buildSchema(parsedFiles, options);
84
+ if (schema.models.length === 0) {
85
+ const checkedPaths = sources
86
+ .map((source) => source.path)
87
+ .join(', ');
88
+ return err('NO_MODELS_FOUND', `No concrete Django models were found in: ${checkedPaths}. A model is a class inheriting from ` +
89
+ `models.Model (directly, or through an abstract base) that is not marked abstract = True in its Meta class.`);
90
+ }
91
+ return ok(schema);
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // File parsing
95
+ // ---------------------------------------------------------------------------
96
+ function parseFile(parser, source) {
97
+ const warnings = [];
98
+ const models = [];
99
+ const enums = [];
100
+ const tree = parser.parse(source.text);
101
+ if (tree.rootNode.hasError) {
102
+ warnings.push(`${source.path}: the file contains Python syntax errors; some models or fields may be missing from the output.`);
103
+ }
104
+ const classNodes = collectTopLevelClasses(tree.rootNode);
105
+ const enumByClass = new Map();
106
+ for (const classNode of classNodes) {
107
+ const className = classNameOf(classNode);
108
+ if (isChoicesClass(classNode)) {
109
+ const info = parseChoicesClass(classNode, className, enums);
110
+ enumByClass.set(className, info);
111
+ }
112
+ }
113
+ for (const classNode of classNodes) {
114
+ const className = classNameOf(classNode);
115
+ if (isChoicesClass(classNode)) {
116
+ continue;
117
+ }
118
+ const bases = basesOf(classNode);
119
+ const rawModel = parseModelClass(classNode, className, bases, source, enumByClass, enums, warnings);
120
+ if (rawModel !== undefined) {
121
+ models.push(rawModel);
122
+ }
123
+ }
124
+ return { models, enums, warnings };
125
+ }
126
+ function collectTopLevelClasses(root) {
127
+ const classNodes = [];
128
+ for (const child of root.namedChildren) {
129
+ if (child.type === 'class_definition') {
130
+ classNodes.push(child);
131
+ }
132
+ else if (child.type === 'decorated_definition') {
133
+ const definition = child.childForFieldName('definition');
134
+ if (definition !== null && definition.type === 'class_definition') {
135
+ classNodes.push(definition);
136
+ }
137
+ }
138
+ }
139
+ return classNodes;
140
+ }
141
+ function classNameOf(classNode) {
142
+ const nameNode = classNode.childForFieldName('name');
143
+ return nameNode === null ? '' : nameNode.text;
144
+ }
145
+ function basesOf(classNode) {
146
+ const superclasses = classNode.childForFieldName('superclasses');
147
+ if (superclasses === null) {
148
+ return [];
149
+ }
150
+ return superclasses.namedChildren
151
+ .filter((child) => child.type !== 'keyword_argument' && child.type !== 'comment')
152
+ .map((child) => child.text.replace(/\s+/g, ''));
153
+ }
154
+ function bodyStatements(classNode) {
155
+ const body = classNode.childForFieldName('body');
156
+ return body === null ? [] : body.namedChildren;
157
+ }
158
+ const MODEL_BASE_NAMES = new Set([
159
+ 'Model',
160
+ 'AbstractUser',
161
+ 'AbstractBaseUser',
162
+ ]);
163
+ function isDjangoModelBase(baseName) {
164
+ return MODEL_BASE_NAMES.has(lastSegment(baseName));
165
+ }
166
+ /** True when the class is a model: it has fields, a Django base, or inherits from another model. */
167
+ function isModelClass(rawModel, rawByName, visiting) {
168
+ if (rawModel.fields.length > 0 ||
169
+ rawModel.relations.length > 0 ||
170
+ rawModel.bases.some(isDjangoModelBase)) {
171
+ return true;
172
+ }
173
+ visiting.add(rawModel.name);
174
+ return rawModel.bases.some((baseName) => {
175
+ const parent = rawByName.get(lastSegment(baseName));
176
+ return (parent !== undefined &&
177
+ !visiting.has(parent.name) &&
178
+ isModelClass(parent, rawByName, new Set(visiting)));
179
+ });
180
+ }
181
+ function isChoicesClass(classNode) {
182
+ return basesOf(classNode).some((base) => /Choices$/.test(lastSegment(base)));
183
+ }
184
+ /** Returns the evaluated call when the statement looks like `name = something(...)`. */
185
+ function assignmentOf(statement) {
186
+ if (statement.type !== 'expression_statement') {
187
+ return undefined;
188
+ }
189
+ const assignment = statement.namedChildren[0];
190
+ if (assignment === undefined || assignment.type !== 'assignment') {
191
+ return undefined;
192
+ }
193
+ const left = assignment.childForFieldName('left');
194
+ const right = assignment.childForFieldName('right');
195
+ if (left === null || right === null || left.type !== 'identifier') {
196
+ return undefined;
197
+ }
198
+ return { name: left.text, value: evaluateNode(right) };
199
+ }
200
+ function fieldCallOf(statement) {
201
+ const assignment = assignmentOf(statement);
202
+ if (assignment === undefined || assignment.value.kind !== 'call') {
203
+ return undefined;
204
+ }
205
+ const calleeName = lastSegment(assignment.value.callee);
206
+ if (RELATION_FIELDS.has(calleeName) ||
207
+ calleeName in SCALAR_FIELDS ||
208
+ calleeName.endsWith('Field')) {
209
+ return { name: assignment.name, call: assignment.value };
210
+ }
211
+ return undefined;
212
+ }
213
+ // ---------------------------------------------------------------------------
214
+ // Choices (TextChoices / IntegerChoices) -> enums
215
+ // ---------------------------------------------------------------------------
216
+ function parseChoicesClass(classNode, enumName, enums) {
217
+ const isInteger = basesOf(classNode).some((base) => lastSegment(base) === 'IntegerChoices');
218
+ if (isInteger) {
219
+ return { kind: 'integer' };
220
+ }
221
+ const values = [];
222
+ for (const statement of bodyStatements(classNode)) {
223
+ const assignment = assignmentOf(statement);
224
+ if (assignment === undefined || assignment.name.startsWith('_')) {
225
+ continue;
226
+ }
227
+ const value = assignment.value;
228
+ if (value.kind === 'string') {
229
+ values.push({
230
+ name: assignment.name,
231
+ dbValue: value.value,
232
+ label: toPascalCase(assignment.name),
233
+ });
234
+ }
235
+ else if (value.kind === 'list') {
236
+ const first = value.items[0];
237
+ const second = value.items[1];
238
+ if (first !== undefined && first.kind === 'string') {
239
+ const label = second !== undefined && second.kind === 'string'
240
+ ? second.value
241
+ : undefined;
242
+ values.push({
243
+ name: assignment.name,
244
+ dbValue: first.value,
245
+ ...(label === undefined ? {} : { label }),
246
+ });
247
+ }
248
+ }
249
+ }
250
+ enums.push({ name: enumName, values });
251
+ return { kind: 'string', enumName, values };
252
+ }
253
+ function memberNameFromValue(rawValue, usedNames) {
254
+ let memberName = rawValue
255
+ .replace(/[^A-Za-z0-9]+/g, '_')
256
+ .replace(/^_+|_+$/g, '')
257
+ .toUpperCase();
258
+ if (memberName.length === 0 || /^[0-9]/.test(memberName)) {
259
+ memberName = `V_${memberName}`;
260
+ }
261
+ let candidate = memberName;
262
+ let suffix = 2;
263
+ while (usedNames.has(candidate)) {
264
+ candidate = `${memberName}_${suffix}`;
265
+ suffix += 1;
266
+ }
267
+ usedNames.add(candidate);
268
+ return candidate;
269
+ }
270
+ // ---------------------------------------------------------------------------
271
+ // Model classes
272
+ // ---------------------------------------------------------------------------
273
+ function parseModelClass(classNode, className, bases, source, fileEnums, allEnums, warnings) {
274
+ const enumByClass = new Map(fileEnums);
275
+ const statements = bodyStatements(classNode);
276
+ // Nested Choices classes are namespaced by their model to keep enum names unique.
277
+ for (const statement of statements) {
278
+ if (statement.type === 'class_definition' && isChoicesClass(statement)) {
279
+ const nestedName = classNameOf(statement);
280
+ const info = parseChoicesClass(statement, `${className}${nestedName}`, allEnums);
281
+ enumByClass.set(nestedName, info);
282
+ }
283
+ }
284
+ const fieldContext = {
285
+ modelName: className,
286
+ enumByClass,
287
+ newEnums: allEnums,
288
+ warnings,
289
+ };
290
+ const fields = [];
291
+ const relations = [];
292
+ const indexes = [];
293
+ let meta = { isAbstract: false, isProxy: false, indexes: [] };
294
+ let compositePrimaryKey;
295
+ for (const statement of statements) {
296
+ if (statement.type === 'class_definition' &&
297
+ classNameOf(statement) === 'Meta') {
298
+ meta = parseMeta(statement, `${source.path}: ${className}.Meta`, warnings);
299
+ continue;
300
+ }
301
+ const special = parseSpecialAttribute(statement, className, warnings);
302
+ if (special.consumed) {
303
+ if (special.compositeKey !== undefined) {
304
+ compositePrimaryKey = special.compositeKey;
305
+ }
306
+ continue;
307
+ }
308
+ const fieldCall = fieldCallOf(statement);
309
+ if (fieldCall === undefined) {
310
+ continue;
311
+ }
312
+ const parsed = parseFieldCall(fieldCall.name, fieldCall.call, fieldContext);
313
+ if (parsed.field !== undefined) {
314
+ fields.push(parsed.field);
315
+ }
316
+ if (parsed.relation !== undefined) {
317
+ relations.push(parsed.relation);
318
+ }
319
+ if (parsed.index !== undefined) {
320
+ indexes.push(parsed.index);
321
+ }
322
+ }
323
+ if (bases.length === 0 && fields.length === 0 && relations.length === 0) {
324
+ // A plain Python class with no bases and no model fields (helper, constants): not a model.
325
+ return undefined;
326
+ }
327
+ return {
328
+ name: className,
329
+ appLabel: meta.appLabel ?? source.appLabel,
330
+ filePath: source.path,
331
+ bases,
332
+ isAbstract: meta.isAbstract,
333
+ isProxy: meta.isProxy,
334
+ ...(meta.swappable === undefined ? {} : { swappable: meta.swappable }),
335
+ ...(meta.tableName === undefined ? {} : { tableName: meta.tableName }),
336
+ ...(compositePrimaryKey === undefined ? {} : { compositePrimaryKey }),
337
+ fields,
338
+ relations,
339
+ indexes: [...indexes, ...meta.indexes],
340
+ };
341
+ }
342
+ /** Handles class attributes that are not ordinary columns (CompositePrimaryKey, generic relations). */
343
+ function parseSpecialAttribute(statement, className, warnings) {
344
+ const assignment = assignmentOf(statement);
345
+ if (assignment === undefined || assignment.value.kind !== 'call') {
346
+ return { consumed: false };
347
+ }
348
+ const calleeName = lastSegment(assignment.value.callee);
349
+ if (calleeName === 'CompositePrimaryKey') {
350
+ const names = assignment.value.args.flatMap((arg) => arg.kind === 'string' ? [arg.value] : []);
351
+ if (names.length === 0) {
352
+ warnings.push(`${className}.${assignment.name}: CompositePrimaryKey(...) lists no field names that could be read; the model keeps an automatic id.`);
353
+ return { consumed: true };
354
+ }
355
+ return { consumed: true, compositeKey: names };
356
+ }
357
+ if (VIRTUAL_RELATION_FIELDS.has(calleeName)) {
358
+ warnings.push(`${className}.${assignment.name}: ${calleeName} is a virtual field without a column and was skipped; ` +
359
+ `its content_type and object_id columns are converted as ordinary fields.`);
360
+ return { consumed: true };
361
+ }
362
+ return { consumed: false };
363
+ }
364
+ function parseMeta(metaNode, location, warnings) {
365
+ const meta = { isAbstract: false, isProxy: false, indexes: [] };
366
+ for (const statement of bodyStatements(metaNode)) {
367
+ const assignment = assignmentOf(statement);
368
+ if (assignment === undefined) {
369
+ continue;
370
+ }
371
+ const value = assignment.value;
372
+ switch (assignment.name) {
373
+ case 'abstract':
374
+ meta.isAbstract = value.kind === 'bool' && value.value;
375
+ break;
376
+ case 'proxy':
377
+ meta.isProxy = value.kind === 'bool' && value.value;
378
+ break;
379
+ case 'swappable':
380
+ if (value.kind === 'string') {
381
+ meta.swappable = value.value;
382
+ }
383
+ break;
384
+ case 'db_table':
385
+ if (value.kind === 'string') {
386
+ meta.tableName = value.value;
387
+ }
388
+ break;
389
+ case 'app_label':
390
+ if (value.kind === 'string') {
391
+ meta.appLabel = value.value;
392
+ }
393
+ break;
394
+ case 'managed':
395
+ if (value.kind === 'bool' && !value.value) {
396
+ warnings.push(`${location}: managed = False is ignored; the model is converted like any other table.`);
397
+ }
398
+ break;
399
+ case 'unique_together':
400
+ meta.indexes.push(...parseUniqueTogether(value));
401
+ break;
402
+ case 'index_together':
403
+ meta.indexes.push(...parseUniqueTogether(value).map((index) => ({
404
+ ...index,
405
+ isUnique: false,
406
+ })));
407
+ break;
408
+ case 'indexes':
409
+ meta.indexes.push(...parseIndexList(value, false, location, warnings));
410
+ break;
411
+ case 'constraints':
412
+ meta.indexes.push(...parseIndexList(value, true, location, warnings));
413
+ break;
414
+ default:
415
+ break;
416
+ }
417
+ }
418
+ return meta;
419
+ }
420
+ function stringItems(value) {
421
+ if (value === undefined) {
422
+ return [];
423
+ }
424
+ if (value.kind === 'string') {
425
+ return [value.value];
426
+ }
427
+ if (value.kind === 'list') {
428
+ return value.items.flatMap((item) => item.kind === 'string' ? [item.value.replace(/^-/, '')] : []);
429
+ }
430
+ return [];
431
+ }
432
+ function parseUniqueTogether(value) {
433
+ if (value.kind !== 'list') {
434
+ return [];
435
+ }
436
+ const allStrings = value.items.every((item) => item.kind === 'string');
437
+ if (allStrings) {
438
+ return [{ fields: stringItems(value), isUnique: true }];
439
+ }
440
+ return value.items
441
+ .map((item) => ({
442
+ fields: stringItems(item),
443
+ isUnique: true,
444
+ }))
445
+ .filter((index) => index.fields.length > 0);
446
+ }
447
+ function parseIndexList(value, constraintsOnly, location, warnings) {
448
+ if (value.kind !== 'list') {
449
+ return [];
450
+ }
451
+ const indexes = [];
452
+ for (const item of value.items) {
453
+ if (item.kind !== 'call') {
454
+ continue;
455
+ }
456
+ const calleeName = lastSegment(item.callee);
457
+ if (calleeName === 'UniqueConstraint' &&
458
+ item.kwargs['condition'] !== undefined) {
459
+ warnings.push(`${location}: UniqueConstraint(...) with condition= is a partial unique constraint and was skipped; ` +
460
+ `converting it would make the columns unconditionally unique.`);
461
+ continue;
462
+ }
463
+ const fields = stringItems(item.kwargs['fields']);
464
+ const explicitName = item.kwargs['name'];
465
+ const name = explicitName !== undefined && explicitName.kind === 'string'
466
+ ? explicitName.value
467
+ : undefined;
468
+ if (calleeName === 'Index' && !constraintsOnly && fields.length > 0) {
469
+ indexes.push({
470
+ fields,
471
+ isUnique: false,
472
+ ...(name === undefined ? {} : { name }),
473
+ });
474
+ }
475
+ else if (calleeName === 'UniqueConstraint' && fields.length > 0) {
476
+ indexes.push({
477
+ fields,
478
+ isUnique: true,
479
+ ...(name === undefined ? {} : { name }),
480
+ });
481
+ }
482
+ else if (calleeName === 'CheckConstraint' ||
483
+ calleeName === 'UniqueConstraint' ||
484
+ calleeName === 'Index') {
485
+ warnings.push(`${location}: ${calleeName}(...) could not be converted (only field-based indexes and unique constraints are supported).`);
486
+ }
487
+ }
488
+ return indexes;
489
+ }
490
+ function boolKwarg(call, key) {
491
+ const value = call.kwargs[key];
492
+ return value !== undefined && value.kind === 'bool' && value.value;
493
+ }
494
+ function stringKwarg(call, key) {
495
+ const value = call.kwargs[key];
496
+ return value !== undefined && value.kind === 'string'
497
+ ? value.value
498
+ : undefined;
499
+ }
500
+ function numberKwarg(call, key) {
501
+ const value = call.kwargs[key];
502
+ return value !== undefined && value.kind === 'number'
503
+ ? value.value
504
+ : undefined;
505
+ }
506
+ function parseFieldCall(fieldName, call, context) {
507
+ const calleeName = lastSegment(call.callee);
508
+ if (RELATION_FIELDS.has(calleeName)) {
509
+ return parseRelationField(fieldName, calleeName, call, context);
510
+ }
511
+ if (calleeName === 'ArrayField') {
512
+ return parseArrayField(fieldName, call, context);
513
+ }
514
+ if (calleeName === 'GeneratedField') {
515
+ return parseGeneratedField(fieldName, call, context);
516
+ }
517
+ return parseScalarField(fieldName, calleeName, call, context);
518
+ }
519
+ function syntheticCall(callee) {
520
+ return { kind: 'call', callee, args: [], kwargs: {}, text: `${callee}()` };
521
+ }
522
+ /** Returns Python source text for an evaluated expression (used for GeneratedField). */
523
+ function sourceTextOf(value) {
524
+ switch (value.kind) {
525
+ case 'call':
526
+ return value.text;
527
+ case 'name':
528
+ return value.value;
529
+ case 'other':
530
+ return value.text.replace(/\s*\n\s*/g, ' ');
531
+ case 'string':
532
+ return JSON.stringify(value.value);
533
+ case 'number':
534
+ return String(value.value);
535
+ case 'bool':
536
+ return value.value ? 'True' : 'False';
537
+ case 'none':
538
+ return 'None';
539
+ case 'list':
540
+ return `[${value.items.map(sourceTextOf).join(', ')}]`;
541
+ default:
542
+ return '';
543
+ }
544
+ }
545
+ /** django.contrib.postgres ArrayField: the element type is the base_field, nested arrays add dimensions. */
546
+ function parseArrayField(fieldName, call, context) {
547
+ const location = `${context.modelName}.${fieldName}`;
548
+ const baseValue = call.args[0] ?? call.kwargs['base_field'];
549
+ let elementCall;
550
+ if (baseValue !== undefined && baseValue.kind === 'call') {
551
+ elementCall = baseValue;
552
+ }
553
+ else {
554
+ context.warnings.push(`${location}: the base_field of ArrayField(...) could not be read; the elements were assumed to be strings.`);
555
+ elementCall = syntheticCall('CharField');
556
+ }
557
+ const element = parseFieldCall(fieldName, elementCall, context);
558
+ if (element.field === undefined) {
559
+ context.warnings.push(`${location}: the base_field of ArrayField(...) is not a column type; the field was skipped.`);
560
+ return {};
561
+ }
562
+ if (call.kwargs['size'] !== undefined) {
563
+ context.warnings.push(`${location}: ArrayField size= is not represented in the schema; arrays are unbounded.`);
564
+ }
565
+ const isPrimaryKey = boolKwarg(call, 'primary_key');
566
+ const field = {
567
+ ...element.field,
568
+ name: fieldName,
569
+ columnName: stringKwarg(call, 'db_column') ?? fieldName,
570
+ isPrimaryKey,
571
+ isUnique: boolKwarg(call, 'unique') && !isPrimaryKey,
572
+ isNullable: !isPrimaryKey && boolKwarg(call, 'null'),
573
+ isAutoUpdated: false,
574
+ arrayDepth: (element.field.arrayDepth ?? 0) + 1,
575
+ };
576
+ delete field.default;
577
+ delete field.isDbDefault;
578
+ const spec = { type: field.type };
579
+ const defaultValue = resolveDefault(location, call, spec, undefined, context, 'default', true);
580
+ if (defaultValue !== undefined) {
581
+ field.default = defaultValue;
582
+ }
583
+ else if (call.kwargs['db_default'] !== undefined) {
584
+ const dbDefault = resolveDefault(location, call, spec, undefined, context, 'db_default', true);
585
+ if (dbDefault !== undefined) {
586
+ field.default = dbDefault;
587
+ field.isDbDefault = true;
588
+ }
589
+ }
590
+ const parsed = { field };
591
+ if (boolKwarg(call, 'db_index') && !isPrimaryKey && !field.isUnique) {
592
+ parsed.index = { fields: [fieldName], isUnique: false };
593
+ }
594
+ return parsed;
595
+ }
596
+ /** GeneratedField: the column type comes from output_field, the expression is carried as source text. */
597
+ function parseGeneratedField(fieldName, call, context) {
598
+ const location = `${context.modelName}.${fieldName}`;
599
+ const outputValue = call.kwargs['output_field'];
600
+ let outputCall;
601
+ if (outputValue !== undefined && outputValue.kind === 'call') {
602
+ outputCall = outputValue;
603
+ }
604
+ else {
605
+ context.warnings.push(`${location}: the output_field of GeneratedField(...) could not be read; the column was typed as a string.`);
606
+ outputCall = syntheticCall('CharField');
607
+ }
608
+ const output = parseFieldCall(fieldName, outputCall, context);
609
+ if (output.field === undefined) {
610
+ context.warnings.push(`${location}: the output_field of GeneratedField(...) is not a column type; the field was skipped.`);
611
+ return {};
612
+ }
613
+ const expression = call.kwargs['expression'] ?? call.args[0];
614
+ const expressionText = expression === undefined ? '' : sourceTextOf(expression);
615
+ const persist = call.kwargs['db_persist'];
616
+ const field = {
617
+ ...output.field,
618
+ name: fieldName,
619
+ columnName: stringKwarg(call, 'db_column') ?? fieldName,
620
+ isPrimaryKey: false,
621
+ isUnique: boolKwarg(call, 'unique'),
622
+ isNullable: output.field.isNullable || boolKwarg(call, 'null'),
623
+ isAutoUpdated: false,
624
+ };
625
+ delete field.default;
626
+ delete field.isDbDefault;
627
+ if (expressionText.length === 0) {
628
+ context.warnings.push(`${location}: GeneratedField(...) has no readable expression=; it was converted as a plain column.`);
629
+ }
630
+ else {
631
+ field.generated = {
632
+ expression: expressionText,
633
+ isStored: !(persist !== undefined &&
634
+ persist.kind === 'bool' &&
635
+ !persist.value),
636
+ };
637
+ }
638
+ return { field };
639
+ }
640
+ function parseScalarField(fieldName, calleeName, call, context) {
641
+ const location = `${context.modelName}.${fieldName}`;
642
+ let spec = SCALAR_FIELDS[calleeName];
643
+ if (spec === undefined) {
644
+ context.warnings.push(`${location}: unknown field type "${calleeName}"; it was converted as a plain string column. ` +
645
+ `Review the generated type.`);
646
+ spec = { type: 'string' };
647
+ }
648
+ const isPrimaryKey = boolKwarg(call, 'primary_key');
649
+ const field = {
650
+ name: fieldName,
651
+ columnName: stringKwarg(call, 'db_column') ?? fieldName,
652
+ type: spec.type,
653
+ ...(spec.rangeOf === undefined ? {} : { rangeOf: spec.rangeOf }),
654
+ isPrimaryKey,
655
+ isUnique: boolKwarg(call, 'unique') && !isPrimaryKey,
656
+ isNullable: !isPrimaryKey &&
657
+ (boolKwarg(call, 'null') || calleeName === 'NullBooleanField'),
658
+ isAutoUpdated: boolKwarg(call, 'auto_now'),
659
+ };
660
+ const maxLength = numberKwarg(call, 'max_length') ?? spec.defaultMaxLength;
661
+ if (maxLength !== undefined &&
662
+ (spec.type === 'string' || spec.type === 'text')) {
663
+ field.maxLength = maxLength;
664
+ }
665
+ if (spec.type === 'decimal') {
666
+ const maxDigits = numberKwarg(call, 'max_digits');
667
+ const decimalPlaces = numberKwarg(call, 'decimal_places');
668
+ if (maxDigits !== undefined) {
669
+ field.maxDigits = maxDigits;
670
+ }
671
+ if (decimalPlaces !== undefined) {
672
+ field.decimalPlaces = decimalPlaces;
673
+ }
674
+ }
675
+ const enumInfo = resolveChoices(fieldName, call, context);
676
+ if (enumInfo !== undefined && enumInfo.kind === 'string') {
677
+ field.enumName = enumInfo.enumName;
678
+ field.type = 'string';
679
+ }
680
+ let defaultValue = resolveDefault(location, call, spec, enumInfo, context, 'default', false);
681
+ if (call.kwargs['db_default'] !== undefined) {
682
+ if (defaultValue !== undefined) {
683
+ context.warnings.push(`${location}: db_default was ignored because default= is also set.`);
684
+ }
685
+ else {
686
+ defaultValue = resolveDefault(location, call, spec, enumInfo, context, 'db_default', false);
687
+ if (defaultValue !== undefined) {
688
+ field.isDbDefault = true;
689
+ }
690
+ }
691
+ }
692
+ if (defaultValue !== undefined) {
693
+ field.default = defaultValue;
694
+ }
695
+ else if (spec.isAutoIncrement === true) {
696
+ field.default = { kind: 'autoIncrement' };
697
+ }
698
+ if (boolKwarg(call, 'auto_now_add')) {
699
+ field.default = { kind: 'now' };
700
+ }
701
+ const parsed = { field };
702
+ if (boolKwarg(call, 'db_index') && !isPrimaryKey && !field.isUnique) {
703
+ parsed.index = { fields: [fieldName], isUnique: false };
704
+ }
705
+ return parsed;
706
+ }
707
+ function resolveChoices(fieldName, call, context) {
708
+ const choices = call.kwargs['choices'];
709
+ if (choices === undefined) {
710
+ return undefined;
711
+ }
712
+ if (choices.kind === 'name') {
713
+ const className = choices.value.split('.')[0] ?? '';
714
+ return context.enumByClass.get(className);
715
+ }
716
+ if (choices.kind === 'list') {
717
+ const usedNames = new Set();
718
+ const values = [];
719
+ for (const item of choices.items) {
720
+ if (item.kind !== 'list') {
721
+ continue;
722
+ }
723
+ const first = item.items[0];
724
+ const second = item.items[1];
725
+ if (first !== undefined && first.kind === 'string') {
726
+ const label = second !== undefined && second.kind === 'string'
727
+ ? second.value
728
+ : undefined;
729
+ values.push({
730
+ name: memberNameFromValue(first.value, usedNames),
731
+ dbValue: first.value,
732
+ ...(label === undefined ? {} : { label }),
733
+ });
734
+ }
735
+ }
736
+ if (values.length === 0) {
737
+ return undefined;
738
+ }
739
+ const enumName = `${context.modelName}${toPascalCase(fieldName)}`;
740
+ context.newEnums.push({ name: enumName, values });
741
+ return { kind: 'string', enumName, values };
742
+ }
743
+ return undefined;
744
+ }
745
+ /** Unwraps the expression forms db_default accepts (Value(x), Now(), RandomUUID()) into plain defaults. */
746
+ function unwrapDbDefault(value) {
747
+ if (value.kind !== 'call') {
748
+ return value;
749
+ }
750
+ const calleeName = lastSegment(value.callee);
751
+ const first = value.args[0];
752
+ if (calleeName === 'Value' && first !== undefined) {
753
+ return first;
754
+ }
755
+ if (calleeName === 'Now' && value.args.length === 0) {
756
+ return { kind: 'name', value: 'now' };
757
+ }
758
+ if (calleeName === 'RandomUUID' && value.args.length === 0) {
759
+ return { kind: 'name', value: 'uuid.uuid4' };
760
+ }
761
+ return value;
762
+ }
763
+ function resolveDefault(location, call, spec, enumInfo, context, kwarg, isArray) {
764
+ const rawValue = call.kwargs[kwarg];
765
+ if (rawValue === undefined || rawValue.kind === 'none') {
766
+ return undefined;
767
+ }
768
+ const value = kwarg === 'db_default' ? unwrapDbDefault(rawValue) : rawValue;
769
+ const emptyListAllowed = spec.type === 'json' || isArray;
770
+ switch (value.kind) {
771
+ case 'list':
772
+ if (value.items.length === 0 && emptyListAllowed) {
773
+ return { kind: 'literal', value: '[]' };
774
+ }
775
+ context.warnings.push(`${location}: ${kwarg}=[...] is not representable and was dropped.`);
776
+ return undefined;
777
+ case 'other':
778
+ if (value.text.replace(/\s+/g, '') === '{}' &&
779
+ (spec.type === 'json' || spec.type === 'hstore')) {
780
+ return { kind: 'literal', value: '{}' };
781
+ }
782
+ context.warnings.push(`${location}: a computed ${kwarg} value was dropped (only literals and now/uuid are supported).`);
783
+ return undefined;
784
+ case 'bool':
785
+ case 'number':
786
+ return { kind: 'literal', value: value.value };
787
+ case 'string': {
788
+ if (enumInfo !== undefined && enumInfo.kind === 'string') {
789
+ const member = enumInfo.values.find((enumValue) => enumValue.dbValue === value.value);
790
+ if (member !== undefined) {
791
+ return { kind: 'enumValue', value: member.name };
792
+ }
793
+ }
794
+ return { kind: 'literal', value: value.value };
795
+ }
796
+ case 'name': {
797
+ const dotted = value.value;
798
+ if (dotted === 'uuid.uuid4' ||
799
+ dotted === 'uuid4' ||
800
+ dotted === 'uuid.uuid1') {
801
+ return { kind: 'uuid' };
802
+ }
803
+ if (NOW_CALLABLES.has(dotted) || dotted.endsWith('timezone.now')) {
804
+ return { kind: 'now' };
805
+ }
806
+ if (dotted === 'list' && emptyListAllowed) {
807
+ return { kind: 'literal', value: '[]' };
808
+ }
809
+ if (dotted === 'dict' &&
810
+ (spec.type === 'json' || spec.type === 'hstore')) {
811
+ return { kind: 'literal', value: '{}' };
812
+ }
813
+ const segments = dotted.split('.');
814
+ const memberName = segments[1];
815
+ const enumClass = context.enumByClass.get(segments[0] ?? '');
816
+ if (enumClass !== undefined &&
817
+ enumClass.kind === 'string' &&
818
+ memberName !== undefined) {
819
+ return { kind: 'enumValue', value: memberName };
820
+ }
821
+ if (enumClass !== undefined && enumClass.kind === 'integer') {
822
+ return undefined;
823
+ }
824
+ context.warnings.push(`${location}: ${kwarg}=${dotted} is not representable and was dropped.`);
825
+ return undefined;
826
+ }
827
+ case 'call': {
828
+ const arg = value.args[0];
829
+ if (lastSegment(value.callee) === 'Decimal' && arg !== undefined) {
830
+ if (arg.kind === 'string' && !Number.isNaN(Number(arg.value))) {
831
+ return { kind: 'literal', value: Number(arg.value) };
832
+ }
833
+ if (arg.kind === 'number') {
834
+ return { kind: 'literal', value: arg.value };
835
+ }
836
+ }
837
+ context.warnings.push(`${location}: ${kwarg}=${value.callee}(...) is not representable and was dropped.`);
838
+ return undefined;
839
+ }
840
+ default:
841
+ context.warnings.push(`${location}: a computed ${kwarg} value was dropped (only literals and now/uuid are supported).`);
842
+ return undefined;
843
+ }
844
+ }
845
+ function parseRelationField(fieldName, calleeName, call, context) {
846
+ const location = `${context.modelName}.${fieldName}`;
847
+ const target = resolveRelationTarget(call, context);
848
+ if (target === undefined) {
849
+ context.warnings.push(`${location}: could not determine the target model of ${calleeName}(...); the field was skipped.`);
850
+ return {};
851
+ }
852
+ if (calleeName === 'ManyToManyField' &&
853
+ call.kwargs['through'] !== undefined) {
854
+ context.warnings.push(`${location}: ManyToManyField with through= was skipped; the explicit through model carries the relations.`);
855
+ return {};
856
+ }
857
+ const relatedNameValue = stringKwarg(call, 'related_name');
858
+ const relatedName = relatedNameValue === undefined || relatedNameValue.endsWith('+')
859
+ ? undefined
860
+ : relatedNameValue;
861
+ const toField = stringKwarg(call, 'to_field');
862
+ let kind = 'foreignKey';
863
+ if (calleeName === 'OneToOneField') {
864
+ kind = 'oneToOne';
865
+ }
866
+ else if (calleeName === 'ManyToManyField') {
867
+ kind = 'manyToMany';
868
+ }
869
+ else if (boolKwarg(call, 'unique')) {
870
+ kind = 'oneToOne';
871
+ }
872
+ const relation = {
873
+ name: fieldName,
874
+ kind,
875
+ targetModel: target,
876
+ columnName: stringKwarg(call, 'db_column') ?? `${fieldName}_id`,
877
+ isNullable: boolKwarg(call, 'null'),
878
+ onDelete: resolveOnDelete(location, call, context),
879
+ ...(relatedName === undefined ? {} : { relatedName }),
880
+ ...(toField === undefined ? {} : { toField }),
881
+ ...(boolKwarg(call, 'primary_key') ? { isPrimaryKey: true } : {}),
882
+ };
883
+ return { relation };
884
+ }
885
+ function resolveRelationTarget(call, context) {
886
+ const targetValue = call.args[0] ?? call.kwargs['to'];
887
+ if (targetValue === undefined) {
888
+ return undefined;
889
+ }
890
+ switch (targetValue.kind) {
891
+ case 'string':
892
+ return targetValue.value === 'self'
893
+ ? context.modelName
894
+ : lastSegment(targetValue.value);
895
+ case 'name':
896
+ if (lastSegment(targetValue.value) === 'AUTH_USER_MODEL') {
897
+ return AUTH_USER_SETTING;
898
+ }
899
+ return lastSegment(targetValue.value);
900
+ case 'call':
901
+ if (lastSegment(targetValue.callee) === 'get_user_model') {
902
+ return GET_USER_MODEL_CALL;
903
+ }
904
+ return undefined;
905
+ default:
906
+ return undefined;
907
+ }
908
+ }
909
+ function resolveOnDelete(location, call, context) {
910
+ const value = call.kwargs['on_delete'] ?? call.args[1];
911
+ if (value === undefined) {
912
+ return 'cascade';
913
+ }
914
+ const rawName = value.kind === 'name'
915
+ ? lastSegment(value.value)
916
+ : value.kind === 'call'
917
+ ? lastSegment(value.callee)
918
+ : '';
919
+ const mapped = ON_DELETE_MAP[rawName];
920
+ if (mapped !== undefined) {
921
+ return mapped;
922
+ }
923
+ context.warnings.push(`${location}: on_delete=${rawName || 'unknown'} has no equivalent and was converted to NoAction.`);
924
+ return 'noAction';
925
+ }
926
+ const KNOWN_EXTERNAL_BASES = new Set([
927
+ 'Model',
928
+ 'AbstractUser',
929
+ 'AbstractBaseUser',
930
+ 'PermissionsMixin',
931
+ 'Manager',
932
+ ]);
933
+ function buildSchema(parsedFiles, options) {
934
+ const warnings = parsedFiles.flatMap((file) => file.warnings);
935
+ const enumsByName = new Map();
936
+ for (const file of parsedFiles) {
937
+ for (const enumDefinition of file.enums) {
938
+ enumsByName.set(enumDefinition.name, enumDefinition);
939
+ }
940
+ }
941
+ const rawByName = new Map();
942
+ for (const file of parsedFiles) {
943
+ for (const rawModel of file.models) {
944
+ if (rawByName.has(rawModel.name)) {
945
+ warnings.push(`Duplicate model name "${rawModel.name}" (${rawModel.filePath}); only the first definition was converted.`);
946
+ continue;
947
+ }
948
+ rawByName.set(rawModel.name, rawModel);
949
+ }
950
+ }
951
+ retargetUserModel(rawByName, warnings);
952
+ retargetProxies(rawByName, warnings);
953
+ const models = [];
954
+ for (const rawModel of rawByName.values()) {
955
+ if (rawModel.isAbstract ||
956
+ rawModel.isProxy ||
957
+ !isModelClass(rawModel, rawByName, new Set())) {
958
+ continue;
959
+ }
960
+ const members = collectMembers(rawModel, rawByName, warnings, new Set());
961
+ models.push(finalizeModel(rawModel, members, options));
962
+ }
963
+ addStubModels(models, rawByName, warnings, options);
964
+ return { models, enums: [...enumsByName.values()], warnings };
965
+ }
966
+ /** Points relations at settings.AUTH_USER_MODEL / get_user_model() to the swappable model, or "User" with a warning. */
967
+ function retargetUserModel(rawByName, warnings) {
968
+ let swappableName;
969
+ for (const rawModel of rawByName.values()) {
970
+ if (rawModel.swappable === 'AUTH_USER_MODEL' && !rawModel.isProxy) {
971
+ swappableName = rawModel.name;
972
+ break;
973
+ }
974
+ }
975
+ for (const rawModel of rawByName.values()) {
976
+ for (const relation of rawModel.relations) {
977
+ if (relation.targetModel !== AUTH_USER_SETTING &&
978
+ relation.targetModel !== GET_USER_MODEL_CALL) {
979
+ continue;
980
+ }
981
+ if (swappableName !== undefined) {
982
+ relation.targetModel = swappableName;
983
+ continue;
984
+ }
985
+ warnings.push(relation.targetModel === AUTH_USER_SETTING
986
+ ? `${rawModel.name}.${relation.name}: settings.AUTH_USER_MODEL was assumed to be a model named "User".`
987
+ : `${rawModel.name}.${relation.name}: get_user_model() was assumed to return a model named "User".`);
988
+ relation.targetModel = 'User';
989
+ }
990
+ }
991
+ }
992
+ /** Names the concrete model a proxy model stands for, following chains of proxies. */
993
+ function concreteModelOf(proxy, rawByName, visiting) {
994
+ visiting.add(proxy.name);
995
+ for (const baseName of proxy.bases) {
996
+ const shortName = lastSegment(baseName);
997
+ if (shortName === 'Model' || /Mixin$/.test(shortName)) {
998
+ continue;
999
+ }
1000
+ const parent = rawByName.get(shortName);
1001
+ if (parent === undefined) {
1002
+ return shortName;
1003
+ }
1004
+ if (parent.isAbstract || visiting.has(parent.name)) {
1005
+ continue;
1006
+ }
1007
+ return parent.isProxy
1008
+ ? concreteModelOf(parent, rawByName, visiting)
1009
+ : parent.name;
1010
+ }
1011
+ return undefined;
1012
+ }
1013
+ /**
1014
+ * A proxy model (Meta.proxy = True) shares its parent's table, so it is not a model of its
1015
+ * own: relations that target it are pointed at the concrete model instead.
1016
+ */
1017
+ function retargetProxies(rawByName, warnings) {
1018
+ const concreteByProxy = new Map();
1019
+ for (const rawModel of rawByName.values()) {
1020
+ if (!rawModel.isProxy) {
1021
+ continue;
1022
+ }
1023
+ const concrete = concreteModelOf(rawModel, rawByName, new Set());
1024
+ if (concrete === undefined) {
1025
+ warnings.push(`${rawModel.name}: proxy model has no concrete base model that could be found, so it was skipped.`);
1026
+ continue;
1027
+ }
1028
+ concreteByProxy.set(rawModel.name, concrete);
1029
+ warnings.push(`${rawModel.name}: proxy model of "${concrete}" has no table of its own and was merged into "${concrete}"; ` +
1030
+ `relations that target "${rawModel.name}" now point to "${concrete}".`);
1031
+ }
1032
+ if (concreteByProxy.size === 0) {
1033
+ return;
1034
+ }
1035
+ for (const rawModel of rawByName.values()) {
1036
+ for (const relation of rawModel.relations) {
1037
+ const concrete = concreteByProxy.get(relation.targetModel);
1038
+ if (concrete !== undefined) {
1039
+ relation.targetModel = concrete;
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ function collectMembers(rawModel, rawByName, warnings, visiting) {
1045
+ const inherited = { fields: [], relations: [], indexes: [] };
1046
+ visiting.add(rawModel.name);
1047
+ for (const baseName of rawModel.bases) {
1048
+ const shortName = lastSegment(baseName);
1049
+ const parent = rawByName.get(shortName);
1050
+ if (parent === undefined) {
1051
+ if (!KNOWN_EXTERNAL_BASES.has(shortName)) {
1052
+ warnings.push(`${rawModel.name}: base class "${baseName}" was not found in the input files, so any fields it defines ` +
1053
+ `are missing. Add the file that defines it to --input.`);
1054
+ }
1055
+ else if (shortName !== 'Model') {
1056
+ warnings.push(`${rawModel.name}: fields inherited from django.contrib.auth "${shortName}" are not included; ` +
1057
+ `add them to the output manually.`);
1058
+ }
1059
+ continue;
1060
+ }
1061
+ if (visiting.has(parent.name)) {
1062
+ warnings.push(`${rawModel.name}: circular inheritance through "${parent.name}" was ignored.`);
1063
+ continue;
1064
+ }
1065
+ if (parent.isAbstract) {
1066
+ const parentMembers = collectMembers(parent, rawByName, warnings, new Set(visiting));
1067
+ inherited.fields.push(...parentMembers.fields);
1068
+ inherited.relations.push(...parentMembers.relations);
1069
+ inherited.indexes.push(...parentMembers.indexes);
1070
+ }
1071
+ else {
1072
+ const pointerName = `${parent.name.toLowerCase()}_ptr`;
1073
+ inherited.relations.push({
1074
+ name: pointerName,
1075
+ kind: 'oneToOne',
1076
+ targetModel: parent.name,
1077
+ columnName: `${pointerName}_id`,
1078
+ isNullable: false,
1079
+ onDelete: 'cascade',
1080
+ isPrimaryKey: true,
1081
+ });
1082
+ warnings.push(`${rawModel.name}: multi-table inheritance from "${parent.name}" was converted to a one-to-one primary key ` +
1083
+ `named "${pointerName}".`);
1084
+ }
1085
+ }
1086
+ const ownFieldNames = new Set(rawModel.fields.map((field) => field.name));
1087
+ const ownRelationNames = new Set(rawModel.relations.map((relation) => relation.name));
1088
+ return {
1089
+ fields: [
1090
+ ...inherited.fields.filter((field) => !ownFieldNames.has(field.name)),
1091
+ ...rawModel.fields,
1092
+ ],
1093
+ relations: [
1094
+ ...inherited.relations.filter((relation) => !ownRelationNames.has(relation.name)),
1095
+ ...rawModel.relations,
1096
+ ],
1097
+ indexes: [...inherited.indexes, ...rawModel.indexes],
1098
+ };
1099
+ }
1100
+ function autoPrimaryKey(options) {
1101
+ return {
1102
+ name: 'id',
1103
+ columnName: 'id',
1104
+ type: options.autoField,
1105
+ isPrimaryKey: true,
1106
+ isUnique: false,
1107
+ isNullable: false,
1108
+ isAutoUpdated: false,
1109
+ default: { kind: 'autoIncrement' },
1110
+ };
1111
+ }
1112
+ function finalizeModel(rawModel, members, options) {
1113
+ const hasCompositeKey = rawModel.compositePrimaryKey !== undefined &&
1114
+ rawModel.compositePrimaryKey.length > 0;
1115
+ const hasPrimaryKey = hasCompositeKey ||
1116
+ members.fields.some((field) => field.isPrimaryKey) ||
1117
+ members.relations.some((relation) => relation.isPrimaryKey === true);
1118
+ const fields = hasPrimaryKey
1119
+ ? members.fields
1120
+ : [autoPrimaryKey(options), ...members.fields];
1121
+ return {
1122
+ name: rawModel.name,
1123
+ tableName: rawModel.tableName ??
1124
+ `${rawModel.appLabel}_${rawModel.name.toLowerCase()}`,
1125
+ appLabel: rawModel.appLabel,
1126
+ fields,
1127
+ relations: members.relations,
1128
+ indexes: members.indexes,
1129
+ ...(hasCompositeKey && rawModel.compositePrimaryKey !== undefined
1130
+ ? { compositePrimaryKey: rawModel.compositePrimaryKey }
1131
+ : {}),
1132
+ };
1133
+ }
1134
+ function addStubModels(models, rawByName, warnings, options) {
1135
+ const known = new Set(models.map((model) => model.name));
1136
+ const stubs = [];
1137
+ for (const model of models) {
1138
+ for (const relation of model.relations) {
1139
+ const target = relation.targetModel;
1140
+ if (known.has(target) || rawByName.has(target)) {
1141
+ continue;
1142
+ }
1143
+ known.add(target);
1144
+ const isUserModel = target === 'User';
1145
+ stubs.push({
1146
+ name: target,
1147
+ tableName: isUserModel
1148
+ ? 'auth_user'
1149
+ : `${model.appLabel}_${target.toLowerCase()}`,
1150
+ appLabel: isUserModel ? 'auth' : model.appLabel,
1151
+ fields: [autoPrimaryKey(options)],
1152
+ relations: [],
1153
+ indexes: [],
1154
+ });
1155
+ warnings.push(`${model.name}.${relation.name} references model "${target}", which was not found in the input. ` +
1156
+ `A stub model with an auto-increment id was generated; replace it with the real definition.`);
1157
+ }
1158
+ }
1159
+ models.push(...stubs);
1160
+ }
1161
+ //# sourceMappingURL=django.js.map