millas 0.2.12-beta → 0.2.12-beta-2

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 (116) hide show
  1. package/package.json +3 -16
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +400 -167
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +309 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +383 -97
  12. package/src/admin/static/admin.css +1341 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +65 -1013
  19. package/src/admin/views/pages/detail.njk +40 -16
  20. package/src/admin/views/pages/form.njk +47 -599
  21. package/src/admin/views/pages/list.njk +145 -62
  22. package/src/admin/views/partials/form-field.njk +53 -0
  23. package/src/admin/views/partials/form-footer.njk +28 -0
  24. package/src/admin/views/partials/form-readonly.njk +114 -0
  25. package/src/admin/views/partials/form-scripts.njk +476 -0
  26. package/src/admin/views/partials/form-widget.njk +296 -0
  27. package/src/admin/views/partials/json-dialog.njk +80 -0
  28. package/src/admin/views/partials/json-editor.njk +37 -0
  29. package/src/admin.zip +0 -0
  30. package/src/auth/Auth.js +31 -10
  31. package/src/auth/AuthController.js +3 -1
  32. package/src/auth/AuthUser.js +119 -0
  33. package/src/cli.js +4 -2
  34. package/src/commands/createsuperuser.js +254 -0
  35. package/src/commands/lang.js +589 -0
  36. package/src/commands/migrate.js +154 -81
  37. package/src/commands/serve.js +82 -110
  38. package/src/container/AppInitializer.js +215 -0
  39. package/src/container/Application.js +278 -253
  40. package/src/container/HttpServer.js +156 -0
  41. package/src/container/MillasApp.js +29 -279
  42. package/src/container/MillasConfig.js +192 -0
  43. package/src/core/admin.js +5 -0
  44. package/src/core/auth.js +9 -0
  45. package/src/core/db.js +9 -0
  46. package/src/core/foundation.js +59 -0
  47. package/src/core/http.js +11 -0
  48. package/src/core/lang.js +1 -0
  49. package/src/core/mail.js +6 -0
  50. package/src/core/queue.js +7 -0
  51. package/src/core/validation.js +29 -0
  52. package/src/facades/Admin.js +1 -1
  53. package/src/facades/Auth.js +22 -39
  54. package/src/facades/Cache.js +21 -10
  55. package/src/facades/Database.js +1 -1
  56. package/src/facades/Events.js +18 -17
  57. package/src/facades/Facade.js +197 -0
  58. package/src/facades/Http.js +42 -45
  59. package/src/facades/Log.js +25 -49
  60. package/src/facades/Mail.js +27 -32
  61. package/src/facades/Queue.js +22 -15
  62. package/src/facades/Storage.js +18 -10
  63. package/src/facades/Url.js +53 -0
  64. package/src/http/HttpClient.js +673 -0
  65. package/src/http/ResponseDispatcher.js +18 -111
  66. package/src/http/UrlGenerator.js +375 -0
  67. package/src/http/WelcomePage.js +273 -0
  68. package/src/http/adapters/ExpressAdapter.js +315 -0
  69. package/src/http/adapters/HttpAdapter.js +168 -0
  70. package/src/http/adapters/index.js +9 -0
  71. package/src/i18n/I18nServiceProvider.js +91 -0
  72. package/src/i18n/Translator.js +635 -0
  73. package/src/i18n/defaults.js +122 -0
  74. package/src/i18n/index.js +164 -0
  75. package/src/i18n/locales/en.js +55 -0
  76. package/src/i18n/locales/sw.js +48 -0
  77. package/src/index.js +5 -144
  78. package/src/logger/formatters/PrettyFormatter.js +103 -57
  79. package/src/logger/internal.js +2 -2
  80. package/src/logger/patchConsole.js +91 -81
  81. package/src/middleware/MiddlewareRegistry.js +62 -82
  82. package/src/migrations/system/0001_users.js +21 -0
  83. package/src/migrations/system/0002_admin_log.js +25 -0
  84. package/src/migrations/system/0003_sessions.js +23 -0
  85. package/src/orm/fields/index.js +210 -188
  86. package/src/orm/migration/DefaultValueParser.js +325 -0
  87. package/src/orm/migration/InteractiveResolver.js +191 -0
  88. package/src/orm/migration/Makemigrations.js +312 -0
  89. package/src/orm/migration/MigrationGraph.js +227 -0
  90. package/src/orm/migration/MigrationRunner.js +202 -108
  91. package/src/orm/migration/MigrationWriter.js +463 -0
  92. package/src/orm/migration/ModelInspector.js +412 -344
  93. package/src/orm/migration/ModelScanner.js +225 -0
  94. package/src/orm/migration/ProjectState.js +213 -0
  95. package/src/orm/migration/RenameDetector.js +175 -0
  96. package/src/orm/migration/SchemaBuilder.js +8 -81
  97. package/src/orm/migration/operations/base.js +57 -0
  98. package/src/orm/migration/operations/column.js +191 -0
  99. package/src/orm/migration/operations/fields.js +252 -0
  100. package/src/orm/migration/operations/index.js +55 -0
  101. package/src/orm/migration/operations/models.js +152 -0
  102. package/src/orm/migration/operations/registry.js +131 -0
  103. package/src/orm/migration/operations/special.js +51 -0
  104. package/src/orm/migration/utils.js +208 -0
  105. package/src/orm/model/Model.js +81 -13
  106. package/src/providers/AdminServiceProvider.js +66 -9
  107. package/src/providers/AuthServiceProvider.js +46 -7
  108. package/src/providers/CacheStorageServiceProvider.js +5 -3
  109. package/src/providers/DatabaseServiceProvider.js +3 -2
  110. package/src/providers/EventServiceProvider.js +2 -1
  111. package/src/providers/LogServiceProvider.js +7 -3
  112. package/src/providers/MailServiceProvider.js +4 -3
  113. package/src/providers/QueueServiceProvider.js +4 -3
  114. package/src/router/Router.js +119 -152
  115. package/src/scaffold/templates.js +83 -26
  116. package/src/facades/Validation.js +0 -69
