millas 0.1.7 → 0.1.9
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 +32 -19
- package/src/commands/migrate.js +138 -77
- package/src/index.js +10 -2
- package/src/orm/index.js +19 -0
- package/src/orm/migration/MigrationRunner.js +32 -42
- package/src/orm/migration/ModelInspector.js +242 -70
- package/src/orm/migration/dialects/mysql.js +26 -0
- package/src/orm/migration/dialects/postgres.js +18 -0
- package/src/orm/migration/dialects/sqlite.js +24 -0
- package/src/orm/model/Model.js +398 -133
- package/src/orm/query/Aggregates.js +56 -0
- package/src/orm/query/LookupParser.js +308 -0
- package/src/orm/query/Q.js +123 -0
- package/src/orm/query/QueryBuilder.js +266 -82
- package/src/orm/relations/BelongsTo.js +68 -0
- package/src/orm/relations/BelongsToMany.js +188 -0
- package/src/orm/relations/HasMany.js +72 -0
- package/src/orm/relations/HasOne.js +67 -0
- package/src/orm/relations/index.js +8 -0
- package/src/scaffold/maker.js +39 -4
- package/src/scaffold/templates.js +26 -3
|
@@ -6,21 +6,23 @@ const path = require('path');
|
|
|
6
6
|
/**
|
|
7
7
|
* ModelInspector
|
|
8
8
|
*
|
|
9
|
-
* Implements `millas makemigrations` — Django-style migration generation.
|
|
9
|
+
* Implements `millas makemigrations` — Django-style auto-migration generation.
|
|
10
10
|
*
|
|
11
11
|
* Workflow:
|
|
12
|
-
* 1. Scan app/models/ for Model subclasses
|
|
12
|
+
* 1. Scan app/models/ for Model subclasses that have static `fields`
|
|
13
13
|
* 2. Load the last known schema snapshot (.millas/schema.json)
|
|
14
14
|
* 3. Diff current fields vs snapshot
|
|
15
|
-
* 4. Generate timestamped migration file(s) for each change
|
|
16
|
-
* 5. Update the snapshot
|
|
15
|
+
* 4. Generate timestamped migration file(s) for each detected change
|
|
16
|
+
* 5. Update the snapshot so the next run starts clean
|
|
17
17
|
*
|
|
18
18
|
* Detects:
|
|
19
|
-
* - New tables
|
|
20
|
-
* - Dropped tables (model removed)
|
|
19
|
+
* - New tables (model file added)
|
|
20
|
+
* - Dropped tables (model file removed)
|
|
21
21
|
* - Added columns
|
|
22
22
|
* - Removed columns
|
|
23
|
-
* - Changed column
|
|
23
|
+
* - Changed column attributes (type, nullable, unique, default, …)
|
|
24
|
+
*
|
|
25
|
+
* Developers only touch model files — never migration files directly.
|
|
24
26
|
*/
|
|
25
27
|
class ModelInspector {
|
|
26
28
|
constructor(modelsPath, migrationsPath, snapshotPath) {
|
|
@@ -31,7 +33,7 @@ class ModelInspector {
|
|
|
31
33
|
|
|
32
34
|
/**
|
|
33
35
|
* Detect changes and generate migration files.
|
|
34
|
-
* Returns
|
|
36
|
+
* Returns { files: string[], message: string }
|
|
35
37
|
*/
|
|
36
38
|
async makeMigrations() {
|
|
37
39
|
const current = this._scanModels();
|
|
@@ -42,54 +44,144 @@ class ModelInspector {
|
|
|
42
44
|
return { files: [], message: 'No changes detected.' };
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
await fs.ensureDir(this._migrationsPath);
|
|
48
|
+
|
|
49
|
+
// All diffs in this run share the same timestamp prefix so they sort
|
|
50
|
+
// together and apply as a logical group.
|
|
51
|
+
const ts = this._timestamp();
|
|
45
52
|
const files = [];
|
|
53
|
+
|
|
46
54
|
for (const diff of diffs) {
|
|
47
|
-
const file = await this._generateMigration(diff);
|
|
55
|
+
const file = await this._generateMigration(diff, ts);
|
|
48
56
|
if (file) files.push(file);
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
// Persist the new baseline — must happen AFTER generating files so
|
|
60
|
+
// a crash mid-generation doesn't advance the snapshot prematurely.
|
|
51
61
|
this._saveSnapshot(current);
|
|
62
|
+
|
|
52
63
|
return { files, message: `Generated ${files.length} migration file(s).` };
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
// ─── Model scanning ───────────────────────────────────────────────────────
|
|
56
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Walk app/models/ and return a plain-object schema map:
|
|
70
|
+
* { tableName: { columnName: { type, nullable, … }, … }, … }
|
|
71
|
+
*
|
|
72
|
+
* Handles both default exports (`module.exports = MyModel`) and
|
|
73
|
+
* named exports (`module.exports = { MyModel }`).
|
|
74
|
+
*/
|
|
57
75
|
_scanModels() {
|
|
58
76
|
const schema = {};
|
|
77
|
+
|
|
59
78
|
if (!fs.existsSync(this._modelsPath)) return schema;
|
|
60
79
|
|
|
61
80
|
const files = fs.readdirSync(this._modelsPath)
|
|
62
|
-
.filter(f => f.endsWith('.js') && !f.startsWith('.'));
|
|
81
|
+
.filter(f => f.endsWith('.js') && !f.startsWith('.') && f !== 'index.js');
|
|
63
82
|
|
|
64
83
|
for (const file of files) {
|
|
84
|
+
const fullPath = path.join(this._modelsPath, file);
|
|
85
|
+
|
|
86
|
+
// Always bust require cache so the inspector picks up edits made
|
|
87
|
+
// in the same process (e.g. during tests).
|
|
65
88
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
89
|
+
delete require.cache[require.resolve(fullPath)];
|
|
90
|
+
} catch { /* path not yet cached — fine */ }
|
|
68
91
|
|
|
69
|
-
|
|
92
|
+
let exported;
|
|
93
|
+
try {
|
|
94
|
+
exported = require(fullPath);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
// Skip files that fail to parse / have runtime errors
|
|
97
|
+
process.stderr.write(` [makemigrations] Skipping ${file}: ${err.message}\n`);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Collect every candidate class from the export
|
|
102
|
+
const candidates = this._extractClasses(exported);
|
|
103
|
+
|
|
104
|
+
for (const ModelClass of candidates) {
|
|
105
|
+
if (!this._isMillasModel(ModelClass)) continue;
|
|
106
|
+
|
|
107
|
+
const table = this._resolveTable(ModelClass, file);
|
|
70
108
|
schema[table] = this._extractFields(ModelClass.fields);
|
|
71
|
-
} catch {
|
|
72
|
-
// Skip unloadable models
|
|
73
109
|
}
|
|
74
110
|
}
|
|
75
111
|
|
|
76
112
|
return schema;
|
|
77
113
|
}
|
|
78
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Given a module export (class, plain object, or anything), return an
|
|
117
|
+
* array of class/function values that might be Model subclasses.
|
|
118
|
+
*/
|
|
119
|
+
_extractClasses(exported) {
|
|
120
|
+
if (!exported) return [];
|
|
121
|
+
|
|
122
|
+
// Direct class export: module.exports = MyModel
|
|
123
|
+
if (typeof exported === 'function') return [exported];
|
|
124
|
+
|
|
125
|
+
// Named export object: module.exports = { MyModel, AnotherModel }
|
|
126
|
+
if (typeof exported === 'object') {
|
|
127
|
+
return Object.values(exported).filter(v => typeof v === 'function');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A class qualifies as a Millas Model if:
|
|
135
|
+
* - It is a function (class)
|
|
136
|
+
* - It has a static `fields` property that is a non-null object
|
|
137
|
+
*
|
|
138
|
+
* We intentionally do NOT do `instanceof` checks so the inspector
|
|
139
|
+
* works even when the user imports Model from a different resolution
|
|
140
|
+
* path than the one this file was loaded from.
|
|
141
|
+
*/
|
|
142
|
+
_isMillasModel(cls) {
|
|
143
|
+
if (typeof cls !== 'function') return false;
|
|
144
|
+
if (!cls.fields || typeof cls.fields !== 'object') return false;
|
|
145
|
+
// Must have at least one field
|
|
146
|
+
return Object.keys(cls.fields).length > 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Derive the table name from the model class or fall back to the file name.
|
|
151
|
+
*/
|
|
152
|
+
_resolveTable(ModelClass, fileName) {
|
|
153
|
+
// Explicitly set static table = '...'
|
|
154
|
+
if (typeof ModelClass.table === 'string' && ModelClass.table) {
|
|
155
|
+
return ModelClass.table;
|
|
156
|
+
}
|
|
157
|
+
// Convention: file name without extension, pluralised, lowercased
|
|
158
|
+
return fileName.replace(/\.js$/, '').toLowerCase() + 's';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Convert a fields map (whose values may be FieldDefinition instances or
|
|
163
|
+
* plain objects) into a stable plain-object representation suitable for
|
|
164
|
+
* snapshot storage and deterministic JSON comparison.
|
|
165
|
+
*/
|
|
79
166
|
_extractFields(fields) {
|
|
80
167
|
const result = {};
|
|
168
|
+
|
|
81
169
|
for (const [name, field] of Object.entries(fields)) {
|
|
170
|
+
// Normalise — accept both FieldDefinition instances and plain objects
|
|
82
171
|
result[name] = {
|
|
83
|
-
type:
|
|
84
|
-
nullable:
|
|
85
|
-
unique:
|
|
86
|
-
default:
|
|
87
|
-
max:
|
|
88
|
-
unsigned:
|
|
89
|
-
enumValues: field.enumValues
|
|
90
|
-
references: field.references
|
|
172
|
+
type: field.type ?? 'string',
|
|
173
|
+
nullable: field.nullable ?? false,
|
|
174
|
+
unique: field.unique ?? false,
|
|
175
|
+
default: field.default !== undefined ? field.default : null,
|
|
176
|
+
max: field.max ?? null,
|
|
177
|
+
unsigned: field.unsigned ?? false,
|
|
178
|
+
enumValues: field.enumValues ?? null,
|
|
179
|
+
references: field.references ?? null,
|
|
180
|
+
precision: field.precision ?? null,
|
|
181
|
+
scale: field.scale ?? null,
|
|
91
182
|
};
|
|
92
183
|
}
|
|
184
|
+
|
|
93
185
|
return result;
|
|
94
186
|
}
|
|
95
187
|
|
|
@@ -98,23 +190,23 @@ class ModelInspector {
|
|
|
98
190
|
_diff(current, snapshot) {
|
|
99
191
|
const diffs = [];
|
|
100
192
|
|
|
101
|
-
// New tables
|
|
193
|
+
// New tables (model added / first run)
|
|
102
194
|
for (const table of Object.keys(current)) {
|
|
103
195
|
if (!snapshot[table]) {
|
|
104
196
|
diffs.push({ type: 'create_table', table, fields: current[table] });
|
|
105
197
|
}
|
|
106
198
|
}
|
|
107
199
|
|
|
108
|
-
// Dropped tables
|
|
200
|
+
// Dropped tables (model file removed)
|
|
109
201
|
for (const table of Object.keys(snapshot)) {
|
|
110
202
|
if (!current[table]) {
|
|
111
|
-
diffs.push({ type: 'drop_table', table });
|
|
203
|
+
diffs.push({ type: 'drop_table', table, fields: snapshot[table] });
|
|
112
204
|
}
|
|
113
205
|
}
|
|
114
206
|
|
|
115
|
-
// Column changes on existing tables
|
|
207
|
+
// Column-level changes on existing tables
|
|
116
208
|
for (const table of Object.keys(current)) {
|
|
117
|
-
if (!snapshot[table]) continue;
|
|
209
|
+
if (!snapshot[table]) continue; // handled above as create_table
|
|
118
210
|
|
|
119
211
|
const curr = current[table];
|
|
120
212
|
const prev = snapshot[table];
|
|
@@ -129,15 +221,21 @@ class ModelInspector {
|
|
|
129
221
|
// Removed columns
|
|
130
222
|
for (const col of Object.keys(prev)) {
|
|
131
223
|
if (!curr[col]) {
|
|
132
|
-
diffs.push({ type: 'drop_column', table, column: col });
|
|
224
|
+
diffs.push({ type: 'drop_column', table, column: col, field: prev[col] });
|
|
133
225
|
}
|
|
134
226
|
}
|
|
135
227
|
|
|
136
|
-
// Changed columns
|
|
228
|
+
// Changed columns — compare each attribute individually for stability
|
|
137
229
|
for (const col of Object.keys(curr)) {
|
|
138
|
-
if (!prev[col]) continue;
|
|
139
|
-
if (
|
|
140
|
-
diffs.push({
|
|
230
|
+
if (!prev[col]) continue; // new column — already handled above
|
|
231
|
+
if (!this._fieldsEqual(curr[col], prev[col])) {
|
|
232
|
+
diffs.push({
|
|
233
|
+
type: 'alter_column',
|
|
234
|
+
table,
|
|
235
|
+
column: col,
|
|
236
|
+
field: curr[col],
|
|
237
|
+
previous: prev[col],
|
|
238
|
+
});
|
|
141
239
|
}
|
|
142
240
|
}
|
|
143
241
|
}
|
|
@@ -145,16 +243,25 @@ class ModelInspector {
|
|
|
145
243
|
return diffs;
|
|
146
244
|
}
|
|
147
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Stable field equality check that ignores key-ordering differences
|
|
248
|
+
* which can appear when objects are reconstituted from JSON.
|
|
249
|
+
*/
|
|
250
|
+
_fieldsEqual(a, b) {
|
|
251
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
252
|
+
for (const k of keys) {
|
|
253
|
+
if (JSON.stringify(a[k]) !== JSON.stringify(b[k])) return false;
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
148
258
|
// ─── Migration generation ─────────────────────────────────────────────────
|
|
149
259
|
|
|
150
|
-
async _generateMigration(diff) {
|
|
260
|
+
async _generateMigration(diff, ts) {
|
|
151
261
|
const name = this._diffToName(diff);
|
|
152
|
-
const ts = new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14);
|
|
153
262
|
const fileName = `${ts}_${name}.js`;
|
|
154
263
|
const filePath = path.join(this._migrationsPath, fileName);
|
|
155
264
|
|
|
156
|
-
await fs.ensureDir(this._migrationsPath);
|
|
157
|
-
|
|
158
265
|
const content = this._renderMigration(diff, name);
|
|
159
266
|
await fs.writeFile(filePath, content, 'utf8');
|
|
160
267
|
return fileName;
|
|
@@ -167,7 +274,7 @@ class ModelInspector {
|
|
|
167
274
|
case 'add_column': return `add_${diff.column}_to_${diff.table}`;
|
|
168
275
|
case 'drop_column': return `remove_${diff.column}_from_${diff.table}`;
|
|
169
276
|
case 'alter_column': return `alter_${diff.column}_on_${diff.table}`;
|
|
170
|
-
default: return `
|
|
277
|
+
default: return `auto_migration`;
|
|
171
278
|
}
|
|
172
279
|
}
|
|
173
280
|
|
|
@@ -178,8 +285,10 @@ class ModelInspector {
|
|
|
178
285
|
return `'use strict';
|
|
179
286
|
|
|
180
287
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
288
|
+
* Auto-generated migration: ${name}
|
|
289
|
+
* Created by: millas makemigrations
|
|
290
|
+
*
|
|
291
|
+
* DO NOT EDIT — changes to your model will generate a new migration.
|
|
183
292
|
*/
|
|
184
293
|
module.exports = {
|
|
185
294
|
async up(db) {
|
|
@@ -197,7 +306,8 @@ ${this._renderColumns(diff.fields)} });
|
|
|
197
306
|
return `'use strict';
|
|
198
307
|
|
|
199
308
|
/**
|
|
200
|
-
*
|
|
309
|
+
* Auto-generated migration: ${name}
|
|
310
|
+
* Created by: millas makemigrations
|
|
201
311
|
*/
|
|
202
312
|
module.exports = {
|
|
203
313
|
async up(db) {
|
|
@@ -205,11 +315,10 @@ module.exports = {
|
|
|
205
315
|
},
|
|
206
316
|
|
|
207
317
|
async down(db) {
|
|
208
|
-
//
|
|
318
|
+
// Restore the table — regenerate by reverting the model deletion
|
|
319
|
+
// and running: millas makemigrations
|
|
209
320
|
await db.schema.createTable('${diff.table}', (t) => {
|
|
210
|
-
|
|
211
|
-
t.timestamps();
|
|
212
|
-
});
|
|
321
|
+
${this._renderColumns(diff.fields || {})} });
|
|
213
322
|
},
|
|
214
323
|
};
|
|
215
324
|
`;
|
|
@@ -218,7 +327,8 @@ module.exports = {
|
|
|
218
327
|
return `'use strict';
|
|
219
328
|
|
|
220
329
|
/**
|
|
221
|
-
*
|
|
330
|
+
* Auto-generated migration: ${name}
|
|
331
|
+
* Created by: millas makemigrations
|
|
222
332
|
*/
|
|
223
333
|
module.exports = {
|
|
224
334
|
async up(db) {
|
|
@@ -239,7 +349,8 @@ ${this._renderColumn(' ', diff.column, diff.field)}
|
|
|
239
349
|
return `'use strict';
|
|
240
350
|
|
|
241
351
|
/**
|
|
242
|
-
*
|
|
352
|
+
* Auto-generated migration: ${name}
|
|
353
|
+
* Created by: millas makemigrations
|
|
243
354
|
*/
|
|
244
355
|
module.exports = {
|
|
245
356
|
async up(db) {
|
|
@@ -250,7 +361,7 @@ module.exports = {
|
|
|
250
361
|
|
|
251
362
|
async down(db) {
|
|
252
363
|
await db.schema.table('${diff.table}', (t) => {
|
|
253
|
-
|
|
364
|
+
${this._renderColumn(' ', diff.column, diff.field)}
|
|
254
365
|
});
|
|
255
366
|
},
|
|
256
367
|
};
|
|
@@ -260,7 +371,8 @@ module.exports = {
|
|
|
260
371
|
return `'use strict';
|
|
261
372
|
|
|
262
373
|
/**
|
|
263
|
-
*
|
|
374
|
+
* Auto-generated migration: ${name}
|
|
375
|
+
* Created by: millas makemigrations
|
|
264
376
|
* Changed: ${JSON.stringify(diff.previous)} → ${JSON.stringify(diff.field)}
|
|
265
377
|
*/
|
|
266
378
|
module.exports = {
|
|
@@ -284,6 +396,9 @@ ${this._renderColumn(' ', diff.column, diff.previous, '.alter()')}
|
|
|
284
396
|
}
|
|
285
397
|
|
|
286
398
|
_renderColumns(fields) {
|
|
399
|
+
if (!fields || Object.keys(fields).length === 0) {
|
|
400
|
+
return ' t.increments(\'id\');\n t.timestamps();\n';
|
|
401
|
+
}
|
|
287
402
|
return Object.entries(fields)
|
|
288
403
|
.map(([name, field]) => this._renderColumn(' ', name, field))
|
|
289
404
|
.join('\n') + '\n';
|
|
@@ -291,30 +406,81 @@ ${this._renderColumn(' ', diff.column, diff.previous, '.alter()')}
|
|
|
291
406
|
|
|
292
407
|
_renderColumn(indent, name, field, suffix = '') {
|
|
293
408
|
let line;
|
|
409
|
+
|
|
294
410
|
switch (field.type) {
|
|
295
|
-
case 'id':
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
case '
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
case '
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
case '
|
|
307
|
-
|
|
308
|
-
|
|
411
|
+
case 'id':
|
|
412
|
+
return `${indent}t.increments('${name}')${suffix};`;
|
|
413
|
+
|
|
414
|
+
case 'string':
|
|
415
|
+
line = `t.string('${name}', ${field.max || 255})`;
|
|
416
|
+
break;
|
|
417
|
+
|
|
418
|
+
case 'text':
|
|
419
|
+
line = `t.text('${name}')`;
|
|
420
|
+
break;
|
|
421
|
+
|
|
422
|
+
case 'integer':
|
|
423
|
+
line = field.unsigned
|
|
424
|
+
? `t.integer('${name}').unsigned()`
|
|
425
|
+
: `t.integer('${name}')`;
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case 'bigInteger':
|
|
429
|
+
line = field.unsigned
|
|
430
|
+
? `t.bigInteger('${name}').unsigned()`
|
|
431
|
+
: `t.bigInteger('${name}')`;
|
|
432
|
+
break;
|
|
433
|
+
|
|
434
|
+
case 'float':
|
|
435
|
+
line = `t.float('${name}')`;
|
|
436
|
+
break;
|
|
437
|
+
|
|
438
|
+
case 'decimal':
|
|
439
|
+
line = `t.decimal('${name}', ${field.precision || 8}, ${field.scale || 2})`;
|
|
440
|
+
break;
|
|
441
|
+
|
|
442
|
+
case 'boolean':
|
|
443
|
+
line = `t.boolean('${name}')`;
|
|
444
|
+
break;
|
|
445
|
+
|
|
446
|
+
case 'json':
|
|
447
|
+
line = `t.json('${name}')`;
|
|
448
|
+
break;
|
|
449
|
+
|
|
450
|
+
case 'date':
|
|
451
|
+
line = `t.date('${name}')`;
|
|
452
|
+
break;
|
|
453
|
+
|
|
454
|
+
case 'timestamp':
|
|
455
|
+
line = `t.timestamp('${name}', { useTz: false })`;
|
|
456
|
+
break;
|
|
457
|
+
|
|
458
|
+
case 'enum':
|
|
459
|
+
line = `t.enum('${name}', ${JSON.stringify(field.enumValues || [])})`;
|
|
460
|
+
break;
|
|
461
|
+
|
|
462
|
+
case 'uuid':
|
|
463
|
+
line = `t.uuid('${name}')`;
|
|
464
|
+
break;
|
|
465
|
+
|
|
466
|
+
default:
|
|
467
|
+
line = `t.string('${name}')`;
|
|
309
468
|
}
|
|
310
469
|
|
|
311
|
-
if (field.nullable)
|
|
312
|
-
else if (field.type !== 'id')
|
|
313
|
-
|
|
314
|
-
if (field.
|
|
315
|
-
|
|
316
|
-
if (field.
|
|
317
|
-
line += `.
|
|
470
|
+
if (field.nullable) line += '.nullable()';
|
|
471
|
+
else if (field.type !== 'id') line += '.notNullable()';
|
|
472
|
+
|
|
473
|
+
if (field.unique) line += '.unique()';
|
|
474
|
+
|
|
475
|
+
if (field.default !== null && field.default !== undefined) {
|
|
476
|
+
line += `.defaultTo(${JSON.stringify(field.default)})`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (field.references) {
|
|
480
|
+
line += `.references('${field.references.column}')` +
|
|
481
|
+
`.inTable('${field.references.table}')` +
|
|
482
|
+
`.onDelete('CASCADE')`;
|
|
483
|
+
}
|
|
318
484
|
|
|
319
485
|
return `${indent}${line}${suffix};`;
|
|
320
486
|
}
|
|
@@ -333,6 +499,12 @@ ${this._renderColumn(' ', diff.column, diff.previous, '.alter()')}
|
|
|
333
499
|
fs.ensureDirSync(path.dirname(this._snapshotPath));
|
|
334
500
|
fs.writeJsonSync(this._snapshotPath, schema, { spaces: 2 });
|
|
335
501
|
}
|
|
502
|
+
|
|
503
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
_timestamp() {
|
|
506
|
+
return new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14);
|
|
507
|
+
}
|
|
336
508
|
}
|
|
337
509
|
|
|
338
510
|
module.exports = ModelInspector;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MySQL / MariaDB — drop all user tables in the active database.
|
|
5
|
+
*/
|
|
6
|
+
async function dropAllTables(db) {
|
|
7
|
+
const dbName = db.client.config.connection.database;
|
|
8
|
+
|
|
9
|
+
const rows = await db
|
|
10
|
+
.select('TABLE_NAME as name')
|
|
11
|
+
.from('information_schema.TABLES')
|
|
12
|
+
.where('TABLE_SCHEMA', dbName)
|
|
13
|
+
.where('TABLE_TYPE', 'BASE TABLE');
|
|
14
|
+
|
|
15
|
+
if (rows.length === 0) return;
|
|
16
|
+
|
|
17
|
+
await db.raw('SET FOREIGN_KEY_CHECKS = 0');
|
|
18
|
+
|
|
19
|
+
for (const { name } of rows) {
|
|
20
|
+
await db.schema.dropTableIfExists(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await db.raw('SET FOREIGN_KEY_CHECKS = 1');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { dropAllTables };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostgreSQL — drop all user tables in the public schema.
|
|
5
|
+
*/
|
|
6
|
+
async function dropAllTables(db) {
|
|
7
|
+
const rows = await db
|
|
8
|
+
.select('tablename')
|
|
9
|
+
.from('pg_tables')
|
|
10
|
+
.where('schemaname', 'public');
|
|
11
|
+
|
|
12
|
+
if (rows.length === 0) return;
|
|
13
|
+
|
|
14
|
+
const names = rows.map(r => `"${r.tablename}"`).join(', ');
|
|
15
|
+
await db.raw(`DROP TABLE IF EXISTS ${names} CASCADE`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { dropAllTables };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQLite — drop all user tables.
|
|
5
|
+
* Reads table names from sqlite_master.
|
|
6
|
+
*/
|
|
7
|
+
async function dropAllTables(db) {
|
|
8
|
+
// Disable FK checks so drops don't fail on references
|
|
9
|
+
await db.raw('PRAGMA foreign_keys = OFF');
|
|
10
|
+
|
|
11
|
+
const tables = await db
|
|
12
|
+
.select('name')
|
|
13
|
+
.from('sqlite_master')
|
|
14
|
+
.where('type', 'table')
|
|
15
|
+
.whereNot('name', 'like', 'sqlite_%');
|
|
16
|
+
|
|
17
|
+
for (const { name } of tables) {
|
|
18
|
+
await db.schema.dropTableIfExists(name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await db.raw('PRAGMA foreign_keys = ON');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = { dropAllTables };
|