millas 0.2.20 → 0.2.23
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 +1 -1
- package/src/admin/QueryEngine.js +17 -13
- package/src/cli.js +3 -0
- package/src/core/db.js +9 -8
- package/src/events/EventEmitter.js +12 -1
- package/src/facades/Database.js +55 -34
- package/src/orm/drivers/DatabaseManager.js +12 -0
- package/src/orm/fields/index.js +18 -0
- package/src/orm/migration/MigrationWriter.js +6 -0
- package/src/orm/migration/ModelInspector.js +4 -0
- package/src/orm/migration/operations/column.js +59 -95
- package/src/orm/migration/operations/fields.js +6 -6
- package/src/orm/migration/operations/models.js +3 -3
- package/src/orm/model/Model.js +293 -61
- package/src/orm/query/F.js +98 -0
- package/src/orm/query/LookupParser.js +316 -157
- package/src/orm/query/QueryBuilder.js +230 -7
- package/src/providers/DatabaseServiceProvider.js +2 -2
package/package.json
CHANGED
package/src/admin/QueryEngine.js
CHANGED
|
@@ -145,7 +145,7 @@ class QueryEngine {
|
|
|
145
145
|
// ── Execute data + count in parallel ──────────────────────────────────
|
|
146
146
|
const [rows, countResult] = await Promise.all([
|
|
147
147
|
q.clone().limit(limit).offset(offset),
|
|
148
|
-
q.clone().count('* as count').first(),
|
|
148
|
+
q.clone().clearOrder().clearSelect().count('* as count').first(),
|
|
149
149
|
]);
|
|
150
150
|
|
|
151
151
|
const total = Number(countResult?.count ?? 0);
|
|
@@ -231,12 +231,12 @@ class QueryEngine {
|
|
|
231
231
|
case 'in': return q.whereIn(col, Array.isArray(value) ? value : [value]);
|
|
232
232
|
case 'notin': return q.whereNotIn(col, Array.isArray(value) ? value : [value]);
|
|
233
233
|
case 'between': return q.whereBetween(col, Array.isArray(value) ? value : [value, value]);
|
|
234
|
-
case 'contains':
|
|
235
|
-
case 'icontains': return q.where(col, 'like', `%${value}%`);
|
|
236
|
-
case 'startswith':
|
|
237
|
-
case 'istartswith': return q.where(col, 'like', `${value}%`);
|
|
238
|
-
case 'endswith':
|
|
239
|
-
case 'iendswith': return q.where(col, 'like', `%${value}`);
|
|
234
|
+
case 'contains': return q.where(col, 'like', `%${value}%`);
|
|
235
|
+
case 'icontains': return q.where(q.client?.config?.client?.includes('pg') ? col : col, q.client?.config?.client?.includes('pg') ? 'ilike' : 'like', `%${value}%`);
|
|
236
|
+
case 'startswith': return q.where(col, 'like', `${value}%`);
|
|
237
|
+
case 'istartswith': return q.where(col, q.client?.config?.client?.includes('pg') ? 'ilike' : 'like', `${value}%`);
|
|
238
|
+
case 'endswith': return q.where(col, 'like', `%${value}`);
|
|
239
|
+
case 'iendswith': return q.where(col, q.client?.config?.client?.includes('pg') ? 'ilike' : 'like', `%${value}`);
|
|
240
240
|
default: return q.where(key, value);
|
|
241
241
|
}
|
|
242
242
|
}
|
|
@@ -246,15 +246,19 @@ class QueryEngine {
|
|
|
246
246
|
* Uses strftime for SQLite/MySQL; falls back gracefully on PostgreSQL.
|
|
247
247
|
*/
|
|
248
248
|
_applyDateHierarchy(q, col, year, month) {
|
|
249
|
+
const client = q.client?.config?.client || 'sqlite3';
|
|
250
|
+
const isPg = client.includes('pg') || client.includes('postgres');
|
|
251
|
+
const isMy = client.includes('mysql') || client.includes('maria');
|
|
252
|
+
|
|
249
253
|
if (year) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
254
|
+
if (isPg) q = q.whereRaw(`EXTRACT(YEAR FROM "${col}") = ?`, [Number(year)]);
|
|
255
|
+
else if (isMy) q = q.whereRaw(`YEAR(\`${col}\`) = ?`, [Number(year)]);
|
|
256
|
+
else q = q.whereRaw(`strftime('%Y', \`${col}\`) = ?`, [String(year)]);
|
|
253
257
|
}
|
|
254
258
|
if (month) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
259
|
+
if (isPg) q = q.whereRaw(`EXTRACT(MONTH FROM "${col}") = ?`, [Number(month)]);
|
|
260
|
+
else if (isMy) q = q.whereRaw(`MONTH(\`${col}\`) = ?`, [Number(month)]);
|
|
261
|
+
else q = q.whereRaw(`strftime('%m', \`${col}\`) = ?`, [String(month).padStart(2, '0')]);
|
|
258
262
|
}
|
|
259
263
|
return q;
|
|
260
264
|
}
|
package/src/cli.js
CHANGED
package/src/core/db.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
1
|
+
const { Model, fields } = require('../orm');
|
|
2
|
+
const { migrations } = require('../orm/migration/operations');
|
|
3
|
+
const F = require('../orm/query/F');
|
|
4
|
+
const Q = require('../orm/query/Q');
|
|
5
|
+
const HasMany = require('../orm/relations/HasMany');
|
|
6
|
+
const BelongsTo = require('../orm/relations/BelongsTo');
|
|
7
|
+
const HasOne = require('../orm/relations/HasOne');
|
|
8
|
+
const BelongsToMany = require('../orm/relations/BelongsToMany');
|
|
6
9
|
|
|
7
|
-
module.exports = {
|
|
8
|
-
Model, fields,migrations
|
|
9
|
-
}
|
|
10
|
+
module.exports = { Model, fields, migrations, F, Q, HasMany, BelongsTo, HasOne, BelongsToMany };
|
|
@@ -81,7 +81,18 @@ class EventEmitter {
|
|
|
81
81
|
async _invoke(handler, event) {
|
|
82
82
|
// Listener class (has handle() on prototype)
|
|
83
83
|
if (typeof handler === 'function' && typeof handler.prototype?.handle === 'function') {
|
|
84
|
-
|
|
84
|
+
// Resolve static inject dependencies from the container
|
|
85
|
+
let inst;
|
|
86
|
+
if (handler.inject && Array.isArray(handler.inject) && handler.inject.length) {
|
|
87
|
+
const Facade = require('../facades/Facade');
|
|
88
|
+
const container = Facade._container;
|
|
89
|
+
const deps = handler.inject.map(key => {
|
|
90
|
+
try { return container ? container.make(key) : undefined; } catch { return undefined; }
|
|
91
|
+
});
|
|
92
|
+
inst = new handler(...deps);
|
|
93
|
+
} else {
|
|
94
|
+
inst = new handler();
|
|
95
|
+
}
|
|
85
96
|
if (handler.queue && this._queue) {
|
|
86
97
|
const Job = require('../queue/Job');
|
|
87
98
|
const q = this._queue;
|
package/src/facades/Database.js
CHANGED
|
@@ -1,43 +1,64 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const Facade = require('./Facade');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
|
-
*
|
|
6
|
+
* Database facade — direct access to the knex connection.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const Database = require('millas/facades/Database');
|
|
5
10
|
*
|
|
6
|
-
*
|
|
11
|
+
* // Raw SQL
|
|
12
|
+
* const result = await Database.raw('SELECT NOW()');
|
|
7
13
|
*
|
|
8
|
-
*
|
|
14
|
+
* // Knex query builder
|
|
15
|
+
* const rows = await Database.table('posts').where('published', true).select('*');
|
|
9
16
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* static fields = {
|
|
13
|
-
* id: fields.id(),
|
|
14
|
-
* title: fields.string({ max: 255 }),
|
|
15
|
-
* author: fields.ForeignKey('User', { relatedName: 'posts' }),
|
|
16
|
-
* published: fields.boolean({ default: false }),
|
|
17
|
-
* created_at: fields.timestamp(),
|
|
18
|
-
* updated_at: fields.timestamp(),
|
|
19
|
-
* };
|
|
20
|
-
* }
|
|
17
|
+
* // Named connection
|
|
18
|
+
* const rows = await Database.connection('replica').raw('SELECT 1');
|
|
21
19
|
*/
|
|
20
|
+
class Database {
|
|
21
|
+
static _resolveInstance() {
|
|
22
|
+
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
23
|
+
return DatabaseManager.connection();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Proxy every static call to the knex connection
|
|
28
|
+
module.exports = new Proxy(Database, {
|
|
29
|
+
get(target, prop) {
|
|
30
|
+
// Let real static members through
|
|
31
|
+
if (prop in target || prop === 'then' || prop === 'catch') {
|
|
32
|
+
return target[prop];
|
|
33
|
+
}
|
|
34
|
+
if (typeof prop === 'symbol') return target[prop];
|
|
35
|
+
|
|
36
|
+
// Special case: raw() — normalize result across dialects
|
|
37
|
+
if (prop === 'raw') {
|
|
38
|
+
return async (sql, bindings) => {
|
|
39
|
+
const db = Database._resolveInstance();
|
|
40
|
+
const result = await db.raw(sql, bindings);
|
|
41
|
+
// Postgres returns { rows: [...], command, rowCount, ... }
|
|
42
|
+
// SQLite/MySQL return [rows, fields] or just rows
|
|
43
|
+
if (result && result.rows) return result.rows;
|
|
44
|
+
if (Array.isArray(result)) return result[0] ?? result;
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
22
48
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
ModelInspector,
|
|
31
|
-
DatabaseServiceProvider,
|
|
32
|
-
} = require('../core');
|
|
49
|
+
// Special case: connection(name) returns a named knex instance
|
|
50
|
+
if (prop === 'connection') {
|
|
51
|
+
return (name) => {
|
|
52
|
+
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
53
|
+
return DatabaseManager.connection(name || null);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
33
56
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
DatabaseServiceProvider,
|
|
43
|
-
};
|
|
57
|
+
// Proxy everything else to the default knex connection
|
|
58
|
+
return (...args) => {
|
|
59
|
+
const db = Database._resolveInstance();
|
|
60
|
+
if (typeof db[prop] !== 'function') return db[prop];
|
|
61
|
+
return db[prop](...args);
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -99,6 +99,12 @@ class DatabaseManager {
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
case 'mysql':
|
|
102
|
+
try { require('mysql2'); } catch {
|
|
103
|
+
throw new Error(
|
|
104
|
+
'MySQL driver not installed.\n' +
|
|
105
|
+
'Run: npm install mysql2'
|
|
106
|
+
);
|
|
107
|
+
}
|
|
102
108
|
return knex({
|
|
103
109
|
client: 'mysql2',
|
|
104
110
|
connection: {
|
|
@@ -112,6 +118,12 @@ class DatabaseManager {
|
|
|
112
118
|
});
|
|
113
119
|
|
|
114
120
|
case 'postgres':
|
|
121
|
+
try { require('pg'); } catch {
|
|
122
|
+
throw new Error(
|
|
123
|
+
'PostgreSQL driver not installed.\n' +
|
|
124
|
+
'Run: npm install pg'
|
|
125
|
+
);
|
|
126
|
+
}
|
|
115
127
|
return knex({
|
|
116
128
|
client: 'pg',
|
|
117
129
|
connection: {
|
package/src/orm/fields/index.js
CHANGED
|
@@ -101,6 +101,24 @@ const fields = {
|
|
|
101
101
|
return new FieldDefinition('json', options);
|
|
102
102
|
},
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Array field — stores an ordered list of values.
|
|
106
|
+
*
|
|
107
|
+
* On PostgreSQL: uses native ARRAY type (text[], integer[], etc.)
|
|
108
|
+
* On SQLite/MySQL: falls back to JSON column (same as fields.json())
|
|
109
|
+
*
|
|
110
|
+
* @param {string} [of='text'] — item type: 'text' | 'integer' | 'float' | 'boolean'
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* tags: fields.array() // text[] on PG, json on SQLite
|
|
114
|
+
* scores: fields.array('integer') // integer[] on PG
|
|
115
|
+
* media_ids: fields.array('integer', { nullable: true, default: [] })
|
|
116
|
+
*/
|
|
117
|
+
array(of = 'text', options = {}) {
|
|
118
|
+
if (typeof of === 'object') { options = of; of = 'text'; }
|
|
119
|
+
return new FieldDefinition('array', { arrayOf: of, nullable: true, default: [], ...options });
|
|
120
|
+
},
|
|
121
|
+
|
|
104
122
|
date(options = {}) {
|
|
105
123
|
return new FieldDefinition('date', options);
|
|
106
124
|
},
|
|
@@ -408,6 +408,12 @@ ${opsCode},
|
|
|
408
408
|
case 'id':
|
|
409
409
|
return 'fields.id()';
|
|
410
410
|
|
|
411
|
+
case 'array': {
|
|
412
|
+
const of_ = def.arrayOf || 'text';
|
|
413
|
+
const optsStr = Object.keys(opts).length ? `, ${this._renderOpts(opts)}` : '';
|
|
414
|
+
return `fields.array('${of_}'${optsStr})`;
|
|
415
|
+
}
|
|
416
|
+
|
|
411
417
|
case 'enum': {
|
|
412
418
|
const vals = JSON.stringify(def.enumValues || []);
|
|
413
419
|
const optsStr = Object.keys(opts).length
|
|
@@ -523,6 +523,10 @@ ${this._renderColumn(' ', diff.column, diff.previous, '.alter()')}
|
|
|
523
523
|
line = `t.json('${name}')`;
|
|
524
524
|
break;
|
|
525
525
|
|
|
526
|
+
case 'array':
|
|
527
|
+
line = `t.specificType('${name}', '${field.arrayOf || 'text'}[]')`;
|
|
528
|
+
break;
|
|
529
|
+
|
|
526
530
|
case 'date':
|
|
527
531
|
line = `t.date('${name}')`;
|
|
528
532
|
break;
|
|
@@ -1,81 +1,49 @@
|
|
|
1
1
|
'use strict';
|
|
2
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
3
|
// ─── Core column builder ──────────────────────────────────────────────────────
|
|
21
4
|
|
|
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) {
|
|
5
|
+
function applyColumn(t, name, def, tableName) {
|
|
36
6
|
const col = _buildColumn(t, name, def);
|
|
37
|
-
if (!col) return;
|
|
7
|
+
if (!col) return;
|
|
38
8
|
|
|
39
9
|
_applyModifiers(col, def);
|
|
10
|
+
|
|
11
|
+
// Postgres enum: stored as text + separate CHECK constraint
|
|
12
|
+
if (def.type === 'enum' && def.enumValues?.length) {
|
|
13
|
+
const client = t.client?.config?.client || '';
|
|
14
|
+
if (client.includes('pg') || client.includes('postgres')) {
|
|
15
|
+
const values = def.enumValues.map(v => `'${v}'`).join(', ');
|
|
16
|
+
const constraintName = `${tableName || 'tbl'}_${name}_check`;
|
|
17
|
+
t.check(`"${name}" in (${values})`, [], constraintName);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
40
20
|
}
|
|
41
21
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
22
|
+
function alterColumn(t, name, def, tableName) {
|
|
23
|
+
const client = t.client?.config?.client || '';
|
|
24
|
+
const isPg = client.includes('pg') || client.includes('postgres');
|
|
25
|
+
|
|
26
|
+
if (isPg && def.type === 'enum' && def.enumValues?.length) {
|
|
27
|
+
// Postgres: ALTER COLUMN TYPE with inline CHECK is invalid.
|
|
28
|
+
// Drop old CHECK constraint, add new one.
|
|
29
|
+
const constraintName = `${tableName || 'tbl'}_${name}_check`;
|
|
30
|
+
const values = def.enumValues.map(v => `'${v}'`).join(', ');
|
|
31
|
+
try { t.dropChecks(constraintName); } catch {}
|
|
32
|
+
t.check(`"${name}" in (${values})`, [], constraintName);
|
|
33
|
+
if (def.nullable) t.setNullable(name);
|
|
34
|
+
else t.dropNullable(name);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
56
38
|
const col = _buildColumn(t, name, def, { forAlter: true });
|
|
57
39
|
if (!col) return;
|
|
58
40
|
|
|
59
|
-
_applyModifiers(col, def, { skipFK: true });
|
|
41
|
+
_applyModifiers(col, def, { skipFK: true });
|
|
60
42
|
col.alter();
|
|
61
43
|
}
|
|
62
44
|
|
|
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
45
|
async function attachFKConstraints(db, table, fields) {
|
|
77
46
|
const fkEntries = Object.entries(fields).filter(([, def]) => def.references);
|
|
78
|
-
|
|
79
47
|
if (fkEntries.length === 0) return;
|
|
80
48
|
|
|
81
49
|
await db.schema.alterTable(table, (t) => {
|
|
@@ -91,22 +59,11 @@ async function attachFKConstraints(db, table, fields) {
|
|
|
91
59
|
|
|
92
60
|
// ─── Internal helpers ─────────────────────────────────────────────────────────
|
|
93
61
|
|
|
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
62
|
function _buildColumn(t, name, def, opts = {}) {
|
|
106
63
|
switch (def.type) {
|
|
107
64
|
case 'id':
|
|
108
65
|
if (!opts.forAlter) t.increments(name).primary();
|
|
109
|
-
return null;
|
|
66
|
+
return null;
|
|
110
67
|
|
|
111
68
|
case 'string':
|
|
112
69
|
case 'email':
|
|
@@ -119,14 +76,10 @@ function _buildColumn(t, name, def, opts = {}) {
|
|
|
119
76
|
return t.text(name);
|
|
120
77
|
|
|
121
78
|
case 'integer':
|
|
122
|
-
return def.unsigned
|
|
123
|
-
? t.integer(name).unsigned()
|
|
124
|
-
: t.integer(name);
|
|
79
|
+
return def.unsigned ? t.integer(name).unsigned() : t.integer(name);
|
|
125
80
|
|
|
126
81
|
case 'bigInteger':
|
|
127
|
-
return def.unsigned
|
|
128
|
-
? t.bigInteger(name).unsigned()
|
|
129
|
-
: t.bigInteger(name);
|
|
82
|
+
return def.unsigned ? t.bigInteger(name).unsigned() : t.bigInteger(name);
|
|
130
83
|
|
|
131
84
|
case 'float':
|
|
132
85
|
return t.float(name);
|
|
@@ -140,49 +93,60 @@ function _buildColumn(t, name, def, opts = {}) {
|
|
|
140
93
|
case 'json':
|
|
141
94
|
return t.json(name);
|
|
142
95
|
|
|
96
|
+
case 'array': {
|
|
97
|
+
const client = t.client?.config?.client || '';
|
|
98
|
+
if (client.includes('pg') || client.includes('postgres')) {
|
|
99
|
+
// Native Postgres ARRAY type
|
|
100
|
+
const pgTypeMap = {
|
|
101
|
+
text: 'text', string: 'text',
|
|
102
|
+
integer: 'integer', int: 'integer',
|
|
103
|
+
float: 'float', decimal: 'decimal',
|
|
104
|
+
boolean: 'boolean',
|
|
105
|
+
uuid: 'uuid',
|
|
106
|
+
};
|
|
107
|
+
const pgType = pgTypeMap[def.arrayOf || 'text'] || 'text';
|
|
108
|
+
return t.specificType(name, `${pgType}[]`);
|
|
109
|
+
}
|
|
110
|
+
// SQLite / MySQL — fall back to JSON
|
|
111
|
+
return t.json(name);
|
|
112
|
+
}
|
|
113
|
+
|
|
143
114
|
case 'date':
|
|
144
115
|
return t.date(name);
|
|
145
116
|
|
|
146
117
|
case 'timestamp':
|
|
147
118
|
return t.timestamp(name, { useTz: false });
|
|
148
119
|
|
|
149
|
-
case 'enum':
|
|
120
|
+
case 'enum': {
|
|
121
|
+
const client = t.client?.config?.client || '';
|
|
122
|
+
if (client.includes('pg') || client.includes('postgres')) {
|
|
123
|
+
// Store as text — CHECK constraint added separately in applyColumn
|
|
124
|
+
return t.text(name);
|
|
125
|
+
}
|
|
150
126
|
return t.enu(name, def.enumValues || []);
|
|
127
|
+
}
|
|
151
128
|
|
|
152
129
|
case 'uuid':
|
|
153
130
|
return t.uuid(name);
|
|
154
131
|
|
|
155
132
|
default:
|
|
156
|
-
return t.string(name);
|
|
133
|
+
return t.string(name);
|
|
157
134
|
}
|
|
158
135
|
}
|
|
159
136
|
|
|
160
|
-
/**
|
|
161
|
-
* Apply nullability, uniqueness, default, and FK constraint modifiers
|
|
162
|
-
* to an already-built knex column builder.
|
|
163
|
-
*
|
|
164
|
-
* @param {object} col — knex column builder
|
|
165
|
-
* @param {object} def — normalised field def
|
|
166
|
-
* @param {object} [opts]
|
|
167
|
-
* @param {boolean} [opts.skipFK] — skip FK constraint (used by alterColumn)
|
|
168
|
-
*/
|
|
169
137
|
function _applyModifiers(col, def, opts = {}) {
|
|
170
|
-
// Nullability
|
|
171
138
|
if (def.nullable) {
|
|
172
139
|
col.nullable();
|
|
173
140
|
} else if (def.type !== 'id') {
|
|
174
141
|
col.notNullable();
|
|
175
142
|
}
|
|
176
143
|
|
|
177
|
-
// Uniqueness
|
|
178
144
|
if (def.unique) col.unique();
|
|
179
145
|
|
|
180
|
-
// Default value
|
|
181
146
|
if (def.default !== null && def.default !== undefined) {
|
|
182
147
|
col.defaultTo(def.default);
|
|
183
148
|
}
|
|
184
149
|
|
|
185
|
-
// Inline FK constraint — skipped when deferring to attachFKConstraints()
|
|
186
150
|
if (!opts.skipFK && def.references) {
|
|
187
151
|
const ref = def.references;
|
|
188
152
|
col
|
|
@@ -192,4 +156,4 @@ function _applyModifiers(col, def, opts = {}) {
|
|
|
192
156
|
}
|
|
193
157
|
}
|
|
194
158
|
|
|
195
|
-
module.exports = { applyColumn, alterColumn, attachFKConstraints };
|
|
159
|
+
module.exports = { applyColumn, alterColumn, attachFKConstraints };
|
|
@@ -53,7 +53,7 @@ class AddField extends BaseOperation {
|
|
|
53
53
|
await this._safeBackfill(db, def);
|
|
54
54
|
} else {
|
|
55
55
|
await db.schema.table(this.table, (t) => {
|
|
56
|
-
applyColumn(t, this.column, def);
|
|
56
|
+
applyColumn(t, this.column, def, this.table);
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -94,7 +94,7 @@ class AddField extends BaseOperation {
|
|
|
94
94
|
|
|
95
95
|
// Step 1: add as nullable
|
|
96
96
|
await db.schema.table(this.table, (t) => {
|
|
97
|
-
applyColumn(t, this.column, { ...def, nullable: true, default: null });
|
|
97
|
+
applyColumn(t, this.column, { ...def, nullable: true, default: null }, this.table);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
// Step 2: backfill
|
|
@@ -115,7 +115,7 @@ class AddField extends BaseOperation {
|
|
|
115
115
|
|
|
116
116
|
// Step 3: tighten to NOT NULL
|
|
117
117
|
await db.schema.alterTable(this.table, (t) => {
|
|
118
|
-
alterColumn(t, this.column, { ...def, nullable: false });
|
|
118
|
+
alterColumn(t, this.column, { ...def, nullable: false }, this.table);
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
}
|
|
@@ -148,7 +148,7 @@ class RemoveField extends BaseOperation {
|
|
|
148
148
|
|
|
149
149
|
async down(db) {
|
|
150
150
|
await db.schema.table(this.table, (t) => {
|
|
151
|
-
applyColumn(t, this.column, normaliseField(this.field));
|
|
151
|
+
applyColumn(t, this.column, normaliseField(this.field), this.table);
|
|
152
152
|
});
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -186,13 +186,13 @@ class AlterField extends BaseOperation {
|
|
|
186
186
|
|
|
187
187
|
async up(db) {
|
|
188
188
|
await db.schema.alterTable(this.table, (t) => {
|
|
189
|
-
alterColumn(t, this.column, normaliseField(this.field));
|
|
189
|
+
alterColumn(t, this.column, normaliseField(this.field), this.table);
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
async down(db) {
|
|
194
194
|
await db.schema.alterTable(this.table, (t) => {
|
|
195
|
-
alterColumn(t, this.column, normaliseField(this.previousField));
|
|
195
|
+
alterColumn(t, this.column, normaliseField(this.previousField), this.table);
|
|
196
196
|
});
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -48,7 +48,7 @@ class CreateModel extends BaseOperation {
|
|
|
48
48
|
async up(db) {
|
|
49
49
|
await db.schema.createTable(this.table, (t) => {
|
|
50
50
|
for (const [name, def] of Object.entries(this.fields)) {
|
|
51
|
-
applyColumn(t, name, normaliseField(def));
|
|
51
|
+
applyColumn(t, name, normaliseField(def), this.table);
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
await this._applyIndexes(db);
|
|
@@ -61,7 +61,7 @@ class CreateModel extends BaseOperation {
|
|
|
61
61
|
async upWithoutFKs(db) {
|
|
62
62
|
await db.schema.createTable(this.table, (t) => {
|
|
63
63
|
for (const [name, def] of Object.entries(this.fields)) {
|
|
64
|
-
applyColumn(t, name, { ...normaliseField(def), references: null });
|
|
64
|
+
applyColumn(t, name, { ...normaliseField(def), references: null }, this.table);
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
await this._applyIndexes(db);
|
|
@@ -134,7 +134,7 @@ class DeleteModel extends BaseOperation {
|
|
|
134
134
|
async down(db) {
|
|
135
135
|
await db.schema.createTable(this.table, (t) => {
|
|
136
136
|
for (const [name, def] of Object.entries(this.fields)) {
|
|
137
|
-
applyColumn(t, name, normaliseField(def));
|
|
137
|
+
applyColumn(t, name, normaliseField(def), this.table);
|
|
138
138
|
}
|
|
139
139
|
});
|
|
140
140
|
}
|