millas 0.2.12-beta → 0.2.12-beta-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -16
- package/src/auth/Auth.js +13 -8
- package/src/auth/AuthController.js +3 -1
- package/src/auth/AuthUser.js +98 -0
- package/src/cli.js +1 -1
- package/src/commands/serve.js +81 -110
- package/src/container/AppInitializer.js +158 -0
- package/src/container/Application.js +278 -253
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +23 -280
- package/src/container/MillasConfig.js +163 -0
- package/src/core/auth.js +9 -0
- package/src/core/db.js +8 -0
- package/src/core/foundation.js +67 -0
- package/src/core/http.js +11 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/facades/Admin.js +1 -1
- package/src/facades/Auth.js +22 -39
- package/src/facades/Cache.js +21 -10
- package/src/facades/Database.js +1 -1
- package/src/facades/Events.js +18 -17
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +42 -45
- package/src/facades/Log.js +25 -49
- package/src/facades/Mail.js +27 -32
- package/src/facades/Queue.js +22 -15
- package/src/facades/Storage.js +18 -10
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/ResponseDispatcher.js +18 -111
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/index.js +5 -144
- package/src/logger/formatters/PrettyFormatter.js +15 -5
- package/src/logger/internal.js +2 -2
- package/src/logger/patchConsole.js +91 -81
- package/src/middleware/MiddlewareRegistry.js +62 -82
- package/src/orm/migration/ModelInspector.js +339 -340
- package/src/providers/AuthServiceProvider.js +9 -5
- package/src/providers/CacheStorageServiceProvider.js +3 -1
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +3 -2
- package/src/providers/MailServiceProvider.js +3 -2
- package/src/providers/QueueServiceProvider.js +3 -2
- package/src/router/Router.js +119 -152
- package/src/scaffold/templates.js +8 -7
- package/src/facades/Validation.js +0 -69
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs
|
|
4
|
-
const path
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
5
|
const MillasLog = require('../../logger/internal');
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -26,267 +26,266 @@ const MillasLog = require('../../logger/internal');
|
|
|
26
26
|
* Developers only touch model files — never migration files directly.
|
|
27
27
|
*/
|
|
28
28
|
class ModelInspector {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Detect changes and generate migration files.
|
|
37
|
-
* Returns { files: string[], message: string }
|
|
38
|
-
*/
|
|
39
|
-
async makeMigrations() {
|
|
40
|
-
const current = this._scanModels();
|
|
41
|
-
const snapshot = this._loadSnapshot();
|
|
42
|
-
const diffs = this._diff(current, snapshot);
|
|
43
|
-
|
|
44
|
-
if (diffs.length === 0) {
|
|
45
|
-
return { files: [], message: 'No changes detected.' };
|
|
29
|
+
constructor(modelsPath, migrationsPath, snapshotPath) {
|
|
30
|
+
this._modelsPath = modelsPath;
|
|
31
|
+
this._migrationsPath = migrationsPath;
|
|
32
|
+
this._snapshotPath = snapshotPath || path.join(process.cwd(), '.millas', 'schema.json');
|
|
46
33
|
}
|
|
47
34
|
|
|
48
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Detect changes and generate migration files.
|
|
37
|
+
* Returns { files: string[], message: string }
|
|
38
|
+
*/
|
|
39
|
+
async makeMigrations() {
|
|
40
|
+
const current = this._scanModels();
|
|
41
|
+
const snapshot = this._loadSnapshot();
|
|
42
|
+
const diffs = this._diff(current, snapshot);
|
|
43
|
+
|
|
44
|
+
if (diffs.length === 0) {
|
|
45
|
+
return {files: [], message: 'No changes detected.'};
|
|
46
|
+
}
|
|
49
47
|
|
|
50
|
-
|
|
51
|
-
// together and apply as a logical group.
|
|
52
|
-
const ts = this._timestamp();
|
|
53
|
-
const files = [];
|
|
48
|
+
await fs.ensureDir(this._migrationsPath);
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
// All diffs in this run share the same timestamp prefix so they sort
|
|
51
|
+
// together and apply as a logical group.
|
|
52
|
+
const ts = this._timestamp();
|
|
53
|
+
const files = [];
|
|
54
|
+
|
|
55
|
+
for (const diff of diffs) {
|
|
56
|
+
const file = await this._generateMigration(diff, ts);
|
|
57
|
+
if (file) files.push(file);
|
|
58
|
+
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ─── Model scanning ───────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Walk app/models/ and return a plain-object schema map:
|
|
71
|
-
* { tableName: { columnName: { type, nullable, … }, … }, … }
|
|
72
|
-
*
|
|
73
|
-
* Handles both default exports (`module.exports = MyModel`) and
|
|
74
|
-
* named exports (`module.exports = { MyModel }`).
|
|
75
|
-
*/
|
|
76
|
-
_scanModels() {
|
|
77
|
-
const schema = {};
|
|
78
|
-
|
|
79
|
-
if (!fs.existsSync(this._modelsPath)) return schema;
|
|
80
|
-
|
|
81
|
-
const files = fs.readdirSync(this._modelsPath)
|
|
82
|
-
.filter(f => f.endsWith('.js') && !f.startsWith('.') && f !== 'index.js');
|
|
83
|
-
|
|
84
|
-
for (const file of files) {
|
|
85
|
-
const fullPath = path.join(this._modelsPath, file);
|
|
86
|
-
|
|
87
|
-
// Always bust require cache so the inspector picks up edits made
|
|
88
|
-
// in the same process (e.g. during tests).
|
|
89
|
-
try {
|
|
90
|
-
delete require.cache[require.resolve(fullPath)];
|
|
91
|
-
} catch { /* path not yet cached — fine */ }
|
|
92
|
-
|
|
93
|
-
let exported;
|
|
94
|
-
try {
|
|
95
|
-
exported = require(fullPath);
|
|
96
|
-
} catch (err) {
|
|
97
|
-
// Skip files that fail to parse / have runtime errors
|
|
98
|
-
// Log at WARN level — a skipped model is worth knowing about
|
|
99
|
-
// but shouldn't stop the command. Falls back silently if the
|
|
100
|
-
// logger hasn't been configured yet (e.g. bare CLI usage).
|
|
101
|
-
MillasLog.w('makemigrations', `Skipping ${file}: ${err.message}`);
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Collect every candidate class from the export
|
|
106
|
-
const candidates = this._extractClasses(exported);
|
|
107
|
-
|
|
108
|
-
for (const ModelClass of candidates) {
|
|
109
|
-
if (!this._isMillasModel(ModelClass)) continue;
|
|
110
|
-
|
|
111
|
-
const table = this._resolveTable(ModelClass, file);
|
|
112
|
-
schema[table] = this._extractFields(ModelClass.fields);
|
|
113
|
-
}
|
|
60
|
+
// Persist the new baseline — must happen AFTER generating files so
|
|
61
|
+
// a crash mid-generation doesn't advance the snapshot prematurely.
|
|
62
|
+
this._saveSnapshot(current);
|
|
63
|
+
|
|
64
|
+
return {files, message: `Generated ${files.length} migration file(s).`};
|
|
114
65
|
}
|
|
115
66
|
|
|
116
|
-
|
|
117
|
-
}
|
|
67
|
+
// ─── Model scanning ───────────────────────────────────────────────────────
|
|
118
68
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Walk app/models/ and return a plain-object schema map:
|
|
71
|
+
* { tableName: { columnName: { type, nullable, … }, … }, … }
|
|
72
|
+
*
|
|
73
|
+
* Handles both default exports (`module.exports = MyModel`) and
|
|
74
|
+
* named exports (`module.exports = { MyModel }`).
|
|
75
|
+
*/
|
|
76
|
+
_scanModels() {
|
|
77
|
+
const schema = {};
|
|
125
78
|
|
|
126
|
-
|
|
127
|
-
if (typeof exported === 'function') return [exported];
|
|
79
|
+
if (!fs.existsSync(this._modelsPath)) return schema;
|
|
128
80
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return Object.values(exported).filter(v => typeof v === 'function');
|
|
132
|
-
}
|
|
81
|
+
const files = fs.readdirSync(this._modelsPath)
|
|
82
|
+
.filter(f => f.endsWith('.js') && !f.startsWith('.') && f !== 'index.js');
|
|
133
83
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
// Convention: file name without extension, pluralised, lowercased
|
|
162
|
-
return fileName.replace(/\.js$/, '').toLowerCase() + 's';
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Convert a fields map (whose values may be FieldDefinition instances or
|
|
167
|
-
* plain objects) into a stable plain-object representation suitable for
|
|
168
|
-
* snapshot storage and deterministic JSON comparison.
|
|
169
|
-
*/
|
|
170
|
-
_extractFields(fields) {
|
|
171
|
-
const result = {};
|
|
172
|
-
|
|
173
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
174
|
-
// Normalise — accept both FieldDefinition instances and plain objects
|
|
175
|
-
result[name] = {
|
|
176
|
-
type: field.type ?? 'string',
|
|
177
|
-
nullable: field.nullable ?? false,
|
|
178
|
-
unique: field.unique ?? false,
|
|
179
|
-
default: field.default !== undefined ? field.default : null,
|
|
180
|
-
max: field.max ?? null,
|
|
181
|
-
unsigned: field.unsigned ?? false,
|
|
182
|
-
enumValues: field.enumValues ?? null,
|
|
183
|
-
references: field.references ?? null,
|
|
184
|
-
precision: field.precision ?? null,
|
|
185
|
-
scale: field.scale ?? null,
|
|
186
|
-
};
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
const fullPath = path.join(this._modelsPath, file);
|
|
86
|
+
|
|
87
|
+
// Always bust require cache so the inspector picks up edits made
|
|
88
|
+
// in the same process (e.g. during tests).
|
|
89
|
+
try {
|
|
90
|
+
delete require.cache[require.resolve(fullPath)];
|
|
91
|
+
} catch { /* path not yet cached — fine */
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let exported;
|
|
95
|
+
exported = require(fullPath);
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
// Collect every candidate class from the export
|
|
99
|
+
const candidates = this._extractClasses(exported);
|
|
100
|
+
|
|
101
|
+
for (const ModelClass of candidates) {
|
|
102
|
+
if (!this._isMillasModel(ModelClass)) continue;
|
|
103
|
+
|
|
104
|
+
const table = this._resolveTable(ModelClass, file);
|
|
105
|
+
schema[table] = this._extractFields(ModelClass.fields);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return schema;
|
|
187
110
|
}
|
|
188
111
|
|
|
189
|
-
|
|
190
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Given a module export (class, plain object, or anything), return an
|
|
114
|
+
* array of class/function values that might be Model subclasses.
|
|
115
|
+
*/
|
|
116
|
+
_extractClasses(exported) {
|
|
117
|
+
if (!exported) return [];
|
|
191
118
|
|
|
192
|
-
|
|
119
|
+
// Direct class export: module.exports = MyModel
|
|
120
|
+
if (typeof exported === 'function') return [exported];
|
|
193
121
|
|
|
194
|
-
|
|
195
|
-
|
|
122
|
+
// Named export object: module.exports = { MyModel, AnotherModel }
|
|
123
|
+
if (typeof exported === 'object') {
|
|
124
|
+
return Object.values(exported).filter(v => typeof v === 'function');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A class qualifies as a Millas Model if:
|
|
132
|
+
* - It is a function (class)
|
|
133
|
+
* - It has a static `fields` property that is a non-null object
|
|
134
|
+
*
|
|
135
|
+
* We intentionally do NOT do `instanceof` checks so the inspector
|
|
136
|
+
* works even when the user imports Model from a different resolution
|
|
137
|
+
* path than the one this file was loaded from.
|
|
138
|
+
*/
|
|
139
|
+
_isMillasModel(cls) {
|
|
140
|
+
if (typeof cls !== 'function') return false;
|
|
141
|
+
if (!cls.fields || typeof cls.fields !== 'object') return false;
|
|
142
|
+
// Must have at least one field
|
|
143
|
+
return Object.keys(cls.fields).length > 0;
|
|
144
|
+
}
|
|
196
145
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Derive the table name from the model class or fall back to the file name.
|
|
148
|
+
*/
|
|
149
|
+
_resolveTable(ModelClass, fileName) {
|
|
150
|
+
// Explicitly set static table = '...'
|
|
151
|
+
if (typeof ModelClass.table === 'string' && ModelClass.table) {
|
|
152
|
+
return ModelClass.table;
|
|
153
|
+
}
|
|
154
|
+
// Convention: file name without extension, pluralised, lowercased
|
|
155
|
+
return fileName.replace(/\.js$/, '').toLowerCase() + 's';
|
|
202
156
|
}
|
|
203
157
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Convert a fields map (whose values may be FieldDefinition instances or
|
|
160
|
+
* plain objects) into a stable plain-object representation suitable for
|
|
161
|
+
* snapshot storage and deterministic JSON comparison.
|
|
162
|
+
*/
|
|
163
|
+
_extractFields(fields) {
|
|
164
|
+
const result = {};
|
|
165
|
+
|
|
166
|
+
for (const [name, field] of Object.entries(fields)) {
|
|
167
|
+
// Normalise — accept both FieldDefinition instances and plain objects
|
|
168
|
+
result[name] = {
|
|
169
|
+
type: field.type ?? 'string',
|
|
170
|
+
nullable: field.nullable ?? false,
|
|
171
|
+
unique: field.unique ?? false,
|
|
172
|
+
default: field.default !== undefined ? field.default : null,
|
|
173
|
+
max: field.max ?? null,
|
|
174
|
+
unsigned: field.unsigned ?? false,
|
|
175
|
+
enumValues: field.enumValues ?? null,
|
|
176
|
+
references: field.references ?? null,
|
|
177
|
+
precision: field.precision ?? null,
|
|
178
|
+
scale: field.scale ?? null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return result;
|
|
209
183
|
}
|
|
210
184
|
|
|
211
|
-
//
|
|
212
|
-
for (const table of Object.keys(current)) {
|
|
213
|
-
if (!snapshot[table]) continue; // handled above as create_table
|
|
185
|
+
// ─── Diffing ──────────────────────────────────────────────────────────────
|
|
214
186
|
|
|
215
|
-
|
|
216
|
-
|
|
187
|
+
_diff(current, snapshot) {
|
|
188
|
+
const diffs = [];
|
|
217
189
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
190
|
+
// New tables (model added / first run)
|
|
191
|
+
for (const table of Object.keys(current)) {
|
|
192
|
+
if (!snapshot[table]) {
|
|
193
|
+
diffs.push({type: 'create_table', table, fields: current[table]});
|
|
194
|
+
}
|
|
222
195
|
}
|
|
223
|
-
}
|
|
224
196
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
197
|
+
// Dropped tables (model file removed)
|
|
198
|
+
for (const table of Object.keys(snapshot)) {
|
|
199
|
+
if (!current[table]) {
|
|
200
|
+
diffs.push({type: 'drop_table', table, fields: snapshot[table]});
|
|
201
|
+
}
|
|
229
202
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
203
|
+
|
|
204
|
+
// Column-level changes on existing tables
|
|
205
|
+
for (const table of Object.keys(current)) {
|
|
206
|
+
if (!snapshot[table]) continue; // handled above as create_table
|
|
207
|
+
|
|
208
|
+
const curr = current[table];
|
|
209
|
+
const prev = snapshot[table];
|
|
210
|
+
|
|
211
|
+
// Added columns
|
|
212
|
+
for (const col of Object.keys(curr)) {
|
|
213
|
+
if (!prev[col]) {
|
|
214
|
+
diffs.push({type: 'add_column', table, column: col, field: curr[col]});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Removed columns
|
|
219
|
+
for (const col of Object.keys(prev)) {
|
|
220
|
+
if (!curr[col]) {
|
|
221
|
+
diffs.push({type: 'drop_column', table, column: col, field: prev[col]});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Changed columns — compare each attribute individually for stability
|
|
226
|
+
for (const col of Object.keys(curr)) {
|
|
227
|
+
if (!prev[col]) continue; // new column — already handled above
|
|
228
|
+
if (!this._fieldsEqual(curr[col], prev[col])) {
|
|
229
|
+
diffs.push({
|
|
230
|
+
type: 'alter_column',
|
|
231
|
+
table,
|
|
232
|
+
column: col,
|
|
233
|
+
field: curr[col],
|
|
234
|
+
previous: prev[col],
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
243
238
|
}
|
|
244
|
-
|
|
239
|
+
|
|
240
|
+
return diffs;
|
|
245
241
|
}
|
|
246
242
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (JSON.stringify(a[k]) !== JSON.stringify(b[k])) return false;
|
|
243
|
+
/**
|
|
244
|
+
* Stable field equality check that ignores key-ordering differences
|
|
245
|
+
* which can appear when objects are reconstituted from JSON.
|
|
246
|
+
*/
|
|
247
|
+
_fieldsEqual(a, b) {
|
|
248
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
249
|
+
for (const k of keys) {
|
|
250
|
+
if (JSON.stringify(a[k]) !== JSON.stringify(b[k])) return false;
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
258
253
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
await fs.writeFile(filePath, content, 'utf8');
|
|
271
|
-
return fileName;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
_diffToName(diff) {
|
|
275
|
-
switch (diff.type) {
|
|
276
|
-
case 'create_table': return `create_${diff.table}_table`;
|
|
277
|
-
case 'drop_table': return `drop_${diff.table}_table`;
|
|
278
|
-
case 'add_column': return `add_${diff.column}_to_${diff.table}`;
|
|
279
|
-
case 'drop_column': return `remove_${diff.column}_from_${diff.table}`;
|
|
280
|
-
case 'alter_column': return `alter_${diff.column}_on_${diff.table}`;
|
|
281
|
-
default: return `auto_migration`;
|
|
254
|
+
|
|
255
|
+
// ─── Migration generation ─────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
async _generateMigration(diff, ts) {
|
|
258
|
+
const name = this._diffToName(diff);
|
|
259
|
+
const fileName = `${ts}_${name}.js`;
|
|
260
|
+
const filePath = path.join(this._migrationsPath, fileName);
|
|
261
|
+
|
|
262
|
+
const content = this._renderMigration(diff, name);
|
|
263
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
264
|
+
return fileName;
|
|
282
265
|
}
|
|
283
|
-
}
|
|
284
266
|
|
|
285
|
-
|
|
286
|
-
|
|
267
|
+
_diffToName(diff) {
|
|
268
|
+
switch (diff.type) {
|
|
269
|
+
case 'create_table':
|
|
270
|
+
return `create_${diff.table}_table`;
|
|
271
|
+
case 'drop_table':
|
|
272
|
+
return `drop_${diff.table}_table`;
|
|
273
|
+
case 'add_column':
|
|
274
|
+
return `add_${diff.column}_to_${diff.table}`;
|
|
275
|
+
case 'drop_column':
|
|
276
|
+
return `remove_${diff.column}_from_${diff.table}`;
|
|
277
|
+
case 'alter_column':
|
|
278
|
+
return `alter_${diff.column}_on_${diff.table}`;
|
|
279
|
+
default:
|
|
280
|
+
return `auto_migration`;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_renderMigration(diff, name) {
|
|
285
|
+
switch (diff.type) {
|
|
287
286
|
|
|
288
|
-
|
|
289
|
-
|
|
287
|
+
case 'create_table':
|
|
288
|
+
return `'use strict';
|
|
290
289
|
|
|
291
290
|
/**
|
|
292
291
|
* Auto-generated migration: ${name}
|
|
@@ -306,8 +305,8 @@ ${this._renderColumns(diff.fields)} });
|
|
|
306
305
|
};
|
|
307
306
|
`;
|
|
308
307
|
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
case 'drop_table':
|
|
309
|
+
return `'use strict';
|
|
311
310
|
|
|
312
311
|
/**
|
|
313
312
|
* Auto-generated migration: ${name}
|
|
@@ -327,8 +326,8 @@ ${this._renderColumns(diff.fields || {})} });
|
|
|
327
326
|
};
|
|
328
327
|
`;
|
|
329
328
|
|
|
330
|
-
|
|
331
|
-
|
|
329
|
+
case 'add_column':
|
|
330
|
+
return `'use strict';
|
|
332
331
|
|
|
333
332
|
/**
|
|
334
333
|
* Auto-generated migration: ${name}
|
|
@@ -349,8 +348,8 @@ ${this._renderColumn(' ', diff.column, diff.field)}
|
|
|
349
348
|
};
|
|
350
349
|
`;
|
|
351
350
|
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
case 'drop_column':
|
|
352
|
+
return `'use strict';
|
|
354
353
|
|
|
355
354
|
/**
|
|
356
355
|
* Auto-generated migration: ${name}
|
|
@@ -371,8 +370,8 @@ ${this._renderColumn(' ', diff.column, diff.field)}
|
|
|
371
370
|
};
|
|
372
371
|
`;
|
|
373
372
|
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
case 'alter_column':
|
|
374
|
+
return `'use strict';
|
|
376
375
|
|
|
377
376
|
/**
|
|
378
377
|
* Auto-generated migration: ${name}
|
|
@@ -394,121 +393,121 @@ ${this._renderColumn(' ', diff.column, diff.previous, '.alter()')}
|
|
|
394
393
|
};
|
|
395
394
|
`;
|
|
396
395
|
|
|
397
|
-
|
|
398
|
-
|
|
396
|
+
default:
|
|
397
|
+
return `'use strict';\nmodule.exports = { async up(db) {}, async down(db) {} };\n`;
|
|
398
|
+
}
|
|
399
399
|
}
|
|
400
|
-
}
|
|
401
400
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
_renderColumn(indent, name, field, suffix = '') {
|
|
412
|
-
let line;
|
|
413
|
-
|
|
414
|
-
switch (field.type) {
|
|
415
|
-
case 'id':
|
|
416
|
-
return `${indent}t.increments('${name}')${suffix};`;
|
|
417
|
-
|
|
418
|
-
case 'string':
|
|
419
|
-
line = `t.string('${name}', ${field.max || 255})`;
|
|
420
|
-
break;
|
|
421
|
-
|
|
422
|
-
case 'text':
|
|
423
|
-
line = `t.text('${name}')`;
|
|
424
|
-
break;
|
|
425
|
-
|
|
426
|
-
case 'integer':
|
|
427
|
-
line = field.unsigned
|
|
428
|
-
? `t.integer('${name}').unsigned()`
|
|
429
|
-
: `t.integer('${name}')`;
|
|
430
|
-
break;
|
|
431
|
-
|
|
432
|
-
case 'bigInteger':
|
|
433
|
-
line = field.unsigned
|
|
434
|
-
? `t.bigInteger('${name}').unsigned()`
|
|
435
|
-
: `t.bigInteger('${name}')`;
|
|
436
|
-
break;
|
|
437
|
-
|
|
438
|
-
case 'float':
|
|
439
|
-
line = `t.float('${name}')`;
|
|
440
|
-
break;
|
|
441
|
-
|
|
442
|
-
case 'decimal':
|
|
443
|
-
line = `t.decimal('${name}', ${field.precision || 8}, ${field.scale || 2})`;
|
|
444
|
-
break;
|
|
445
|
-
|
|
446
|
-
case 'boolean':
|
|
447
|
-
line = `t.boolean('${name}')`;
|
|
448
|
-
break;
|
|
449
|
-
|
|
450
|
-
case 'json':
|
|
451
|
-
line = `t.json('${name}')`;
|
|
452
|
-
break;
|
|
453
|
-
|
|
454
|
-
case 'date':
|
|
455
|
-
line = `t.date('${name}')`;
|
|
456
|
-
break;
|
|
457
|
-
|
|
458
|
-
case 'timestamp':
|
|
459
|
-
line = `t.timestamp('${name}', { useTz: false })`;
|
|
460
|
-
break;
|
|
461
|
-
|
|
462
|
-
case 'enum':
|
|
463
|
-
line = `t.enum('${name}', ${JSON.stringify(field.enumValues || [])})`;
|
|
464
|
-
break;
|
|
465
|
-
|
|
466
|
-
case 'uuid':
|
|
467
|
-
line = `t.uuid('${name}')`;
|
|
468
|
-
break;
|
|
469
|
-
|
|
470
|
-
default:
|
|
471
|
-
line = `t.string('${name}')`;
|
|
401
|
+
_renderColumns(fields) {
|
|
402
|
+
if (!fields || Object.keys(fields).length === 0) {
|
|
403
|
+
return ' t.increments(\'id\');\n t.timestamps();\n';
|
|
404
|
+
}
|
|
405
|
+
return Object.entries(fields)
|
|
406
|
+
.map(([name, field]) => this._renderColumn(' ', name, field))
|
|
407
|
+
.join('\n') + '\n';
|
|
472
408
|
}
|
|
473
409
|
|
|
474
|
-
|
|
475
|
-
|
|
410
|
+
_renderColumn(indent, name, field, suffix = '') {
|
|
411
|
+
let line;
|
|
476
412
|
|
|
477
|
-
|
|
413
|
+
switch (field.type) {
|
|
414
|
+
case 'id':
|
|
415
|
+
return `${indent}t.increments('${name}')${suffix};`;
|
|
478
416
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
417
|
+
case 'string':
|
|
418
|
+
line = `t.string('${name}', ${field.max || 255})`;
|
|
419
|
+
break;
|
|
482
420
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
421
|
+
case 'text':
|
|
422
|
+
line = `t.text('${name}')`;
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case 'integer':
|
|
426
|
+
line = field.unsigned
|
|
427
|
+
? `t.integer('${name}').unsigned()`
|
|
428
|
+
: `t.integer('${name}')`;
|
|
429
|
+
break;
|
|
430
|
+
|
|
431
|
+
case 'bigInteger':
|
|
432
|
+
line = field.unsigned
|
|
433
|
+
? `t.bigInteger('${name}').unsigned()`
|
|
434
|
+
: `t.bigInteger('${name}')`;
|
|
435
|
+
break;
|
|
436
|
+
|
|
437
|
+
case 'float':
|
|
438
|
+
line = `t.float('${name}')`;
|
|
439
|
+
break;
|
|
440
|
+
|
|
441
|
+
case 'decimal':
|
|
442
|
+
line = `t.decimal('${name}', ${field.precision || 8}, ${field.scale || 2})`;
|
|
443
|
+
break;
|
|
444
|
+
|
|
445
|
+
case 'boolean':
|
|
446
|
+
line = `t.boolean('${name}')`;
|
|
447
|
+
break;
|
|
448
|
+
|
|
449
|
+
case 'json':
|
|
450
|
+
line = `t.json('${name}')`;
|
|
451
|
+
break;
|
|
452
|
+
|
|
453
|
+
case 'date':
|
|
454
|
+
line = `t.date('${name}')`;
|
|
455
|
+
break;
|
|
456
|
+
|
|
457
|
+
case 'timestamp':
|
|
458
|
+
line = `t.timestamp('${name}', { useTz: false })`;
|
|
459
|
+
break;
|
|
488
460
|
|
|
489
|
-
|
|
490
|
-
|
|
461
|
+
case 'enum':
|
|
462
|
+
line = `t.enum('${name}', ${JSON.stringify(field.enumValues || [])})`;
|
|
463
|
+
break;
|
|
491
464
|
|
|
492
|
-
|
|
465
|
+
case 'uuid':
|
|
466
|
+
line = `t.uuid('${name}')`;
|
|
467
|
+
break;
|
|
493
468
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
469
|
+
default:
|
|
470
|
+
line = `t.string('${name}')`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (field.nullable) line += '.nullable()';
|
|
474
|
+
else if (field.type !== 'id') line += '.notNullable()';
|
|
475
|
+
|
|
476
|
+
if (field.unique) line += '.unique()';
|
|
477
|
+
|
|
478
|
+
if (field.default !== null && field.default !== undefined) {
|
|
479
|
+
line += `.defaultTo(${JSON.stringify(field.default)})`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (field.references) {
|
|
483
|
+
line += `.references('${field.references.column}')` +
|
|
484
|
+
`.inTable('${field.references.table}')` +
|
|
485
|
+
`.onDelete('CASCADE')`;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return `${indent}${line}${suffix};`;
|
|
499
489
|
}
|
|
500
|
-
}
|
|
501
490
|
|
|
502
|
-
|
|
503
|
-
fs.ensureDirSync(path.dirname(this._snapshotPath));
|
|
504
|
-
fs.writeJsonSync(this._snapshotPath, schema, { spaces: 2 });
|
|
505
|
-
}
|
|
491
|
+
// ─── Snapshot ─────────────────────────────────────────────────────────────
|
|
506
492
|
|
|
507
|
-
|
|
493
|
+
_loadSnapshot() {
|
|
494
|
+
try {
|
|
495
|
+
return fs.readJsonSync(this._snapshotPath);
|
|
496
|
+
} catch {
|
|
497
|
+
return {};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
508
500
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
501
|
+
_saveSnapshot(schema) {
|
|
502
|
+
fs.ensureDirSync(path.dirname(this._snapshotPath));
|
|
503
|
+
fs.writeJsonSync(this._snapshotPath, schema, {spaces: 2});
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
_timestamp() {
|
|
509
|
+
return new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14);
|
|
510
|
+
}
|
|
512
511
|
}
|
|
513
512
|
|
|
514
513
|
module.exports = ModelInspector;
|