millas 0.2.20 → 0.2.21
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/orm/drivers/DatabaseManager.js +12 -0
- package/src/orm/migration/operations/column.js +41 -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 +290 -59
- package/src/orm/query/F.js +98 -0
- package/src/orm/query/LookupParser.js +316 -157
- package/src/orm/query/QueryBuilder.js +177 -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 };
|
|
@@ -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: {
|
|
@@ -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);
|
|
@@ -146,43 +99,36 @@ function _buildColumn(t, name, def, opts = {}) {
|
|
|
146
99
|
case 'timestamp':
|
|
147
100
|
return t.timestamp(name, { useTz: false });
|
|
148
101
|
|
|
149
|
-
case 'enum':
|
|
102
|
+
case 'enum': {
|
|
103
|
+
const client = t.client?.config?.client || '';
|
|
104
|
+
if (client.includes('pg') || client.includes('postgres')) {
|
|
105
|
+
// Store as text — CHECK constraint added separately in applyColumn
|
|
106
|
+
return t.text(name);
|
|
107
|
+
}
|
|
150
108
|
return t.enu(name, def.enumValues || []);
|
|
109
|
+
}
|
|
151
110
|
|
|
152
111
|
case 'uuid':
|
|
153
112
|
return t.uuid(name);
|
|
154
113
|
|
|
155
114
|
default:
|
|
156
|
-
return t.string(name);
|
|
115
|
+
return t.string(name);
|
|
157
116
|
}
|
|
158
117
|
}
|
|
159
118
|
|
|
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
119
|
function _applyModifiers(col, def, opts = {}) {
|
|
170
|
-
// Nullability
|
|
171
120
|
if (def.nullable) {
|
|
172
121
|
col.nullable();
|
|
173
122
|
} else if (def.type !== 'id') {
|
|
174
123
|
col.notNullable();
|
|
175
124
|
}
|
|
176
125
|
|
|
177
|
-
// Uniqueness
|
|
178
126
|
if (def.unique) col.unique();
|
|
179
127
|
|
|
180
|
-
// Default value
|
|
181
128
|
if (def.default !== null && def.default !== undefined) {
|
|
182
129
|
col.defaultTo(def.default);
|
|
183
130
|
}
|
|
184
131
|
|
|
185
|
-
// Inline FK constraint — skipped when deferring to attachFKConstraints()
|
|
186
132
|
if (!opts.skipFK && def.references) {
|
|
187
133
|
const ref = def.references;
|
|
188
134
|
col
|
|
@@ -192,4 +138,4 @@ function _applyModifiers(col, def, opts = {}) {
|
|
|
192
138
|
}
|
|
193
139
|
}
|
|
194
140
|
|
|
195
|
-
module.exports = { applyColumn, alterColumn, attachFKConstraints };
|
|
141
|
+
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
|
}
|