@@ -0,0 +1,463 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * MigrationWriter
5
+ *
6
+ * Diffs two ProjectState objects and produces a list of Operations,
7
+ * then renders them as a clean, Django-style migration file.
8
+ *
9
+ * Generated file format mirrors Django closely:
10
+ *
11
+ * // Generated by Millas 0.2.12 on 2026-03-17 14:30
12
+ * const { migrations, fields } = require('millas/core/db');
13
+ *
14
+ * module.exports = class Migration {
15
+ * static initial = true; // first migration only
16
+ * static dependencies = [
17
+ * ['system', '0001_users'],
18
+ * ];
19
+ * static operations = [
20
+ * migrations.CreateModel({
21
+ * name: 'Student',
22
+ * fields: [
23
+ * ['id', fields.id()],
24
+ * ['name', fields.string({ max: 100 })],
25
+ * ['age', fields.integer()],
26
+ * ],
27
+ * }),
28
+ * migrations.AddField({
29
+ * modelName: 'student',
30
+ * name: 'slug',
31
+ * field: fields.string({ max: 255 }),
32
+ * preserveDefault: false,
33
+ * }),
34
+ * ];
35
+ * };
36
+ */
37
+
38
+ const { fieldsEqual } = require('./utils');
39
+
40
+ const MILLAS_VERSION = (() => {
41
+ try { return require('../../..').version || require('../../../package.json').version; } catch {}
42
+ try { return require('../../../package.json').version; } catch {}
43
+ return '0.x';
44
+ })();
45
+
46
+ // Tables created by system migrations — never appear in app migrations.
47
+ // NOTE: 'users' is intentionally absent — the app owns its own user table
48
+ // by defining a User model that extends AuthUser (abstract). Same as Django's
49
+ // AUTH_USER_MODEL pattern where AbstractUser creates no table.
50
+ const SYSTEM_TABLES = new Set([
51
+ 'millas_admin_log',
52
+ 'millas_sessions',
53
+ 'millas_migrations',
54
+ ]);
55
+
56
+ class MigrationWriter {
57
+
58
+ // ─── Diff ──────────────────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Diff history state vs current state → list of operation descriptors.
62
+ */
63
+ diff(historyState, currentState) {
64
+ const ops = [];
65
+ const histSch = historyState.toSchema();
66
+ const currSch = currentState.toSchema();
67
+
68
+ // New tables
69
+ const newTableOps = [];
70
+ for (const table of Object.keys(currSch)) {
71
+ if (SYSTEM_TABLES.has(table)) continue;
72
+ if (!histSch[table]) {
73
+ newTableOps.push({ type: 'CreateModel', table, fields: currSch[table] });
74
+ }
75
+ }
76
+
77
+ // Topologically sort new CreateModel ops so that if Post has a FK → users,
78
+ // CreateModel(users) appears before CreateModel(posts) in the migration file.
79
+ // Cycles are detected and noted — the two-pass FK creation in Operations.js
80
+ // handles them safely at migrate time.
81
+ ops.push(...this._sortCreateModels(newTableOps));
82
+
83
+ // Dropped tables
84
+ for (const table of Object.keys(histSch)) {
85
+ if (SYSTEM_TABLES.has(table)) continue;
86
+ if (!currSch[table]) {
87
+ ops.push({ type: 'DeleteModel', table, fields: histSch[table] });
88
+ }
89
+ }
90
+
91
+ // Column-level changes
92
+ for (const table of Object.keys(currSch)) {
93
+ if (SYSTEM_TABLES.has(table)) continue;
94
+ if (!histSch[table]) continue;
95
+
96
+ const curr = currSch[table];
97
+ const prev = histSch[table];
98
+
99
+ for (const col of Object.keys(curr)) {
100
+ if (!prev[col]) {
101
+ const field = curr[col];
102
+ const isDangerous = !field.nullable &&
103
+ (field.default === null || field.default === undefined) &&
104
+ field.type !== 'id' &&
105
+ !!histSch[table];
106
+ ops.push({ type: 'AddField', table, column: col, field, _needsDefault: isDangerous });
107
+ } else if (!this._fieldsEqual(curr[col], prev[col])) {
108
+ ops.push({ type: 'AlterField', table, column: col, field: curr[col], previousField: prev[col] });
109
+ }
110
+ }
111
+
112
+ for (const col of Object.keys(prev)) {
113
+ if (!curr[col]) {
114
+ ops.push({ type: 'RemoveField', table, column: col, field: prev[col] });
115
+ }
116
+ }
117
+ }
118
+
119
+ return ops;
120
+ }
121
+
122
+ // ─── Render ────────────────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Render a complete migration file as JavaScript source.
126
+ *
127
+ * @param {Array<object>} ops — from diff(), already resolved
128
+ * @param {Array<Array>} dependencies — [[source, migName], ...]
129
+ * @param {string} name — snake_case migration name
130
+ * @param {object} meta — { initial?, number, date }
131
+ */
132
+ render(ops, dependencies, name, meta = {}) {
133
+ const date = meta.date || new Date().toISOString().replace('T', ' ').slice(0, 16);
134
+ const isInitial = meta.initial || false;
135
+
136
+ // ── Header ────────────────────────────────────────────────────────────────
137
+ const header = `// Generated by Millas ${MILLAS_VERSION} on ${date}`;
138
+
139
+ // ── Dependencies ──────────────────────────────────────────────────────────
140
+ const depsCode = dependencies.length > 0
141
+ ? `[\n${dependencies.map(([s, n]) => ` ['${s}', '${n}'],`).join('\n')}\n ]`
142
+ : '[]';
143
+
144
+ // ── Operations ────────────────────────────────────────────────────────────
145
+ const opsCode = ops.map(op => ' ' + this._renderOp(op)).join(',\n\n');
146
+
147
+ // ── initial flag ─────────────────────────────────────────────────────────
148
+ const initialLine = isInitial ? '\n static initial = true;\n' : '';
149
+
150
+ return `${header}
151
+
152
+ const { migrations, fields } = require('millas/core/db');
153
+
154
+ module.exports = class Migration {
155
+ static dependencies = ${depsCode};
156
+ ${initialLine}
157
+ static operations = [
158
+ ${opsCode},
159
+ ];
160
+ };
161
+ `;
162
+ }
163
+
164
+ // ─── Operation rendering ──────────────────────────────────────────────────
165
+
166
+ _renderOp(op) {
167
+ switch (op.type) {
168
+
169
+ case 'CreateModel':
170
+ return `migrations.CreateModel({\n` +
171
+ ` name: '${this._modelName(op.table)}',\n` +
172
+ ` fields: [\n` +
173
+ Object.entries(op.fields)
174
+ .map(([col, def]) => ` ['${col}', ${this._renderField(def)}],`)
175
+ .join('\n') + '\n' +
176
+ ` ],\n` +
177
+ ` })`;
178
+
179
+ case 'DeleteModel':
180
+ // Django omits fields= — not needed in the migration file
181
+ return `migrations.DeleteModel({\n` +
182
+ ` name: '${this._modelName(op.table)}',\n` +
183
+ ` })`;
184
+
185
+ case 'AddField': {
186
+ const lines = [
187
+ ` modelName: '${op.table}',`,
188
+ ` name: '${op.column}',`,
189
+ ` field: ${this._renderField(op.field)},`,
190
+ ];
191
+ if (op.oneOffDefault !== undefined) {
192
+ lines.push(` oneOffDefault: ${this._renderDefault(op.oneOffDefault)},`);
193
+ lines.push(` preserveDefault: false,`);
194
+ }
195
+ return `migrations.AddField({\n${lines.join('\n')}\n })`;
196
+ }
197
+
198
+ case 'RemoveField':
199
+ // Django omits field= — it's not needed in the migration file
200
+ return `migrations.RemoveField({\n` +
201
+ ` modelName: '${op.table}',\n` +
202
+ ` name: '${op.column}',\n` +
203
+ ` })`;
204
+
205
+ case 'AlterField':
206
+ return `migrations.AlterField({\n` +
207
+ ` modelName: '${op.table}',\n` +
208
+ ` name: '${op.column}',\n` +
209
+ ` field: ${this._renderField(op.field)},\n` +
210
+ ` })`;
211
+
212
+ case 'RenameField':
213
+ return `migrations.RenameField({\n` +
214
+ ` modelName: '${op.table}',\n` +
215
+ ` oldName: '${op.oldColumn}',\n` +
216
+ ` newName: '${op.newColumn}',\n` +
217
+ ` })`;
218
+
219
+ case 'RenameModel':
220
+ return `migrations.RenameModel({\n` +
221
+ ` oldName: '${op.oldTable}',\n` +
222
+ ` newName: '${op.newTable}',\n` +
223
+ ` })`;
224
+
225
+ case 'RunSQL':
226
+ return `migrations.RunSQL({\n` +
227
+ ` sql: ${JSON.stringify(op.sql)},\n` +
228
+ (op.reverseSql ? ` reverseSql: ${JSON.stringify(op.reverseSql)},\n` : '') +
229
+ ` })`;
230
+
231
+ default:
232
+ return `/* unknown: ${JSON.stringify(op)} */`;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Render a field definition as a readable `fields.xxx(...)` call.
238
+ * This is the key readability improvement over JSON.stringify.
239
+ *
240
+ * Examples:
241
+ * fields.id()
242
+ * fields.string({ max: 100 })
243
+ * fields.string({ max: 255, unique: true })
244
+ * fields.integer({ nullable: true })
245
+ * fields.boolean({ default: false })
246
+ * fields.decimal(10, 4)
247
+ * fields.decimal(10, 4, { default: 0 })
248
+ * fields.enum(['admin', 'user'], { default: 'user' })
249
+ * fields.timestamp({ nullable: true })
250
+ */
251
+ _renderField(def) {
252
+ if (!def) return 'fields.string()';
253
+
254
+ // ── ForeignKey / OneToOne ─────────────────────────────────────────────────
255
+ // Both are stored as type:'integer' with _isForeignKey:true after normalisation.
256
+ // Render as fields.ForeignKey('table', {...}) / fields.OneToOne('table', {...})
257
+ // so the generated migration file is human-readable and round-trips correctly.
258
+ if (def._isForeignKey && def.references) {
259
+ const fn = def._isOneToOne ? 'OneToOne' : 'ForeignKey';
260
+ const table = def.references.table;
261
+ const opts = {};
262
+ if (def.nullable) opts.nullable = true;
263
+ const onDelete = def._fkOnDelete ?? def.references.onDelete ?? 'CASCADE';
264
+ if (onDelete !== 'CASCADE') opts.onDelete = onDelete;
265
+ const toField = def.references.column ?? 'id';
266
+ if (toField !== 'id') opts.toField = toField;
267
+ if (def._fkRelatedName) opts.relatedName = def._fkRelatedName;
268
+ const optsStr = Object.keys(opts).length
269
+ ? `, ${this._renderOpts(opts)}`
270
+ : '';
271
+ return `fields.${fn}('${table}'${optsStr})`;
272
+ }
273
+
274
+ const type = def.type || 'string';
275
+
276
+ // ── decimal: positional args (precision, scale, options) ─────────────────
277
+ // fields.decimal() signature is decimal(precision=8, scale=2, options={})
278
+ // We must NOT pass precision/scale in the options object — they are positional.
279
+ if (type === 'decimal') {
280
+ const precision = def.precision ?? 8;
281
+ const scale = def.scale ?? 2;
282
+ const isDefaultPrecision = precision === 8;
283
+ const isDefaultScale = scale === 2;
284
+
285
+ // Extra options (anything besides precision/scale)
286
+ const extraOpts = {};
287
+ if (def.nullable === true) extraOpts.nullable = true;
288
+ if (def.unique === true) extraOpts.unique = true;
289
+ if (def.default !== null && def.default !== undefined) extraOpts.default = def.default;
290
+
291
+ const hasExtra = Object.keys(extraOpts).length > 0;
292
+
293
+ if (isDefaultPrecision && isDefaultScale && !hasExtra) {
294
+ // fields.decimal()
295
+ return 'fields.decimal()';
296
+ }
297
+ if (isDefaultPrecision && isDefaultScale && hasExtra) {
298
+ // fields.decimal(8, 2, { default: 0 }) — but since 8,2 are defaults, omit them
299
+ // Actually we must pass them explicitly so the reader gets correct positional args
300
+ return `fields.decimal(8, 2, ${this._renderOpts(extraOpts)})`;
301
+ }
302
+ if (!hasExtra) {
303
+ // fields.decimal(10, 4)
304
+ return `fields.decimal(${precision}, ${scale})`;
305
+ }
306
+ // fields.decimal(10, 4, { default: 0 })
307
+ return `fields.decimal(${precision}, ${scale}, ${this._renderOpts(extraOpts)})`;
308
+ }
309
+
310
+ // Collect non-default options only — keeps output clean
311
+ const opts = {};
312
+ if (def.nullable === true) opts.nullable = true;
313
+ if (def.unique === true) opts.unique = true;
314
+ if (def.unsigned === true) opts.unsigned = true;
315
+ if (def.default !== null && def.default !== undefined) opts.default = def.default;
316
+ if (def.max !== null && def.max !== undefined && type !== 'id') opts.max = def.max;
317
+ if (def.references) opts.references = def.references;
318
+
319
+ switch (type) {
320
+ case 'id':
321
+ return 'fields.id()';
322
+
323
+ case 'enum': {
324
+ const vals = JSON.stringify(def.enumValues || []);
325
+ const optsStr = Object.keys(opts).length
326
+ ? `, ${this._renderOpts(opts)}`
327
+ : '';
328
+ return `fields.enum(${vals}${optsStr})`;
329
+ }
330
+
331
+ default: {
332
+ const optsStr = Object.keys(opts).length
333
+ ? `(${this._renderOpts(opts)})`
334
+ : '()';
335
+ return `fields.${type}${optsStr}`;
336
+ }
337
+ }
338
+ }
339
+
340
+ _renderOpts(opts) {
341
+ if (Object.keys(opts).length === 0) return '';
342
+ const entries = Object.entries(opts).map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
343
+ return `{ ${entries.join(', ')} }`;
344
+ }
345
+
346
+ /**
347
+ * Render a oneOffDefault descriptor.
348
+ * Callable → readable expression string reference.
349
+ * Literal → the value itself.
350
+ */
351
+ _renderDefault(descriptor) {
352
+ if (descriptor === null || descriptor === undefined) return 'null';
353
+ if (typeof descriptor === 'object' && 'kind' in descriptor) {
354
+ if (descriptor.kind === 'callable') return descriptor.expression;
355
+ if (descriptor.kind === 'literal') return JSON.stringify(descriptor.value);
356
+ }
357
+ return JSON.stringify(descriptor); // legacy
358
+ }
359
+
360
+ // ─── Naming helpers ───────────────────────────────────────────────────────
361
+
362
+ /**
363
+ * Return the table name as-is for use in migration files.
364
+ * We write the actual table name (e.g. 'landlord_verification') directly
365
+ * so that _tableFromName() in the migrations proxy gets back the exact
366
+ * same string — no lossy PascalCase round-trip.
367
+ */
368
+ _modelName(table) {
369
+ return table;
370
+ }
371
+
372
+ /**
373
+ * Topologically sort a list of CreateModel ops by their FK dependencies.
374
+ *
375
+ * If Post has a FK → users, CreateModel(users) must come before
376
+ * CreateModel(posts). This makes the generated migration file readable
377
+ * and correct for most cases.
378
+ *
379
+ * Circular references (A → B → A) are detected. The sort still completes —
380
+ * cycles are broken arbitrarily and a warning is emitted. The two-pass
381
+ * FK creation in Operations.CreateModel.up() handles the actual DB safety.
382
+ *
383
+ * @param {Array<object>} createOps — CreateModel op descriptors
384
+ * @returns {Array<object>} — same ops, dependency-first order
385
+ */
386
+ _sortCreateModels(createOps) {
387
+ if (createOps.length <= 1) return createOps;
388
+
389
+ // Build a set of tables being created in this batch
390
+ const newTables = new Set(createOps.map(op => op.table));
391
+
392
+ // Build adjacency: table → Set of tables it depends on (via FK).
393
+ // Only track deps within this batch — cross-migration deps are handled
394
+ // by migration-level dependencies[], not op ordering.
395
+ const deps = new Map();
396
+ for (const op of createOps) {
397
+ const opDeps = new Set();
398
+ for (const def of Object.values(op.fields)) {
399
+ const ref = def.references;
400
+ if (ref && ref.table && newTables.has(ref.table) && ref.table !== op.table) {
401
+ opDeps.add(ref.table);
402
+ }
403
+ }
404
+ deps.set(op.table, opDeps);
405
+ }
406
+
407
+ // Kahn's algorithm — processes cycles gracefully without recursion
408
+ // in-degree here means: how many tables in this batch must be created
409
+ // BEFORE this table (i.e. how many tables does this table depend on).
410
+ const inDegree = new Map();
411
+ for (const op of createOps) inDegree.set(op.table, 0);
412
+ for (const [table, tableDeps] of deps) {
413
+ // table depends on each dep → table's in-degree increases by 1 per dep
414
+ inDegree.set(table, (inDegree.get(table) || 0) + tableDeps.size);
415
+ }
416
+
417
+ // Tables with no in-edges (nothing depends on them yet) go first
418
+ const queue = createOps
419
+ .filter(op => inDegree.get(op.table) === 0)
420
+ .map(op => op.table)
421
+ .sort(); // sort for determinism
422
+
423
+ const tableIndex = new Map(createOps.map(op => [op.table, op]));
424
+ const sorted = [];
425
+ const remaining = new Set(createOps.map(op => op.table));
426
+
427
+ while (queue.length > 0) {
428
+ queue.sort(); // deterministic pick among ready nodes
429
+ const table = queue.shift();
430
+ remaining.delete(table);
431
+ sorted.push(tableIndex.get(table));
432
+
433
+ // Reduce in-degree for every table that depended on the one we just placed
434
+ for (const [candidate, candidateDeps] of deps) {
435
+ if (candidateDeps.has(table)) {
436
+ const newDeg = (inDegree.get(candidate) || 0) - 1;
437
+ inDegree.set(candidate, newDeg);
438
+ if (newDeg === 0 && remaining.has(candidate)) queue.push(candidate);
439
+ }
440
+ }
441
+ }
442
+
443
+ // Any remaining nodes are part of a cycle — append with a warning.
444
+ // Operations.CreateModel.up() handles them safely via deferred FK constraints.
445
+ if (remaining.size > 0) {
446
+ const cycleList = [...remaining].sort().join(', ');
447
+ process.stderr.write(
448
+ `[millas] Warning: circular FK reference detected between tables: ${cycleList}.\n` +
449
+ ` Tables will still be created safely (FK constraints applied after all tables exist),\n` +
450
+ ` but you should review the relationship design.\n`
451
+ );
452
+ for (const table of [...remaining].sort()) {
453
+ sorted.push(tableIndex.get(table));
454
+ }
455
+ }
456
+
457
+ return sorted;
458
+ }
459
+
460
+ _fieldsEqual(a, b) { return fieldsEqual(a, b); }
461
+ }
462
+
463
+ module.exports = MigrationWriter;