millas 0.2.12-beta-1 → 0.2.13-beta

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,213 @@
1
+ 'use strict';
2
+
3
+ const { tableFromClass, modelNameToTable, isSnakeCase } = require('./utils');
4
+
5
+ /**
6
+ * ProjectState
7
+ *
8
+ * An in-memory representation of the full database schema at a given point
9
+ * in migration history. Built by replaying migration operations in order.
10
+ *
11
+ * This is the Django-equivalent of ProjectState / ModelState.
12
+ * It is NEVER derived from the live database — only from migration files.
13
+ *
14
+ * Shape:
15
+ * state.models = Map<tableName, ModelState>
16
+ *
17
+ * ModelState:
18
+ * { table, fields: Map<columnName, FieldState>, meta: {} }
19
+ *
20
+ * FieldState (plain object, serialisable):
21
+ * { type, nullable, unique, default, max, unsigned, enumValues,
22
+ * references, precision, scale }
23
+ */
24
+ class ProjectState {
25
+ constructor() {
26
+ // Map<table, { table, fields: Map<name, fieldState> }>
27
+ this.models = new Map();
28
+ }
29
+
30
+ // ─── Mutation (called by operations during replay) ────────────────────────
31
+
32
+ createModel(table, fields) {
33
+ if (this.models.has(table)) {
34
+ throw new Error(`ProjectState: table "${table}" already exists`);
35
+ }
36
+ const fieldMap = new Map();
37
+ for (const [name, def] of Object.entries(fields)) {
38
+ fieldMap.set(name, normaliseField(def));
39
+ }
40
+ this.models.set(table, { table, fields: fieldMap });
41
+ }
42
+
43
+ deleteModel(table) {
44
+ this.models.delete(table);
45
+ }
46
+
47
+ addField(table, column, fieldDef) {
48
+ const model = this._requireModel(table);
49
+ if (model.fields.has(column)) {
50
+ throw new Error(`ProjectState: column "${column}" already exists on "${table}"`);
51
+ }
52
+ model.fields.set(column, normaliseField(fieldDef));
53
+ }
54
+
55
+ removeField(table, column) {
56
+ const model = this._requireModel(table);
57
+ model.fields.delete(column);
58
+ }
59
+
60
+ alterField(table, column, fieldDef) {
61
+ const model = this._requireModel(table);
62
+ model.fields.set(column, normaliseField(fieldDef));
63
+ }
64
+
65
+ renameField(table, oldColumn, newColumn) {
66
+ const model = this._requireModel(table);
67
+ const def = model.fields.get(oldColumn);
68
+ if (!def) throw new Error(`ProjectState: column "${oldColumn}" not found on "${table}"`);
69
+ model.fields.delete(oldColumn);
70
+ model.fields.set(newColumn, def);
71
+ }
72
+
73
+ renameModel(oldTable, newTable) {
74
+ const model = this._requireModel(oldTable);
75
+ this.models.delete(oldTable);
76
+ model.table = newTable;
77
+ this.models.set(newTable, model);
78
+ }
79
+
80
+ // ─── Queries ──────────────────────────────────────────────────────────────
81
+
82
+ hasTable(table) {
83
+ return this.models.has(table);
84
+ }
85
+
86
+ getFields(table) {
87
+ return this._requireModel(table).fields;
88
+ }
89
+
90
+ /** Return a plain-object snapshot of the full state (for diffing). */
91
+ toSchema() {
92
+ const schema = {};
93
+ for (const [table, model] of this.models) {
94
+ schema[table] = {};
95
+ for (const [col, def] of model.fields) {
96
+ schema[table][col] = { ...def };
97
+ }
98
+ }
99
+ return schema;
100
+ }
101
+
102
+ /** Deep clone — used to capture state at a point in time. */
103
+ clone() {
104
+ const copy = new ProjectState();
105
+ for (const [table, model] of this.models) {
106
+ const fieldMap = new Map();
107
+ for (const [col, def] of model.fields) {
108
+ fieldMap.set(col, { ...def });
109
+ }
110
+ copy.models.set(table, { table, fields: fieldMap });
111
+ }
112
+ return copy;
113
+ }
114
+
115
+ // ─── Internal ─────────────────────────────────────────────────────────────
116
+
117
+ _requireModel(table) {
118
+ const m = this.models.get(table);
119
+ if (!m) throw new Error(`ProjectState: table "${table}" not found`);
120
+ return m;
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Normalise a field definition (FieldDefinition instance or plain object)
126
+ * into a stable plain object for storage in ProjectState.
127
+ *
128
+ * Handles both:
129
+ * - Legacy foreignId() / raw references object → references: { table, column, onDelete }
130
+ * - Modern ForeignKey() / OneToOne() → _isForeignKey + _fkModel* resolved here
131
+ *
132
+ * For ForeignKey fields, the target table is resolved eagerly from _fkModel so that
133
+ * the migration system can diff and generate FK constraints correctly.
134
+ */
135
+ function normaliseField(def) {
136
+ if (!def) return { type: 'string', nullable: false, unique: false, default: null, max: null, unsigned: false, enumValues: null, references: null, precision: null, scale: null };
137
+ const type = def.type ?? 'string';
138
+ const precision = def.precision ?? (type === 'decimal' ? 8 : null);
139
+ const scale = def.scale ?? (type === 'decimal' ? 2 : null);
140
+
141
+ // ── Resolve modern ForeignKey / OneToOne references ───────────────────────
142
+ // fields.ForeignKey() stores metadata in _isForeignKey + _fkModel* rather
143
+ // than the legacy `references` plain object. Resolve that here so all
144
+ // downstream code (Operations, SchemaBuilder, MigrationWriter) sees a
145
+ // uniform `references: { table, column, onDelete }` shape.
146
+ let references = def.references ?? null;
147
+ if (def._isForeignKey && !references) {
148
+ const targetTable = _resolveTargetTable(def._fkModel, def._fkModelRef);
149
+ if (targetTable) {
150
+ references = {
151
+ table: targetTable,
152
+ column: def._fkToField ?? 'id',
153
+ onDelete: def._fkOnDelete ?? 'CASCADE',
154
+ };
155
+ }
156
+ }
157
+
158
+ return {
159
+ type,
160
+ nullable: def.nullable ?? false,
161
+ unique: def.unique ?? false,
162
+ default: def.default !== undefined ? def.default : null,
163
+ max: def.max ?? null,
164
+ unsigned: def.unsigned ?? false,
165
+ enumValues: def.enumValues ?? null,
166
+ references,
167
+ precision,
168
+ scale,
169
+ // Preserve FK metadata so MigrationWriter can render fields.ForeignKey(...)
170
+ // instead of a bare fields.integer(...). Stripped from plain objects (migration files).
171
+ _isForeignKey: def._isForeignKey ?? false,
172
+ _isOneToOne: def._isOneToOne ?? false,
173
+ _fkOnDelete: def._fkOnDelete ?? null,
174
+ _fkRelatedName: def._fkRelatedName ?? null,
175
+ };
176
+ }
177
+
178
+ /**
179
+ * Resolve a _fkModel / _fkModelRef pair to a table name string.
180
+ *
181
+ * _fkModel may be:
182
+ * - A Model class (has static .table)
183
+ * - A string model name like 'User' or 'self'
184
+ * - null / undefined
185
+ *
186
+ * _fkModelRef is a lazy () => ModelClass resolver generated by _makeModelRef().
187
+ */
188
+ /**
189
+ * Resolve a _fkModel / _fkModelRef pair to a table name string.
190
+ * Delegates to tableFromClass / modelNameToTable from utils.js.
191
+ */
192
+ function _resolveTargetTable(fkModel, fkModelRef) {
193
+ if (fkModel && typeof fkModel === 'function') {
194
+ const table = tableFromClass(fkModel);
195
+ if (table) return table;
196
+ }
197
+ if (typeof fkModelRef === 'function') {
198
+ try {
199
+ const resolved = fkModelRef();
200
+ if (resolved) {
201
+ const table = tableFromClass(resolved);
202
+ if (table) return table;
203
+ }
204
+ } catch { /* unresolvable at scan time */ }
205
+ }
206
+ if (typeof fkModel === 'string' && fkModel !== 'self') {
207
+ return isSnakeCase(fkModel) ? fkModel : modelNameToTable(fkModel);
208
+ }
209
+ return null;
210
+ }
211
+
212
+
213
+ module.exports = { ProjectState, normaliseField };
@@ -0,0 +1,175 @@
1
+ 'use strict';
2
+
3
+ const readline = require('readline');
4
+
5
+ /**
6
+ * RenameDetector
7
+ *
8
+ * Detects likely field renames during makemigrations and prompts the developer
9
+ * to confirm, exactly as Django does:
10
+ *
11
+ * Was student.age4 renamed to student.age7 (a IntegerField)? [y/N]
12
+ *
13
+ * ── Algorithm ─────────────────────────────────────────────────────────────────
14
+ *
15
+ * For each table with both RemoveField and AddField ops:
16
+ * 1. Find pairs where types match
17
+ * 2. Score similarity (type match required; attribute similarity is a bonus)
18
+ * 3. Prompt for each candidate, highest-score first
19
+ * 4. On confirm → replace the Remove+Add pair with a single RenameField
20
+ * 5. On deny → keep Remove + Add as separate ops
21
+ *
22
+ * Multiple renames in a single run: each candidate is prompted independently.
23
+ * Chained renames across migrations: handled automatically because the history
24
+ * state is replayed from all existing migrations, so the "old name" is always
25
+ * whatever the field is currently called in the DB.
26
+ *
27
+ * ── Matching rules ────────────────────────────────────────────────────────────
28
+ *
29
+ * Required: same table, same type
30
+ * Bonus: same nullable, same default, same max/precision/enumValues
31
+ * No match: different types (an integer cannot rename to a string)
32
+ *
33
+ * ── Non-interactive mode ──────────────────────────────────────────────────────
34
+ *
35
+ * Never prompts. All Remove+Add pairs are kept as-is.
36
+ * This matches Django's --no-input behaviour.
37
+ */
38
+ class RenameDetector {
39
+ constructor(options = {}) {
40
+ this._nonInteractive = options.nonInteractive || !process.stdin.isTTY;
41
+ }
42
+
43
+ /**
44
+ * Given a flat ops list from MigrationWriter.diff(), detect and resolve renames.
45
+ * Returns a new ops list with confirmed renames replaced by RenameField ops.
46
+ *
47
+ * @param {Array<object>} ops
48
+ * @returns {Promise<Array<object>>}
49
+ */
50
+ async detect(ops) {
51
+ if (this._nonInteractive) return ops;
52
+
53
+ // Group RemoveField and AddField by table
54
+ const removes = ops.filter(op => op.type === 'RemoveField');
55
+ const adds = ops.filter(op => op.type === 'AddField');
56
+
57
+ if (removes.length === 0 || adds.length === 0) return ops;
58
+
59
+ // Build rename candidates: (remove, add) pairs on the same table with same type
60
+ const candidates = [];
61
+ for (const rem of removes) {
62
+ for (const add of adds) {
63
+ if (rem.table !== add.table) continue;
64
+ if (rem.field.type !== add.field.type) continue;
65
+
66
+ const score = this._similarity(rem.field, add.field);
67
+ candidates.push({ rem, add, score });
68
+ }
69
+ }
70
+
71
+ if (candidates.length === 0) return ops;
72
+
73
+ // Sort by score descending — highest confidence first
74
+ candidates.sort((a, b) => b.score - a.score);
75
+
76
+ // Track which ops have been consumed by a confirmed rename
77
+ const consumed = new Set(); // op references
78
+
79
+ for (const { rem, add } of candidates) {
80
+ // Skip if either op was already consumed by a previous rename
81
+ if (consumed.has(rem) || consumed.has(add)) continue;
82
+
83
+ const fieldTypeLabel = this._fieldTypeLabel(rem.field);
84
+ const confirmed = await this._ask(rem.table, rem.column, add.column, fieldTypeLabel);
85
+
86
+ if (confirmed) {
87
+ consumed.add(rem);
88
+ consumed.add(add);
89
+ // Mark for replacement — attach the rename info to the RemoveField op
90
+ rem._renameToColumn = add.column;
91
+ }
92
+ }
93
+
94
+ // Rebuild ops list: replace consumed pairs with RenameField, preserve order
95
+ const result = [];
96
+ for (const op of ops) {
97
+ if (consumed.has(op)) {
98
+ if (op.type === 'RemoveField' && op._renameToColumn) {
99
+ // Replace Remove with RenameField
100
+ result.push({
101
+ type: 'RenameField',
102
+ table: op.table,
103
+ oldColumn: op.column,
104
+ newColumn: op._renameToColumn,
105
+ });
106
+ delete op._renameToColumn;
107
+ }
108
+ // Skip the AddField that was consumed — it's been folded into RenameField
109
+ } else {
110
+ result.push(op);
111
+ }
112
+ }
113
+
114
+ return result;
115
+ }
116
+
117
+ // ─── Internals ─────────────────────────────────────────────────────────────
118
+
119
+ /**
120
+ * Score how similar two field definitions are.
121
+ * Type match is a prerequisite (checked before calling this).
122
+ * Returns a score 0–5: higher = more likely a rename.
123
+ */
124
+ _similarity(a, b) {
125
+ let score = 1; // base: types match
126
+ if (a.nullable === b.nullable) score++;
127
+ if (JSON.stringify(a.default) === JSON.stringify(b.default)) score++;
128
+ if (a.max === b.max) score++;
129
+ if (JSON.stringify(a.enumValues) === JSON.stringify(b.enumValues)) score++;
130
+ if (a.unique === b.unique) score++;
131
+ return score;
132
+ }
133
+
134
+ /**
135
+ * Human-readable field type label — matches Django's "a IntegerField" style.
136
+ */
137
+ _fieldTypeLabel(field) {
138
+ const map = {
139
+ id: 'AutoField',
140
+ string: 'CharField',
141
+ text: 'TextField',
142
+ integer: 'IntegerField',
143
+ bigInteger: 'BigIntegerField',
144
+ float: 'FloatField',
145
+ decimal: 'DecimalField',
146
+ boolean: 'BooleanField',
147
+ json: 'JSONField',
148
+ date: 'DateField',
149
+ timestamp: 'DateTimeField',
150
+ enum: 'CharField',
151
+ uuid: 'UUIDField',
152
+ };
153
+ return map[field.type] || `${field.type}Field`;
154
+ }
155
+
156
+ /**
157
+ * Prompt: Was <table>.<oldCol> renamed to <table>.<newCol> (a <Type>)? [y/N]
158
+ * Returns true if confirmed.
159
+ */
160
+ async _ask(table, oldCol, newCol, typeLabel) {
161
+ return new Promise((resolve) => {
162
+ const question = `Was ${table}.${oldCol} renamed to ${table}.${newCol} (a ${typeLabel})? [y/N] `;
163
+ const rl = readline.createInterface({
164
+ input: process.stdin,
165
+ output: process.stdout,
166
+ });
167
+ rl.question(question, (answer) => {
168
+ rl.close();
169
+ resolve(answer.trim().toLowerCase() === 'y');
170
+ });
171
+ });
172
+ }
173
+ }
174
+
175
+ module.exports = RenameDetector;
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const { FieldDefinition } = require('../fields');
3
+ const { applyColumn } = require('./operations/column');
4
+ const { normaliseField } = require('./ProjectState');
4
5
 
5
6
  /**
6
7
  * SchemaBuilder
@@ -27,7 +28,7 @@ class SchemaBuilder {
27
28
  const fields = ModelClass.fields;
28
29
 
29
30
  await this._db.schema.createTable(table, (t) => {
30
- this._applyFields(t, fields, ModelClass);
31
+ this._applyFields(t, fields);
31
32
  });
32
33
  }
33
34
 
@@ -87,87 +88,13 @@ class SchemaBuilder {
87
88
 
88
89
  // ─── Internal ─────────────────────────────────────────────────────────────
89
90
 
90
- _applyFields(tableBuilder, fields, ModelClass) {
91
+ // Delegates to operations/column.js applyColumn — single source of truth
92
+ // for the type → knex column builder mapping.
93
+ _applyFields(tableBuilder, fields) {
91
94
  for (const [name, field] of Object.entries(fields)) {
92
- this._applyField(tableBuilder, name, field, ModelClass);
93
- }
94
- }
95
-
96
- _applyField(t, name, field) {
97
- let col;
98
-
99
- switch (field.type) {
100
- case 'id':
101
- t.increments(name);
102
- return;
103
-
104
- case 'string':
105
- col = t.string(name, field.max || 255);
106
- break;
107
-
108
- case 'text':
109
- col = t.text(name);
110
- break;
111
-
112
- case 'integer':
113
- col = field.unsigned ? t.integer(name).unsigned() : t.integer(name);
114
- break;
115
-
116
- case 'bigInteger':
117
- col = field.unsigned ? t.bigInteger(name).unsigned() : t.bigInteger(name);
118
- break;
119
-
120
- case 'float':
121
- col = t.float(name);
122
- break;
123
-
124
- case 'decimal':
125
- col = t.decimal(name, field.precision || 8, field.scale || 2);
126
- break;
127
-
128
- case 'boolean':
129
- col = t.boolean(name);
130
- break;
131
-
132
- case 'json':
133
- col = t.json(name);
134
- break;
135
-
136
- case 'date':
137
- col = t.date(name);
138
- break;
139
-
140
- case 'timestamp':
141
- col = t.timestamp(name, { useTz: false });
142
- break;
143
-
144
- case 'enum':
145
- col = t.enum(name, field.enumValues || []);
146
- break;
147
-
148
- case 'uuid':
149
- col = t.uuid(name);
150
- break;
151
-
152
- default:
153
- col = t.string(name);
154
- }
155
-
156
- if (!col) return;
157
-
158
- if (field.nullable) col = col.nullable();
159
- else if (field.type !== 'id') col = col.notNullable();
160
-
161
- if (field.unique) col = col.unique();
162
-
163
- if (field.default !== undefined) col = col.defaultTo(field.default);
164
-
165
- if (field.references) {
166
- col = col.references(field.references.column)
167
- .inTable(field.references.table)
168
- .onDelete('CASCADE');
95
+ applyColumn(tableBuilder, name, normaliseField(field));
169
96
  }
170
97
  }
171
98
  }
172
99
 
173
- module.exports = SchemaBuilder;
100
+ module.exports = SchemaBuilder;
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * BaseOperation
5
+ *
6
+ * Abstract base class for all migration operations.
7
+ *
8
+ * Every operation must implement:
9
+ * applyState(projectState) — mutate the in-memory ProjectState (no DB touch)
10
+ * up(db) — apply the change to the live database
11
+ * down(db) — revert the change from the live database
12
+ * toJSON() — return a plain serialisable descriptor
13
+ *
14
+ * The `type` property is set by each subclass and must match the key used
15
+ * in the deserialise() registry in registry.js.
16
+ */
17
+ class BaseOperation {
18
+ /**
19
+ * Mutate the in-memory ProjectState.
20
+ * Called during migration graph replay (makemigrations) — never touches DB.
21
+ * @param {import('../ProjectState').ProjectState} _state
22
+ */
23
+ // eslint-disable-next-line no-unused-vars
24
+ applyState(_state) {
25
+ throw new Error(`${this.constructor.name}.applyState() not implemented`);
26
+ }
27
+
28
+ /**
29
+ * Apply this operation to the live database (forward migration).
30
+ * @param {import('knex').Knex} _db
31
+ */
32
+ // eslint-disable-next-line no-unused-vars
33
+ async up(_db) {
34
+ throw new Error(`${this.constructor.name}.up() not implemented`);
35
+ }
36
+
37
+ /**
38
+ * Revert this operation from the live database (rollback).
39
+ * @param {import('knex').Knex} _db
40
+ */
41
+ // eslint-disable-next-line no-unused-vars
42
+ async down(_db) {
43
+ throw new Error(`${this.constructor.name}.down() not implemented`);
44
+ }
45
+
46
+ /**
47
+ * Return a plain, JSON-serialisable descriptor for this operation.
48
+ * Used by MigrationWriter to write migration files and by MigrationGraph
49
+ * to reload them via deserialise().
50
+ * @returns {object}
51
+ */
52
+ toJSON() {
53
+ throw new Error(`${this.constructor.name}.toJSON() not implemented`);
54
+ }
55
+ }
56
+
57
+ module.exports = { BaseOperation };