millas 0.1.7 → 0.1.8
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/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/scaffold/templates.js +26 -3
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "millas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
7
|
-
".":
|
|
8
|
-
"./src":
|
|
9
|
-
"./src/*":
|
|
10
|
-
"./bin/*":
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./src": "./src/index.js",
|
|
9
|
+
"./src/*": "./src/*.js",
|
|
10
|
+
"./bin/*": "./bin/*.js"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
13
|
"millas": "./bin/millas.js"
|
|
@@ -16,9 +16,20 @@
|
|
|
16
16
|
"test": "echo \"Run phase-specific tests in tests/\""
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
|
-
"framework",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
19
|
+
"framework",
|
|
20
|
+
"express",
|
|
21
|
+
"orm",
|
|
22
|
+
"cli",
|
|
23
|
+
"auth",
|
|
24
|
+
"queue",
|
|
25
|
+
"cache",
|
|
26
|
+
"admin",
|
|
27
|
+
"mail",
|
|
28
|
+
"events",
|
|
29
|
+
"laravel",
|
|
30
|
+
"django",
|
|
31
|
+
"fastapi",
|
|
32
|
+
"millas"
|
|
22
33
|
],
|
|
23
34
|
"author": "Millas Framework",
|
|
24
35
|
"license": "MIT",
|
|
@@ -26,22 +37,24 @@
|
|
|
26
37
|
"node": ">=18.0.0"
|
|
27
38
|
},
|
|
28
39
|
"dependencies": {
|
|
29
|
-
"bcryptjs":
|
|
30
|
-
"chalk":
|
|
31
|
-
"commander":
|
|
32
|
-
"fs-extra":
|
|
33
|
-
"inquirer":
|
|
34
|
-
"jsonwebtoken":"^9.0.3",
|
|
35
|
-
"knex":
|
|
36
|
-
"nodemailer":
|
|
37
|
-
"nunjucks":
|
|
38
|
-
"ora":
|
|
40
|
+
"bcryptjs": "3.0.2",
|
|
41
|
+
"chalk": "4.1.2",
|
|
42
|
+
"commander": "^11.0.0",
|
|
43
|
+
"fs-extra": "^11.0.0",
|
|
44
|
+
"inquirer": "8.2.6",
|
|
45
|
+
"jsonwebtoken": "^9.0.3",
|
|
46
|
+
"knex": "^3.1.0",
|
|
47
|
+
"nodemailer": "^6.9.0",
|
|
48
|
+
"nunjucks": "^3.2.4",
|
|
49
|
+
"ora": "5.4.1"
|
|
39
50
|
},
|
|
40
51
|
"peerDependencies": {
|
|
41
52
|
"express": "^4.18.0"
|
|
42
53
|
},
|
|
43
54
|
"peerDependenciesMeta": {
|
|
44
|
-
"express": {
|
|
55
|
+
"express": {
|
|
56
|
+
"optional": false
|
|
57
|
+
}
|
|
45
58
|
},
|
|
46
59
|
"files": [
|
|
47
60
|
"bin/",
|
package/src/commands/migrate.js
CHANGED
|
@@ -6,133 +6,185 @@ const fs = require('fs-extra');
|
|
|
6
6
|
|
|
7
7
|
module.exports = function (program) {
|
|
8
8
|
|
|
9
|
+
// ── makemigrations ────────────────────────────────────────────────────────
|
|
10
|
+
|
|
9
11
|
program
|
|
10
12
|
.command('makemigrations')
|
|
11
|
-
.description('
|
|
13
|
+
.description('Scan model files, detect schema changes, generate migration files')
|
|
12
14
|
.action(async () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
try {
|
|
16
|
+
const ctx = getProjectContext();
|
|
17
|
+
// Fixed: was incorrectly destructured as { ModelInspector }
|
|
18
|
+
const ModelInspector = require('../orm/migration/ModelInspector');
|
|
19
|
+
const inspector = new ModelInspector(
|
|
20
|
+
ctx.modelsPath,
|
|
21
|
+
ctx.migrationsPath,
|
|
22
|
+
ctx.snapshotPath,
|
|
23
|
+
);
|
|
24
|
+
const result = await inspector.makeMigrations();
|
|
25
|
+
|
|
26
|
+
if (result.files.length === 0) {
|
|
27
|
+
console.log(chalk.yellow(`\n ${result.message}\n`));
|
|
28
|
+
} else {
|
|
29
|
+
console.log(chalk.green(`\n ✔ ${result.message}`));
|
|
30
|
+
result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
|
|
31
|
+
console.log(chalk.gray('\n Run: millas migrate to apply these migrations.\n'));
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(chalk.red(`\n ✖ makemigrations failed: ${err.message}\n`));
|
|
35
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
36
|
+
process.exit(1);
|
|
27
37
|
}
|
|
28
38
|
});
|
|
29
39
|
|
|
40
|
+
// ── migrate ───────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
30
42
|
program
|
|
31
43
|
.command('migrate')
|
|
32
44
|
.description('Run all pending migrations')
|
|
33
45
|
.action(async () => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
46
|
+
try {
|
|
47
|
+
const runner = await getRunner();
|
|
48
|
+
const result = await runner.migrate();
|
|
49
|
+
printResult(result, 'Ran');
|
|
50
|
+
} catch (err) {
|
|
51
|
+
bail('migrate', err);
|
|
52
|
+
}
|
|
37
53
|
});
|
|
38
54
|
|
|
55
|
+
// ── migrate:fresh ─────────────────────────────────────────────────────────
|
|
56
|
+
|
|
39
57
|
program
|
|
40
58
|
.command('migrate:fresh')
|
|
41
|
-
.description('Drop
|
|
59
|
+
.description('Drop ALL tables then re-run every migration from scratch')
|
|
42
60
|
.action(async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
61
|
+
try {
|
|
62
|
+
console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
|
|
63
|
+
const runner = await getRunner();
|
|
64
|
+
const result = await runner.fresh();
|
|
65
|
+
printResult(result, 'Ran');
|
|
66
|
+
} catch (err) {
|
|
67
|
+
bail('migrate:fresh', err);
|
|
68
|
+
}
|
|
47
69
|
});
|
|
48
70
|
|
|
71
|
+
// ── migrate:rollback ──────────────────────────────────────────────────────
|
|
72
|
+
|
|
49
73
|
program
|
|
50
74
|
.command('migrate:rollback')
|
|
51
75
|
.description('Rollback the last batch of migrations')
|
|
52
76
|
.option('--steps <n>', 'Number of batches to rollback', '1')
|
|
53
77
|
.action(async (options) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
try {
|
|
79
|
+
const runner = await getRunner();
|
|
80
|
+
const result = await runner.rollback(Number(options.steps));
|
|
81
|
+
printResult(result, 'Rolled back');
|
|
82
|
+
} catch (err) {
|
|
83
|
+
bail('migrate:rollback', err);
|
|
84
|
+
}
|
|
57
85
|
});
|
|
58
86
|
|
|
87
|
+
// ── migrate:reset ─────────────────────────────────────────────────────────
|
|
88
|
+
|
|
59
89
|
program
|
|
60
90
|
.command('migrate:reset')
|
|
61
|
-
.description('Rollback
|
|
91
|
+
.description('Rollback ALL migrations')
|
|
62
92
|
.action(async () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
try {
|
|
94
|
+
const runner = await getRunner();
|
|
95
|
+
const result = await runner.reset();
|
|
96
|
+
printResult(result, 'Rolled back');
|
|
97
|
+
} catch (err) {
|
|
98
|
+
bail('migrate:reset', err);
|
|
99
|
+
}
|
|
66
100
|
});
|
|
67
101
|
|
|
102
|
+
// ── migrate:refresh ───────────────────────────────────────────────────────
|
|
103
|
+
|
|
68
104
|
program
|
|
69
105
|
.command('migrate:refresh')
|
|
70
|
-
.description('Rollback all
|
|
106
|
+
.description('Rollback all then re-run all migrations')
|
|
71
107
|
.action(async () => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
108
|
+
try {
|
|
109
|
+
const runner = await getRunner();
|
|
110
|
+
const result = await runner.refresh();
|
|
111
|
+
printResult(result, 'Ran');
|
|
112
|
+
} catch (err) {
|
|
113
|
+
bail('migrate:refresh', err);
|
|
114
|
+
}
|
|
75
115
|
});
|
|
76
116
|
|
|
117
|
+
// ── migrate:status ────────────────────────────────────────────────────────
|
|
118
|
+
|
|
77
119
|
program
|
|
78
120
|
.command('migrate:status')
|
|
79
|
-
.description('Show the status of all
|
|
121
|
+
.description('Show the status of all migration files')
|
|
80
122
|
.action(async () => {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
123
|
+
try {
|
|
124
|
+
const runner = await getRunner();
|
|
125
|
+
const rows = await runner.status();
|
|
126
|
+
|
|
127
|
+
if (rows.length === 0) {
|
|
128
|
+
console.log(chalk.yellow('\n No migration files found.\n'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const colW = Math.max(...rows.map(r => r.name.length)) + 2;
|
|
133
|
+
console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
|
|
134
|
+
console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
|
|
135
|
+
|
|
136
|
+
for (const row of rows) {
|
|
137
|
+
const status = row.status === 'Ran'
|
|
138
|
+
? chalk.green(row.status.padEnd(10))
|
|
139
|
+
: chalk.yellow(row.status.padEnd(10));
|
|
140
|
+
const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
|
|
141
|
+
console.log(` ${chalk.cyan(row.name.padEnd(colW))} ${status} ${batch}`);
|
|
142
|
+
}
|
|
143
|
+
console.log();
|
|
144
|
+
} catch (err) {
|
|
145
|
+
bail('migrate:status', err);
|
|
99
146
|
}
|
|
100
|
-
console.log();
|
|
101
147
|
});
|
|
102
148
|
|
|
149
|
+
// ── db:seed ───────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
103
151
|
program
|
|
104
152
|
.command('db:seed')
|
|
105
153
|
.description('Run all database seeders')
|
|
106
154
|
.action(async () => {
|
|
107
|
-
|
|
108
|
-
|
|
155
|
+
try {
|
|
156
|
+
const ctx = getProjectContext();
|
|
157
|
+
const seedersDir = ctx.seedersPath;
|
|
109
158
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
159
|
+
if (!fs.existsSync(seedersDir)) {
|
|
160
|
+
console.log(chalk.yellow('\n No seeders directory found.\n'));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
114
163
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
164
|
+
const files = fs.readdirSync(seedersDir)
|
|
165
|
+
.filter(f => f.endsWith('.js') && !f.startsWith('.'))
|
|
166
|
+
.sort();
|
|
118
167
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
168
|
+
if (files.length === 0) {
|
|
169
|
+
console.log(chalk.yellow('\n No seeder files found.\n'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
123
172
|
|
|
124
|
-
console.log();
|
|
125
|
-
for (const file of files) {
|
|
126
|
-
const seeder = require(path.join(seedersDir, file));
|
|
127
173
|
const db = await getDbConnection();
|
|
128
|
-
|
|
129
|
-
|
|
174
|
+
console.log();
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
const seeder = require(path.join(seedersDir, file));
|
|
177
|
+
await seeder.run(db);
|
|
178
|
+
console.log(chalk.green(` ✔ Seeded: ${file}`));
|
|
179
|
+
}
|
|
180
|
+
console.log();
|
|
181
|
+
} catch (err) {
|
|
182
|
+
bail('db:seed', err);
|
|
130
183
|
}
|
|
131
|
-
console.log();
|
|
132
184
|
});
|
|
133
185
|
};
|
|
134
186
|
|
|
135
|
-
// ─── Helpers
|
|
187
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
136
188
|
|
|
137
189
|
function getProjectContext() {
|
|
138
190
|
const cwd = process.cwd();
|
|
@@ -149,26 +201,35 @@ async function getDbConnection() {
|
|
|
149
201
|
if (!fs.existsSync(configPath + '.js')) {
|
|
150
202
|
throw new Error('config/database.js not found. Are you inside a Millas project?');
|
|
151
203
|
}
|
|
152
|
-
const config
|
|
204
|
+
const config = require(configPath);
|
|
153
205
|
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
154
206
|
DatabaseManager.configure(config);
|
|
155
207
|
return DatabaseManager.connection();
|
|
156
208
|
}
|
|
157
209
|
|
|
158
210
|
async function getRunner() {
|
|
211
|
+
// Fixed: was incorrectly destructured as { MigrationRunner }
|
|
159
212
|
const MigrationRunner = require('../orm/migration/MigrationRunner');
|
|
160
213
|
const ctx = getProjectContext();
|
|
161
214
|
const db = await getDbConnection();
|
|
162
215
|
return new MigrationRunner(db, ctx.migrationsPath);
|
|
163
216
|
}
|
|
164
217
|
|
|
165
|
-
function
|
|
218
|
+
function printResult(result, verb) {
|
|
166
219
|
const list = result.ran || result.rolledBack || [];
|
|
167
220
|
if (list.length === 0) {
|
|
168
221
|
console.log(chalk.yellow(`\n ${result.message}\n`));
|
|
169
222
|
return;
|
|
170
223
|
}
|
|
171
224
|
console.log(chalk.green(`\n ✔ ${result.message}`));
|
|
172
|
-
list.forEach(f =>
|
|
225
|
+
list.forEach(f =>
|
|
226
|
+
console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`)),
|
|
227
|
+
);
|
|
173
228
|
console.log();
|
|
174
229
|
}
|
|
230
|
+
|
|
231
|
+
function bail(cmd, err) {
|
|
232
|
+
console.error(chalk.red(`\n ✖ ${cmd} failed: ${err.message}\n`));
|
|
233
|
+
if (process.env.DEBUG) console.error(err.stack);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
@@ -7,19 +7,19 @@ const path = require('path');
|
|
|
7
7
|
* MigrationRunner
|
|
8
8
|
*
|
|
9
9
|
* Handles the full migration lifecycle:
|
|
10
|
-
* - run pending migrations
|
|
11
|
-
* - rollback last batch
|
|
12
|
-
* - show status table
|
|
13
|
-
* - drop all + re-run
|
|
14
|
-
* - rollback all
|
|
15
|
-
* - rollback all + re-run
|
|
10
|
+
* - run pending migrations (migrate)
|
|
11
|
+
* - rollback last batch (migrate:rollback)
|
|
12
|
+
* - show status table (migrate:status)
|
|
13
|
+
* - drop all + re-run (migrate:fresh)
|
|
14
|
+
* - rollback all (migrate:reset)
|
|
15
|
+
* - rollback all + re-run (migrate:refresh)
|
|
16
16
|
*
|
|
17
17
|
* Migration history is tracked in the `millas_migrations` table.
|
|
18
18
|
* Each migration file must export { up(db), down(db) }.
|
|
19
19
|
*/
|
|
20
20
|
class MigrationRunner {
|
|
21
21
|
/**
|
|
22
|
-
* @param {object} knexConn
|
|
22
|
+
* @param {object} knexConn — live knex connection
|
|
23
23
|
* @param {string} migrationsPath — absolute path to migrations dir
|
|
24
24
|
*/
|
|
25
25
|
constructor(knexConn, migrationsPath) {
|
|
@@ -29,9 +29,7 @@ class MigrationRunner {
|
|
|
29
29
|
|
|
30
30
|
// ─── Public commands ──────────────────────────────────────────────────────
|
|
31
31
|
|
|
32
|
-
/**
|
|
33
|
-
* Run all pending migrations.
|
|
34
|
-
*/
|
|
32
|
+
/** Run all pending migrations. */
|
|
35
33
|
async migrate() {
|
|
36
34
|
await this._ensureTable();
|
|
37
35
|
const pending = await this._pending();
|
|
@@ -53,9 +51,7 @@ class MigrationRunner {
|
|
|
53
51
|
return { ran, batch, message: `Ran ${ran.length} migration(s).` };
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
/**
|
|
57
|
-
* Rollback the last batch of migrations.
|
|
58
|
-
*/
|
|
54
|
+
/** Rollback the last batch of migrations. */
|
|
59
55
|
async rollback(steps = 1) {
|
|
60
56
|
await this._ensureTable();
|
|
61
57
|
const batches = await this._lastBatches(steps);
|
|
@@ -66,7 +62,6 @@ class MigrationRunner {
|
|
|
66
62
|
|
|
67
63
|
const rolledBack = [];
|
|
68
64
|
|
|
69
|
-
// Run in reverse order
|
|
70
65
|
for (const row of [...batches].reverse()) {
|
|
71
66
|
const migration = this._load(row.name);
|
|
72
67
|
await migration.down(this._db);
|
|
@@ -77,18 +72,14 @@ class MigrationRunner {
|
|
|
77
72
|
return { rolledBack, message: `Rolled back ${rolledBack.length} migration(s).` };
|
|
78
73
|
}
|
|
79
74
|
|
|
80
|
-
/**
|
|
81
|
-
* Drop all tables and re-run every migration.
|
|
82
|
-
*/
|
|
75
|
+
/** Drop all tables and re-run every migration. */
|
|
83
76
|
async fresh() {
|
|
84
77
|
await this._dropAllTables();
|
|
85
78
|
await this._ensureTable();
|
|
86
79
|
return this.migrate();
|
|
87
80
|
}
|
|
88
81
|
|
|
89
|
-
/**
|
|
90
|
-
* Rollback ALL migrations.
|
|
91
|
-
*/
|
|
82
|
+
/** Rollback ALL migrations. */
|
|
92
83
|
async reset() {
|
|
93
84
|
await this._ensureTable();
|
|
94
85
|
const all = await this._db('millas_migrations').orderBy('id', 'desc');
|
|
@@ -108,17 +99,13 @@ class MigrationRunner {
|
|
|
108
99
|
return { rolledBack, message: `Reset ${rolledBack.length} migration(s).` };
|
|
109
100
|
}
|
|
110
101
|
|
|
111
|
-
/**
|
|
112
|
-
* Rollback all then re-run all.
|
|
113
|
-
*/
|
|
102
|
+
/** Rollback all then re-run all. */
|
|
114
103
|
async refresh() {
|
|
115
104
|
await this.reset();
|
|
116
105
|
return this.migrate();
|
|
117
106
|
}
|
|
118
107
|
|
|
119
|
-
/**
|
|
120
|
-
* Return status of all migration files.
|
|
121
|
-
*/
|
|
108
|
+
/** Return status of all migration files. */
|
|
122
109
|
async status() {
|
|
123
110
|
await this._ensureTable();
|
|
124
111
|
const files = this._files();
|
|
@@ -127,7 +114,7 @@ class MigrationRunner {
|
|
|
127
114
|
return files.map(file => ({
|
|
128
115
|
name: file,
|
|
129
116
|
status: ran.has(file) ? 'Ran' : 'Pending',
|
|
130
|
-
batch: ran.get
|
|
117
|
+
batch: ran.get(file) || null,
|
|
131
118
|
}));
|
|
132
119
|
}
|
|
133
120
|
|
|
@@ -153,9 +140,7 @@ class MigrationRunner {
|
|
|
153
140
|
|
|
154
141
|
async _ranNames() {
|
|
155
142
|
const rows = await this._db('millas_migrations').select('name', 'batch');
|
|
156
|
-
|
|
157
|
-
// Map with has() and get()
|
|
158
|
-
return map;
|
|
143
|
+
return new Map(rows.map(r => [r.name, r.batch]));
|
|
159
144
|
}
|
|
160
145
|
|
|
161
146
|
async _nextBatch() {
|
|
@@ -168,34 +153,39 @@ class MigrationRunner {
|
|
|
168
153
|
if (!maxBatch?.max) return [];
|
|
169
154
|
|
|
170
155
|
const fromBatch = maxBatch.max - steps + 1;
|
|
171
|
-
// Fetch all then filter in JS to avoid needing >= support in all drivers
|
|
172
156
|
const all = await this._db('millas_migrations').orderBy('id', 'desc');
|
|
173
157
|
return all.filter(r => r.batch >= fromBatch);
|
|
174
158
|
}
|
|
175
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Drop all user tables — dialect-aware.
|
|
162
|
+
* Resolves the knex client name and delegates to the right helper.
|
|
163
|
+
*/
|
|
176
164
|
async _dropAllTables() {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
165
|
+
const clientName = this._db.client.config.client || 'sqlite3';
|
|
166
|
+
|
|
167
|
+
let dialect;
|
|
168
|
+
if (clientName.includes('pg') || clientName.includes('postgres')) {
|
|
169
|
+
dialect = require('./dialects/postgres');
|
|
170
|
+
} else if (clientName.includes('mysql') || clientName.includes('maria')) {
|
|
171
|
+
dialect = require('./dialects/mysql');
|
|
172
|
+
} else {
|
|
173
|
+
// Default: sqlite / sqlite3
|
|
174
|
+
dialect = require('./dialects/sqlite');
|
|
186
175
|
}
|
|
176
|
+
|
|
177
|
+
await dialect.dropAllTables(this._db);
|
|
187
178
|
}
|
|
188
179
|
|
|
189
180
|
_files() {
|
|
190
181
|
if (!fs.existsSync(this._path)) return [];
|
|
191
182
|
return fs.readdirSync(this._path)
|
|
192
183
|
.filter(f => f.endsWith('.js') && !f.startsWith('.'))
|
|
193
|
-
.sort();
|
|
184
|
+
.sort();
|
|
194
185
|
}
|
|
195
186
|
|
|
196
187
|
_load(name) {
|
|
197
188
|
const filePath = path.join(this._path, name);
|
|
198
|
-
// Clear require cache so fresh loads work in tests
|
|
199
189
|
delete require.cache[require.resolve(filePath)];
|
|
200
190
|
const migration = require(filePath);
|
|
201
191
|
if (typeof migration.up !== 'function' || typeof migration.down !== 'function') {
|
|
@@ -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 };
|
|
@@ -66,6 +66,8 @@ storage/logs/*.log
|
|
|
66
66
|
storage/uploads/*
|
|
67
67
|
!storage/uploads/.gitkeep
|
|
68
68
|
database/database.sqlite
|
|
69
|
+
# Millas migration snapshot — auto-generated, do not commit
|
|
70
|
+
.millas/
|
|
69
71
|
`,
|
|
70
72
|
|
|
71
73
|
// ─── millas.config.js ─────────────────────────────────────────
|
|
@@ -356,17 +358,37 @@ millas make:controller UserController
|
|
|
356
358
|
|
|
357
359
|
# Generate a model
|
|
358
360
|
millas make:model User
|
|
361
|
+
\`\`\`
|
|
362
|
+
|
|
363
|
+
## Database Migrations (Django-style)
|
|
364
|
+
|
|
365
|
+
Millas handles migrations automatically — you only edit your model files.
|
|
366
|
+
|
|
367
|
+
\`\`\`bash
|
|
368
|
+
# 1. Edit app/models/User.js — add, remove, or change fields
|
|
369
|
+
# 2. Generate migration files from your changes
|
|
370
|
+
millas makemigrations
|
|
359
371
|
|
|
360
|
-
#
|
|
372
|
+
# 3. Apply pending migrations to the database
|
|
361
373
|
millas migrate
|
|
362
374
|
\`\`\`
|
|
363
375
|
|
|
376
|
+
Other migration commands:
|
|
377
|
+
|
|
378
|
+
\`\`\`bash
|
|
379
|
+
millas migrate:status # Show which migrations have run
|
|
380
|
+
millas migrate:rollback # Undo the last batch
|
|
381
|
+
millas migrate:fresh # Drop everything and re-run all migrations
|
|
382
|
+
millas migrate:reset # Roll back all migrations
|
|
383
|
+
millas migrate:refresh # Reset + re-run (like fresh but using down() methods)
|
|
384
|
+
\`\`\`
|
|
385
|
+
|
|
364
386
|
## Project Structure
|
|
365
387
|
|
|
366
388
|
\`\`\`
|
|
367
389
|
app/
|
|
368
390
|
controllers/ # HTTP controllers
|
|
369
|
-
models/ # ORM models
|
|
391
|
+
models/ # ORM models ← only file you edit for schema changes
|
|
370
392
|
services/ # Business logic
|
|
371
393
|
middleware/ # HTTP middleware
|
|
372
394
|
jobs/ # Background jobs
|
|
@@ -374,13 +396,14 @@ bootstrap/
|
|
|
374
396
|
app.js # Application entry point
|
|
375
397
|
config/ # Configuration files
|
|
376
398
|
database/
|
|
377
|
-
migrations/ #
|
|
399
|
+
migrations/ # Auto-generated — do not edit by hand
|
|
378
400
|
seeders/ # Database seeders
|
|
379
401
|
routes/
|
|
380
402
|
web.js # Web routes
|
|
381
403
|
api.js # API routes
|
|
382
404
|
storage/ # Logs, uploads
|
|
383
405
|
providers/ # Service providers
|
|
406
|
+
.millas/ # Migration snapshot (gitignored)
|
|
384
407
|
\`\`\`
|
|
385
408
|
`,
|
|
386
409
|
};
|