millas 0.2.12-beta-1 → 0.2.13

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 (120) hide show
  1. package/package.json +3 -2
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +516 -199
  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 +318 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +393 -97
  12. package/src/admin/static/admin.css +1422 -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 +87 -1046
  19. package/src/admin/views/pages/detail.njk +56 -21
  20. package/src/admin/views/pages/error.njk +65 -0
  21. package/src/admin/views/pages/form.njk +47 -599
  22. package/src/admin/views/pages/list.njk +270 -62
  23. package/src/admin/views/partials/form-field.njk +53 -0
  24. package/src/admin/views/partials/form-footer.njk +28 -0
  25. package/src/admin/views/partials/form-readonly.njk +114 -0
  26. package/src/admin/views/partials/form-scripts.njk +480 -0
  27. package/src/admin/views/partials/form-widget.njk +297 -0
  28. package/src/admin/views/partials/icons.njk +64 -0
  29. package/src/admin/views/partials/json-dialog.njk +80 -0
  30. package/src/admin/views/partials/json-editor.njk +37 -0
  31. package/src/ai/AIManager.js +954 -0
  32. package/src/ai/AITokenBudget.js +250 -0
  33. package/src/ai/PromptGuard.js +216 -0
  34. package/src/ai/agents.js +218 -0
  35. package/src/ai/conversation.js +213 -0
  36. package/src/ai/drivers.js +734 -0
  37. package/src/ai/files.js +249 -0
  38. package/src/ai/media.js +303 -0
  39. package/src/ai/pricing.js +152 -0
  40. package/src/ai/provider_tools.js +114 -0
  41. package/src/ai/types.js +356 -0
  42. package/src/auth/Auth.js +18 -2
  43. package/src/auth/AuthUser.js +65 -44
  44. package/src/cli.js +3 -1
  45. package/src/commands/createsuperuser.js +267 -0
  46. package/src/commands/lang.js +589 -0
  47. package/src/commands/migrate.js +154 -81
  48. package/src/commands/serve.js +3 -4
  49. package/src/container/AppInitializer.js +101 -20
  50. package/src/container/Application.js +31 -1
  51. package/src/container/MillasApp.js +10 -3
  52. package/src/container/MillasConfig.js +35 -6
  53. package/src/core/admin.js +5 -0
  54. package/src/core/db.js +2 -1
  55. package/src/core/foundation.js +2 -10
  56. package/src/core/lang.js +1 -0
  57. package/src/errors/HttpError.js +32 -16
  58. package/src/facades/AI.js +411 -0
  59. package/src/facades/Hash.js +67 -0
  60. package/src/facades/Process.js +144 -0
  61. package/src/hashing/Hash.js +262 -0
  62. package/src/http/HtmlEscape.js +162 -0
  63. package/src/http/MillasRequest.js +63 -7
  64. package/src/http/MillasResponse.js +70 -4
  65. package/src/http/ResponseDispatcher.js +21 -27
  66. package/src/http/SafeFilePath.js +195 -0
  67. package/src/http/SafeRedirect.js +62 -0
  68. package/src/http/SecurityBootstrap.js +70 -0
  69. package/src/http/helpers.js +40 -125
  70. package/src/http/index.js +10 -1
  71. package/src/http/middleware/CsrfMiddleware.js +258 -0
  72. package/src/http/middleware/RateLimiter.js +314 -0
  73. package/src/http/middleware/SecurityHeaders.js +281 -0
  74. package/src/i18n/I18nServiceProvider.js +91 -0
  75. package/src/i18n/Translator.js +643 -0
  76. package/src/i18n/defaults.js +122 -0
  77. package/src/i18n/index.js +164 -0
  78. package/src/i18n/locales/en.js +55 -0
  79. package/src/i18n/locales/sw.js +48 -0
  80. package/src/logger/LogRedactor.js +247 -0
  81. package/src/logger/Logger.js +1 -1
  82. package/src/logger/formatters/JsonFormatter.js +11 -4
  83. package/src/logger/formatters/PrettyFormatter.js +103 -65
  84. package/src/logger/formatters/SimpleFormatter.js +14 -3
  85. package/src/middleware/ThrottleMiddleware.js +27 -4
  86. package/src/migrations/system/0001_users.js +21 -0
  87. package/src/migrations/system/0002_admin_log.js +25 -0
  88. package/src/migrations/system/0003_sessions.js +23 -0
  89. package/src/orm/fields/index.js +210 -188
  90. package/src/orm/migration/DefaultValueParser.js +325 -0
  91. package/src/orm/migration/InteractiveResolver.js +191 -0
  92. package/src/orm/migration/Makemigrations.js +312 -0
  93. package/src/orm/migration/MigrationGraph.js +227 -0
  94. package/src/orm/migration/MigrationRunner.js +202 -108
  95. package/src/orm/migration/MigrationWriter.js +463 -0
  96. package/src/orm/migration/ModelInspector.js +143 -74
  97. package/src/orm/migration/ModelScanner.js +225 -0
  98. package/src/orm/migration/ProjectState.js +213 -0
  99. package/src/orm/migration/RenameDetector.js +175 -0
  100. package/src/orm/migration/SchemaBuilder.js +8 -81
  101. package/src/orm/migration/operations/base.js +57 -0
  102. package/src/orm/migration/operations/column.js +191 -0
  103. package/src/orm/migration/operations/fields.js +252 -0
  104. package/src/orm/migration/operations/index.js +55 -0
  105. package/src/orm/migration/operations/models.js +152 -0
  106. package/src/orm/migration/operations/registry.js +131 -0
  107. package/src/orm/migration/operations/special.js +51 -0
  108. package/src/orm/migration/utils.js +208 -0
  109. package/src/orm/model/Model.js +81 -13
  110. package/src/process/Process.js +333 -0
  111. package/src/providers/AdminServiceProvider.js +66 -9
  112. package/src/providers/AuthServiceProvider.js +40 -5
  113. package/src/providers/CacheStorageServiceProvider.js +2 -2
  114. package/src/providers/DatabaseServiceProvider.js +3 -2
  115. package/src/providers/LogServiceProvider.js +4 -1
  116. package/src/providers/MailServiceProvider.js +1 -1
  117. package/src/providers/QueueServiceProvider.js +1 -1
  118. package/src/router/MiddlewareRegistry.js +27 -2
  119. package/src/scaffold/templates.js +80 -21
  120. package/src/validation/Validator.js +348 -607
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * column.js
5
+ *
6
+ * Knex column builder helpers shared across all field-level operations.
7
+ *
8
+ * Having these in one place means:
9
+ * - The type → knex method mapping is never duplicated
10
+ * - AlterField reuses the same logic as AddField, with `.alter()` appended
11
+ * - FK constraint attachment is explicit and separated from column creation
12
+ *
13
+ * Exports:
14
+ * applyColumn(t, name, def) — add a new column to a table builder
15
+ * alterColumn(t, name, def) — modify an existing column (.alter())
16
+ * attachFKConstraints(db, table, fields) — attach FK constraints via ALTER TABLE
17
+ * after all tables in a migration exist
18
+ */
19
+
20
+ // ─── Core column builder ──────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Add a single column to a knex table builder.
24
+ *
25
+ * Handles all supported field types, nullability, uniqueness, defaults,
26
+ * and inline FK constraints (references).
27
+ *
28
+ * Pass `{ ...def, references: null }` to suppress FK constraint creation
29
+ * when deferring constraints to a later ALTER TABLE pass.
30
+ *
31
+ * @param {object} t — knex table builder (from createTable / table callback)
32
+ * @param {string} name — column name
33
+ * @param {object} def — normalised field definition from ProjectState.normaliseField()
34
+ */
35
+ function applyColumn(t, name, def) {
36
+ const col = _buildColumn(t, name, def);
37
+ if (!col) return; // 'id' handled internally by _buildColumn
38
+
39
+ _applyModifiers(col, def);
40
+ }
41
+
42
+ /**
43
+ * Modify an existing column in a knex alterTable builder.
44
+ * Identical to applyColumn but appends `.alter()` — required by knex to
45
+ * signal that this is a column modification, not a new column addition.
46
+ *
47
+ * Note: FK constraints are NOT altered here — use attachFKConstraints()
48
+ * to manage them separately. Most DBs require DROP CONSTRAINT + re-add
49
+ * for FK changes, which is safer to do explicitly.
50
+ *
51
+ * @param {object} t — knex table builder (from alterTable callback)
52
+ * @param {string} name — column name
53
+ * @param {object} def — normalised field definition
54
+ */
55
+ function alterColumn(t, name, def) {
56
+ const col = _buildColumn(t, name, def, { forAlter: true });
57
+ if (!col) return;
58
+
59
+ _applyModifiers(col, def, { skipFK: true }); // FKs not altered inline
60
+ col.alter();
61
+ }
62
+
63
+ /**
64
+ * Attach FK constraints for a set of fields on a table.
65
+ *
66
+ * Called by MigrationRunner AFTER all tables in a migration have been
67
+ * created — this guarantees all referenced tables exist.
68
+ *
69
+ * All FK columns for a given table are batched into a single ALTER TABLE
70
+ * statement, not one per column.
71
+ *
72
+ * @param {import('knex').Knex} db
73
+ * @param {string} table — table name
74
+ * @param {object} fields — { columnName: normalisedDef, ... }
75
+ */
76
+ async function attachFKConstraints(db, table, fields) {
77
+ const fkEntries = Object.entries(fields).filter(([, def]) => def.references);
78
+
79
+ if (fkEntries.length === 0) return;
80
+
81
+ await db.schema.alterTable(table, (t) => {
82
+ for (const [name, def] of fkEntries) {
83
+ const ref = def.references;
84
+ t.foreign(name)
85
+ .references(ref.column)
86
+ .inTable(ref.table)
87
+ .onDelete(ref.onDelete || 'CASCADE');
88
+ }
89
+ });
90
+ }
91
+
92
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
93
+
94
+ /**
95
+ * Build a knex column builder for a given field type.
96
+ * Returns null for 'id' fields (handled by t.increments which returns void).
97
+ *
98
+ * @param {object} t
99
+ * @param {string} name
100
+ * @param {object} def
101
+ * @param {object} [opts]
102
+ * @param {boolean} [opts.forAlter] — if true, skip t.increments (can't alter PK)
103
+ * @returns {object|null} knex column builder
104
+ */
105
+ function _buildColumn(t, name, def, opts = {}) {
106
+ switch (def.type) {
107
+ case 'id':
108
+ if (!opts.forAlter) t.increments(name).primary();
109
+ return null; // increments() doesn't return a chainable column builder
110
+
111
+ case 'string':
112
+ return t.string(name, def.max || 255);
113
+
114
+ case 'text':
115
+ return t.text(name);
116
+
117
+ case 'integer':
118
+ return def.unsigned
119
+ ? t.integer(name).unsigned()
120
+ : t.integer(name);
121
+
122
+ case 'bigInteger':
123
+ return def.unsigned
124
+ ? t.bigInteger(name).unsigned()
125
+ : t.bigInteger(name);
126
+
127
+ case 'float':
128
+ return t.float(name);
129
+
130
+ case 'decimal':
131
+ return t.decimal(name, def.precision || 8, def.scale || 2);
132
+
133
+ case 'boolean':
134
+ return t.boolean(name);
135
+
136
+ case 'json':
137
+ return t.json(name);
138
+
139
+ case 'date':
140
+ return t.date(name);
141
+
142
+ case 'timestamp':
143
+ return t.timestamp(name, { useTz: false });
144
+
145
+ case 'enum':
146
+ return t.enu(name, def.enumValues || []);
147
+
148
+ case 'uuid':
149
+ return t.uuid(name);
150
+
151
+ default:
152
+ return t.string(name); // safe fallback
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Apply nullability, uniqueness, default, and FK constraint modifiers
158
+ * to an already-built knex column builder.
159
+ *
160
+ * @param {object} col — knex column builder
161
+ * @param {object} def — normalised field def
162
+ * @param {object} [opts]
163
+ * @param {boolean} [opts.skipFK] — skip FK constraint (used by alterColumn)
164
+ */
165
+ function _applyModifiers(col, def, opts = {}) {
166
+ // Nullability
167
+ if (def.nullable) {
168
+ col.nullable();
169
+ } else if (def.type !== 'id') {
170
+ col.notNullable();
171
+ }
172
+
173
+ // Uniqueness
174
+ if (def.unique) col.unique();
175
+
176
+ // Default value
177
+ if (def.default !== null && def.default !== undefined) {
178
+ col.defaultTo(def.default);
179
+ }
180
+
181
+ // Inline FK constraint — skipped when deferring to attachFKConstraints()
182
+ if (!opts.skipFK && def.references) {
183
+ const ref = def.references;
184
+ col
185
+ .references(ref.column)
186
+ .inTable(ref.table)
187
+ .onDelete(ref.onDelete || 'CASCADE');
188
+ }
189
+ }
190
+
191
+ module.exports = { applyColumn, alterColumn, attachFKConstraints };
@@ -0,0 +1,252 @@
1
+ 'use strict';
2
+
3
+ const { BaseOperation } = require('./base');
4
+ const { applyColumn, alterColumn } = require('./column');
5
+ const { normaliseField } = require('../ProjectState');
6
+ const { resolveDefault } = require('../DefaultValueParser');
7
+
8
+ /**
9
+ * fields.js
10
+ *
11
+ * Column-level migration operations:
12
+ * AddField — add a new column (with optional safe backfill for NOT NULL)
13
+ * RemoveField — drop a column
14
+ * AlterField — modify a column definition
15
+ * RenameField — rename a column
16
+ *
17
+ * Key improvement over the old Operations.js:
18
+ * AlterField no longer duplicates the type→knex switch. It delegates to
19
+ * alterColumn() from column.js — one place for all column-building logic.
20
+ */
21
+
22
+ // ─── AddField ─────────────────────────────────────────────────────────────────
23
+
24
+ class AddField extends BaseOperation {
25
+ /**
26
+ * @param {string} table
27
+ * @param {string} column
28
+ * @param {object} field — FieldDefinition or normalised plain object
29
+ * @param {object} [oneOffDefault] — backfill descriptor for existing rows.
30
+ * NOT part of the model schema — only lives in this migration file.
31
+ * Shape: { kind: 'literal', value } | { kind: 'callable', expression }
32
+ * Legacy: plain primitive (backward compat)
33
+ */
34
+ constructor(table, column, field, oneOffDefault = undefined) {
35
+ super();
36
+ this.type = 'AddField';
37
+ this.table = table;
38
+ this.column = column;
39
+ this.field = field;
40
+ this.oneOffDefault = oneOffDefault;
41
+ }
42
+
43
+ applyState(state) {
44
+ state.addField(this.table, this.column, this.field);
45
+ }
46
+
47
+ async up(db) {
48
+ const def = normaliseField(this.field);
49
+ const hasBackfill = this.oneOffDefault !== undefined && this.oneOffDefault !== null;
50
+ const needsSafe = hasBackfill && !def.nullable && def.default === null;
51
+
52
+ if (needsSafe) {
53
+ await this._safeBackfill(db, def);
54
+ } else {
55
+ await db.schema.table(this.table, (t) => {
56
+ applyColumn(t, this.column, def);
57
+ });
58
+ }
59
+ }
60
+
61
+ async down(db) {
62
+ await db.schema.table(this.table, (t) => {
63
+ t.dropColumn(this.column);
64
+ });
65
+ }
66
+
67
+ toJSON() {
68
+ const j = {
69
+ type: 'AddField',
70
+ table: this.table,
71
+ column: this.column,
72
+ field: this.field,
73
+ };
74
+ if (this.oneOffDefault !== undefined) j.oneOffDefault = this.oneOffDefault;
75
+ return j;
76
+ }
77
+
78
+ // ── Private ────────────────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Three-step safe backfill for adding a NOT NULL column to a non-empty table.
82
+ *
83
+ * Step 1 — Add column as nullable so existing rows don't immediately
84
+ * violate the NOT NULL constraint.
85
+ * Step 2 — Backfill existing rows with the one-off default value.
86
+ * Callable defaults (uuid, timestamp) are invoked per row so
87
+ * each row gets its own unique value.
88
+ * Literal defaults are applied in a single bulk UPDATE.
89
+ * Step 3 — Tighten the column to NOT NULL now that all rows have a value.
90
+ */
91
+ async _safeBackfill(db, def) {
92
+ const resolved = resolveDefault(this.oneOffDefault);
93
+ const isCallable = typeof resolved === 'function';
94
+
95
+ // Step 1: add as nullable
96
+ await db.schema.table(this.table, (t) => {
97
+ applyColumn(t, this.column, { ...def, nullable: true, default: null });
98
+ });
99
+
100
+ // Step 2: backfill
101
+ if (isCallable) {
102
+ // Callable — fetch PKs and update each row individually
103
+ const rows = await db(this.table).whereNull(this.column).select('id');
104
+ for (const row of rows) {
105
+ await db(this.table)
106
+ .where('id', row.id)
107
+ .update({ [this.column]: resolved() });
108
+ }
109
+ } else {
110
+ // Literal — single bulk UPDATE
111
+ await db(this.table)
112
+ .whereNull(this.column)
113
+ .update({ [this.column]: resolved });
114
+ }
115
+
116
+ // Step 3: tighten to NOT NULL
117
+ await db.schema.alterTable(this.table, (t) => {
118
+ alterColumn(t, this.column, { ...def, nullable: false });
119
+ });
120
+ }
121
+ }
122
+
123
+ // ─── RemoveField ──────────────────────────────────────────────────────────────
124
+
125
+ class RemoveField extends BaseOperation {
126
+ /**
127
+ * @param {string} table
128
+ * @param {string} column
129
+ * @param {object} field — kept for down() reconstruction
130
+ */
131
+ constructor(table, column, field) {
132
+ super();
133
+ this.type = 'RemoveField';
134
+ this.table = table;
135
+ this.column = column;
136
+ this.field = field;
137
+ }
138
+
139
+ applyState(state) {
140
+ state.removeField(this.table, this.column);
141
+ }
142
+
143
+ async up(db) {
144
+ await db.schema.table(this.table, (t) => {
145
+ t.dropColumn(this.column);
146
+ });
147
+ }
148
+
149
+ async down(db) {
150
+ await db.schema.table(this.table, (t) => {
151
+ applyColumn(t, this.column, normaliseField(this.field));
152
+ });
153
+ }
154
+
155
+ toJSON() {
156
+ return {
157
+ type: 'RemoveField',
158
+ table: this.table,
159
+ column: this.column,
160
+ field: this.field,
161
+ };
162
+ }
163
+ }
164
+
165
+ // ─── AlterField ───────────────────────────────────────────────────────────────
166
+
167
+ class AlterField extends BaseOperation {
168
+ /**
169
+ * @param {string} table
170
+ * @param {string} column
171
+ * @param {object} field — new field definition
172
+ * @param {object} previousField — old field definition (for down())
173
+ */
174
+ constructor(table, column, field, previousField) {
175
+ super();
176
+ this.type = 'AlterField';
177
+ this.table = table;
178
+ this.column = column;
179
+ this.field = field;
180
+ this.previousField = previousField;
181
+ }
182
+
183
+ applyState(state) {
184
+ state.alterField(this.table, this.column, this.field);
185
+ }
186
+
187
+ async up(db) {
188
+ await db.schema.alterTable(this.table, (t) => {
189
+ alterColumn(t, this.column, normaliseField(this.field));
190
+ });
191
+ }
192
+
193
+ async down(db) {
194
+ await db.schema.alterTable(this.table, (t) => {
195
+ alterColumn(t, this.column, normaliseField(this.previousField));
196
+ });
197
+ }
198
+
199
+ toJSON() {
200
+ return {
201
+ type: 'AlterField',
202
+ table: this.table,
203
+ column: this.column,
204
+ field: this.field,
205
+ previousField: this.previousField,
206
+ };
207
+ }
208
+ }
209
+
210
+ // ─── RenameField ──────────────────────────────────────────────────────────────
211
+
212
+ class RenameField extends BaseOperation {
213
+ /**
214
+ * @param {string} table
215
+ * @param {string} oldColumn
216
+ * @param {string} newColumn
217
+ */
218
+ constructor(table, oldColumn, newColumn) {
219
+ super();
220
+ this.type = 'RenameField';
221
+ this.table = table;
222
+ this.oldColumn = oldColumn;
223
+ this.newColumn = newColumn;
224
+ }
225
+
226
+ applyState(state) {
227
+ state.renameField(this.table, this.oldColumn, this.newColumn);
228
+ }
229
+
230
+ async up(db) {
231
+ await db.schema.table(this.table, (t) => {
232
+ t.renameColumn(this.oldColumn, this.newColumn);
233
+ });
234
+ }
235
+
236
+ async down(db) {
237
+ await db.schema.table(this.table, (t) => {
238
+ t.renameColumn(this.newColumn, this.oldColumn);
239
+ });
240
+ }
241
+
242
+ toJSON() {
243
+ return {
244
+ type: 'RenameField',
245
+ table: this.table,
246
+ oldColumn: this.oldColumn,
247
+ newColumn: this.newColumn,
248
+ };
249
+ }
250
+ }
251
+
252
+ module.exports = { AddField, RemoveField, AlterField, RenameField };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * operations/index.js
5
+ *
6
+ * Public surface of the operations/ folder.
7
+ *
8
+ * Import from here for the full set:
9
+ * require('./operations')
10
+ *
11
+ * Or import directly from a sub-module when you only need one concern:
12
+ * require('./operations/models') — CreateModel, DeleteModel, RenameModel
13
+ * require('./operations/fields') — AddField, RemoveField, AlterField, RenameField
14
+ * require('./operations/column') — applyColumn, alterColumn, attachFKConstraints
15
+ * require('./operations/registry') — deserialise, migrations proxy
16
+ * require('./operations/special') — RunSQL
17
+ */
18
+
19
+ const { BaseOperation } = require('./base');
20
+ const { applyColumn, alterColumn,
21
+ attachFKConstraints } = require('./column');
22
+ const { CreateModel, DeleteModel, RenameModel } = require('./models');
23
+ const { AddField, RemoveField,
24
+ AlterField, RenameField } = require('./fields');
25
+ const { RunSQL } = require('./special');
26
+ const { deserialise, migrations, _tableFromName } = require('./registry');
27
+
28
+ module.exports = {
29
+ // Base
30
+ BaseOperation,
31
+
32
+ // Column helpers
33
+ applyColumn,
34
+ alterColumn,
35
+ attachFKConstraints,
36
+
37
+ // Table-level ops
38
+ CreateModel,
39
+ DeleteModel,
40
+ RenameModel,
41
+
42
+ // Field-level ops
43
+ AddField,
44
+ RemoveField,
45
+ AlterField,
46
+ RenameField,
47
+
48
+ // Escape hatch
49
+ RunSQL,
50
+
51
+ // Registry
52
+ deserialise,
53
+ migrations,
54
+ _tableFromName,
55
+ };
@@ -0,0 +1,152 @@
1
+ 'use strict';
2
+
3
+ const { BaseOperation } = require('./base');
4
+ const { applyColumn, attachFKConstraints } = require('./column');
5
+ const { normaliseField } = require('../ProjectState');
6
+
7
+ /**
8
+ * models.js
9
+ *
10
+ * Table-level migration operations:
11
+ * CreateModel — CREATE TABLE
12
+ * DeleteModel — DROP TABLE
13
+ * RenameModel — RENAME TABLE
14
+ *
15
+ * FK constraint strategy (CreateModel):
16
+ * MigrationRunner calls upWithoutFKs() for every CreateModel in a migration
17
+ * first, then calls applyFKConstraints() for each — so all tables exist
18
+ * before any constraint is attached. This handles any ordering and circular
19
+ * references without extra DB round-trips per column.
20
+ *
21
+ * up() still creates with inline FKs for the single-CreateModel case
22
+ * (e.g. AddField to an existing schema) where ordering is never an issue.
23
+ */
24
+
25
+ // ─── CreateModel ──────────────────────────────────────────────────────────────
26
+
27
+ class CreateModel extends BaseOperation {
28
+ /**
29
+ * @param {string} table
30
+ * @param {object} fields — { columnName: normalisedFieldDef }
31
+ */
32
+ constructor(table, fields) {
33
+ super();
34
+ this.type = 'CreateModel';
35
+ this.table = table;
36
+ this.fields = fields;
37
+ }
38
+
39
+ applyState(state) {
40
+ state.createModel(this.table, this.fields);
41
+ }
42
+
43
+ // Standard up() — inline FKs. Safe when only one CreateModel in a migration.
44
+ async up(db) {
45
+ await db.schema.createTable(this.table, (t) => {
46
+ for (const [name, def] of Object.entries(this.fields)) {
47
+ applyColumn(t, name, normaliseField(def));
48
+ }
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Create the table with FK columns as plain integers (no constraints).
54
+ * Called by MigrationRunner phase 1 when a migration has multiple CreateModel ops.
55
+ */
56
+ async upWithoutFKs(db) {
57
+ await db.schema.createTable(this.table, (t) => {
58
+ for (const [name, def] of Object.entries(this.fields)) {
59
+ applyColumn(t, name, { ...normaliseField(def), references: null });
60
+ }
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Attach all FK constraints for this table in a single ALTER TABLE.
66
+ * Called by MigrationRunner phase 2, after all tables exist.
67
+ */
68
+ async applyFKConstraints(db) {
69
+ const normalisedFields = Object.fromEntries(
70
+ Object.entries(this.fields).map(([name, def]) => [name, normaliseField(def)])
71
+ );
72
+ await attachFKConstraints(db, this.table, normalisedFields);
73
+ }
74
+
75
+ async down(db) {
76
+ await db.schema.dropTableIfExists(this.table);
77
+ }
78
+
79
+ toJSON() {
80
+ return { type: 'CreateModel', table: this.table, fields: this.fields };
81
+ }
82
+ }
83
+
84
+ // ─── DeleteModel ──────────────────────────────────────────────────────────────
85
+
86
+ class DeleteModel extends BaseOperation {
87
+ /**
88
+ * @param {string} table
89
+ * @param {object} fields — kept for down() reconstruction only
90
+ */
91
+ constructor(table, fields) {
92
+ super();
93
+ this.type = 'DeleteModel';
94
+ this.table = table;
95
+ this.fields = fields;
96
+ }
97
+
98
+ applyState(state) {
99
+ state.deleteModel(this.table);
100
+ }
101
+
102
+ async up(db) {
103
+ await db.schema.dropTableIfExists(this.table);
104
+ }
105
+
106
+ // Reconstruct table on rollback. FK constraints included — the table
107
+ // being restored was previously fully formed.
108
+ async down(db) {
109
+ await db.schema.createTable(this.table, (t) => {
110
+ for (const [name, def] of Object.entries(this.fields)) {
111
+ applyColumn(t, name, normaliseField(def));
112
+ }
113
+ });
114
+ }
115
+
116
+ toJSON() {
117
+ return { type: 'DeleteModel', table: this.table, fields: this.fields };
118
+ }
119
+ }
120
+
121
+ // ─── RenameModel ──────────────────────────────────────────────────────────────
122
+
123
+ class RenameModel extends BaseOperation {
124
+ /**
125
+ * @param {string} oldTable
126
+ * @param {string} newTable
127
+ */
128
+ constructor(oldTable, newTable) {
129
+ super();
130
+ this.type = 'RenameModel';
131
+ this.oldTable = oldTable;
132
+ this.newTable = newTable;
133
+ }
134
+
135
+ applyState(state) {
136
+ state.renameModel(this.oldTable, this.newTable);
137
+ }
138
+
139
+ async up(db) {
140
+ await db.schema.renameTable(this.oldTable, this.newTable);
141
+ }
142
+
143
+ async down(db) {
144
+ await db.schema.renameTable(this.newTable, this.oldTable);
145
+ }
146
+
147
+ toJSON() {
148
+ return { type: 'RenameModel', oldTable: this.oldTable, newTable: this.newTable };
149
+ }
150
+ }
151
+
152
+ module.exports = { CreateModel, DeleteModel, RenameModel };