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.
- package/package.json +3 -2
- package/src/admin/ActivityLog.js +153 -52
- package/src/admin/Admin.js +516 -199
- package/src/admin/AdminAuth.js +213 -98
- package/src/admin/FormGenerator.js +372 -0
- package/src/admin/HookRegistry.js +256 -0
- package/src/admin/QueryEngine.js +263 -0
- package/src/admin/ViewContext.js +318 -0
- package/src/admin/WidgetRegistry.js +406 -0
- package/src/admin/index.js +17 -0
- package/src/admin/resources/AdminResource.js +393 -97
- package/src/admin/static/admin.css +1422 -0
- package/src/admin/static/date-picker.css +157 -0
- package/src/admin/static/date-picker.js +316 -0
- package/src/admin/static/json-editor.css +649 -0
- package/src/admin/static/json-editor.js +1429 -0
- package/src/admin/static/ui.js +1044 -0
- package/src/admin/views/layouts/base.njk +87 -1046
- package/src/admin/views/pages/detail.njk +56 -21
- package/src/admin/views/pages/error.njk +65 -0
- package/src/admin/views/pages/form.njk +47 -599
- package/src/admin/views/pages/list.njk +270 -62
- package/src/admin/views/partials/form-field.njk +53 -0
- package/src/admin/views/partials/form-footer.njk +28 -0
- package/src/admin/views/partials/form-readonly.njk +114 -0
- package/src/admin/views/partials/form-scripts.njk +480 -0
- package/src/admin/views/partials/form-widget.njk +297 -0
- package/src/admin/views/partials/icons.njk +64 -0
- package/src/admin/views/partials/json-dialog.njk +80 -0
- package/src/admin/views/partials/json-editor.njk +37 -0
- package/src/ai/AIManager.js +954 -0
- package/src/ai/AITokenBudget.js +250 -0
- package/src/ai/PromptGuard.js +216 -0
- package/src/ai/agents.js +218 -0
- package/src/ai/conversation.js +213 -0
- package/src/ai/drivers.js +734 -0
- package/src/ai/files.js +249 -0
- package/src/ai/media.js +303 -0
- package/src/ai/pricing.js +152 -0
- package/src/ai/provider_tools.js +114 -0
- package/src/ai/types.js +356 -0
- package/src/auth/Auth.js +18 -2
- package/src/auth/AuthUser.js +65 -44
- package/src/cli.js +3 -1
- package/src/commands/createsuperuser.js +267 -0
- package/src/commands/lang.js +589 -0
- package/src/commands/migrate.js +154 -81
- package/src/commands/serve.js +3 -4
- package/src/container/AppInitializer.js +101 -20
- package/src/container/Application.js +31 -1
- package/src/container/MillasApp.js +10 -3
- package/src/container/MillasConfig.js +35 -6
- package/src/core/admin.js +5 -0
- package/src/core/db.js +2 -1
- package/src/core/foundation.js +2 -10
- package/src/core/lang.js +1 -0
- package/src/errors/HttpError.js +32 -16
- package/src/facades/AI.js +411 -0
- package/src/facades/Hash.js +67 -0
- package/src/facades/Process.js +144 -0
- package/src/hashing/Hash.js +262 -0
- package/src/http/HtmlEscape.js +162 -0
- package/src/http/MillasRequest.js +63 -7
- package/src/http/MillasResponse.js +70 -4
- package/src/http/ResponseDispatcher.js +21 -27
- package/src/http/SafeFilePath.js +195 -0
- package/src/http/SafeRedirect.js +62 -0
- package/src/http/SecurityBootstrap.js +70 -0
- package/src/http/helpers.js +40 -125
- package/src/http/index.js +10 -1
- package/src/http/middleware/CsrfMiddleware.js +258 -0
- package/src/http/middleware/RateLimiter.js +314 -0
- package/src/http/middleware/SecurityHeaders.js +281 -0
- package/src/i18n/I18nServiceProvider.js +91 -0
- package/src/i18n/Translator.js +643 -0
- package/src/i18n/defaults.js +122 -0
- package/src/i18n/index.js +164 -0
- package/src/i18n/locales/en.js +55 -0
- package/src/i18n/locales/sw.js +48 -0
- package/src/logger/LogRedactor.js +247 -0
- package/src/logger/Logger.js +1 -1
- package/src/logger/formatters/JsonFormatter.js +11 -4
- package/src/logger/formatters/PrettyFormatter.js +103 -65
- package/src/logger/formatters/SimpleFormatter.js +14 -3
- package/src/middleware/ThrottleMiddleware.js +27 -4
- package/src/migrations/system/0001_users.js +21 -0
- package/src/migrations/system/0002_admin_log.js +25 -0
- package/src/migrations/system/0003_sessions.js +23 -0
- package/src/orm/fields/index.js +210 -188
- package/src/orm/migration/DefaultValueParser.js +325 -0
- package/src/orm/migration/InteractiveResolver.js +191 -0
- package/src/orm/migration/Makemigrations.js +312 -0
- package/src/orm/migration/MigrationGraph.js +227 -0
- package/src/orm/migration/MigrationRunner.js +202 -108
- package/src/orm/migration/MigrationWriter.js +463 -0
- package/src/orm/migration/ModelInspector.js +143 -74
- package/src/orm/migration/ModelScanner.js +225 -0
- package/src/orm/migration/ProjectState.js +213 -0
- package/src/orm/migration/RenameDetector.js +175 -0
- package/src/orm/migration/SchemaBuilder.js +8 -81
- package/src/orm/migration/operations/base.js +57 -0
- package/src/orm/migration/operations/column.js +191 -0
- package/src/orm/migration/operations/fields.js +252 -0
- package/src/orm/migration/operations/index.js +55 -0
- package/src/orm/migration/operations/models.js +152 -0
- package/src/orm/migration/operations/registry.js +131 -0
- package/src/orm/migration/operations/special.js +51 -0
- package/src/orm/migration/utils.js +208 -0
- package/src/orm/model/Model.js +81 -13
- package/src/process/Process.js +333 -0
- package/src/providers/AdminServiceProvider.js +66 -9
- package/src/providers/AuthServiceProvider.js +40 -5
- package/src/providers/CacheStorageServiceProvider.js +2 -2
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/LogServiceProvider.js +4 -1
- package/src/providers/MailServiceProvider.js +1 -1
- package/src/providers/QueueServiceProvider.js +1 -1
- package/src/router/MiddlewareRegistry.js +27 -2
- package/src/scaffold/templates.js +80 -21
- 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 };
|