millas 0.2.11 → 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 +6 -5
- package/src/auth/Auth.js +13 -8
- package/src/auth/AuthController.js +45 -134
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/AuthUser.js +98 -0
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/cli.js +1 -1
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +238 -38
- package/src/container/AppInitializer.js +158 -0
- package/src/container/Application.js +288 -183
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +23 -280
- package/src/container/MillasConfig.js +163 -0
- package/src/controller/Controller.js +79 -300
- 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/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +29 -0
- package/src/facades/Cache.js +28 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +25 -0
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +51 -0
- package/src/facades/Log.js +32 -0
- package/src/facades/Mail.js +35 -0
- package/src/facades/Queue.js +30 -0
- package/src/facades/Storage.js +25 -0
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/MillasRequest.js +253 -0
- package/src/http/MillasResponse.js +196 -0
- package/src/http/RequestContext.js +176 -0
- package/src/http/ResponseDispatcher.js +51 -0
- 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/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +5 -91
- package/src/logger/formatters/PrettyFormatter.js +15 -5
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +145 -0
- package/src/middleware/CorsMiddleware.js +22 -30
- package/src/middleware/LogMiddleware.js +27 -59
- package/src/middleware/Middleware.js +24 -15
- package/src/middleware/MiddlewarePipeline.js +30 -67
- package/src/middleware/MiddlewareRegistry.js +106 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +339 -336
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- 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 +88 -17
- package/src/providers/MailServiceProvider.js +3 -2
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/QueueServiceProvider.js +3 -2
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +121 -222
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +21 -19
- package/src/validation/BaseValidator.js +193 -0
- package/src/validation/Validator.js +680 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const fs
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const MillasLog = require('../../logger/internal');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* ModelInspector
|
|
@@ -25,264 +26,266 @@ const path = require('path');
|
|
|
25
26
|
* Developers only touch model files — never migration files directly.
|
|
26
27
|
*/
|
|
27
28
|
class ModelInspector {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Detect changes and generate migration files.
|
|
36
|
-
* Returns { files: string[], message: string }
|
|
37
|
-
*/
|
|
38
|
-
async makeMigrations() {
|
|
39
|
-
const current = this._scanModels();
|
|
40
|
-
const snapshot = this._loadSnapshot();
|
|
41
|
-
const diffs = this._diff(current, snapshot);
|
|
42
|
-
|
|
43
|
-
if (diffs.length === 0) {
|
|
44
|
-
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');
|
|
45
33
|
}
|
|
46
34
|
|
|
47
|
-
|
|
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
|
+
}
|
|
48
47
|
|
|
49
|
-
|
|
50
|
-
// together and apply as a logical group.
|
|
51
|
-
const ts = this._timestamp();
|
|
52
|
-
const files = [];
|
|
48
|
+
await fs.ensureDir(this._migrationsPath);
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
}
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ─── Model scanning ───────────────────────────────────────────────────────
|
|
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
|
-
*/
|
|
75
|
-
_scanModels() {
|
|
76
|
-
const schema = {};
|
|
77
|
-
|
|
78
|
-
if (!fs.existsSync(this._modelsPath)) return schema;
|
|
79
|
-
|
|
80
|
-
const files = fs.readdirSync(this._modelsPath)
|
|
81
|
-
.filter(f => f.endsWith('.js') && !f.startsWith('.') && f !== 'index.js');
|
|
82
|
-
|
|
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).
|
|
88
|
-
try {
|
|
89
|
-
delete require.cache[require.resolve(fullPath)];
|
|
90
|
-
} catch { /* path not yet cached — fine */ }
|
|
91
|
-
|
|
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);
|
|
108
|
-
schema[table] = this._extractFields(ModelClass.fields);
|
|
109
|
-
}
|
|
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).`};
|
|
110
65
|
}
|
|
111
66
|
|
|
112
|
-
|
|
113
|
-
}
|
|
67
|
+
// ─── Model scanning ───────────────────────────────────────────────────────
|
|
114
68
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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 = {};
|
|
121
78
|
|
|
122
|
-
|
|
123
|
-
if (typeof exported === 'function') return [exported];
|
|
79
|
+
if (!fs.existsSync(this._modelsPath)) return schema;
|
|
124
80
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return Object.values(exported).filter(v => typeof v === 'function');
|
|
128
|
-
}
|
|
81
|
+
const files = fs.readdirSync(this._modelsPath)
|
|
82
|
+
.filter(f => f.endsWith('.js') && !f.startsWith('.') && f !== 'index.js');
|
|
129
83
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
*/
|
|
166
|
-
_extractFields(fields) {
|
|
167
|
-
const result = {};
|
|
168
|
-
|
|
169
|
-
for (const [name, field] of Object.entries(fields)) {
|
|
170
|
-
// Normalise — accept both FieldDefinition instances and plain objects
|
|
171
|
-
result[name] = {
|
|
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,
|
|
182
|
-
};
|
|
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;
|
|
183
110
|
}
|
|
184
111
|
|
|
185
|
-
|
|
186
|
-
|
|
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 [];
|
|
187
118
|
|
|
188
|
-
|
|
119
|
+
// Direct class export: module.exports = MyModel
|
|
120
|
+
if (typeof exported === 'function') return [exported];
|
|
189
121
|
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
}
|
|
192
145
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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';
|
|
198
156
|
}
|
|
199
157
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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;
|
|
205
183
|
}
|
|
206
184
|
|
|
207
|
-
//
|
|
208
|
-
for (const table of Object.keys(current)) {
|
|
209
|
-
if (!snapshot[table]) continue; // handled above as create_table
|
|
185
|
+
// ─── Diffing ──────────────────────────────────────────────────────────────
|
|
210
186
|
|
|
211
|
-
|
|
212
|
-
|
|
187
|
+
_diff(current, snapshot) {
|
|
188
|
+
const diffs = [];
|
|
213
189
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
218
195
|
}
|
|
219
|
-
}
|
|
220
196
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
}
|
|
225
202
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
}
|
|
239
238
|
}
|
|
240
|
-
|
|
239
|
+
|
|
240
|
+
return diffs;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
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;
|
|
254
253
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
await fs.writeFile(filePath, content, 'utf8');
|
|
267
|
-
return fileName;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
_diffToName(diff) {
|
|
271
|
-
switch (diff.type) {
|
|
272
|
-
case 'create_table': return `create_${diff.table}_table`;
|
|
273
|
-
case 'drop_table': return `drop_${diff.table}_table`;
|
|
274
|
-
case 'add_column': return `add_${diff.column}_to_${diff.table}`;
|
|
275
|
-
case 'drop_column': return `remove_${diff.column}_from_${diff.table}`;
|
|
276
|
-
case 'alter_column': return `alter_${diff.column}_on_${diff.table}`;
|
|
277
|
-
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;
|
|
278
265
|
}
|
|
279
|
-
}
|
|
280
266
|
|
|
281
|
-
|
|
282
|
-
|
|
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) {
|
|
283
286
|
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
case 'create_table':
|
|
288
|
+
return `'use strict';
|
|
286
289
|
|
|
287
290
|
/**
|
|
288
291
|
* Auto-generated migration: ${name}
|
|
@@ -302,8 +305,8 @@ ${this._renderColumns(diff.fields)} });
|
|
|
302
305
|
};
|
|
303
306
|
`;
|
|
304
307
|
|
|
305
|
-
|
|
306
|
-
|
|
308
|
+
case 'drop_table':
|
|
309
|
+
return `'use strict';
|
|
307
310
|
|
|
308
311
|
/**
|
|
309
312
|
* Auto-generated migration: ${name}
|
|
@@ -323,8 +326,8 @@ ${this._renderColumns(diff.fields || {})} });
|
|
|
323
326
|
};
|
|
324
327
|
`;
|
|
325
328
|
|
|
326
|
-
|
|
327
|
-
|
|
329
|
+
case 'add_column':
|
|
330
|
+
return `'use strict';
|
|
328
331
|
|
|
329
332
|
/**
|
|
330
333
|
* Auto-generated migration: ${name}
|
|
@@ -345,8 +348,8 @@ ${this._renderColumn(' ', diff.column, diff.field)}
|
|
|
345
348
|
};
|
|
346
349
|
`;
|
|
347
350
|
|
|
348
|
-
|
|
349
|
-
|
|
351
|
+
case 'drop_column':
|
|
352
|
+
return `'use strict';
|
|
350
353
|
|
|
351
354
|
/**
|
|
352
355
|
* Auto-generated migration: ${name}
|
|
@@ -367,8 +370,8 @@ ${this._renderColumn(' ', diff.column, diff.field)}
|
|
|
367
370
|
};
|
|
368
371
|
`;
|
|
369
372
|
|
|
370
|
-
|
|
371
|
-
|
|
373
|
+
case 'alter_column':
|
|
374
|
+
return `'use strict';
|
|
372
375
|
|
|
373
376
|
/**
|
|
374
377
|
* Auto-generated migration: ${name}
|
|
@@ -390,121 +393,121 @@ ${this._renderColumn(' ', diff.column, diff.previous, '.alter()')}
|
|
|
390
393
|
};
|
|
391
394
|
`;
|
|
392
395
|
|
|
393
|
-
|
|
394
|
-
|
|
396
|
+
default:
|
|
397
|
+
return `'use strict';\nmodule.exports = { async up(db) {}, async down(db) {} };\n`;
|
|
398
|
+
}
|
|
395
399
|
}
|
|
396
|
-
}
|
|
397
400
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
_renderColumn(indent, name, field, suffix = '') {
|
|
408
|
-
let line;
|
|
409
|
-
|
|
410
|
-
switch (field.type) {
|
|
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}')`;
|
|
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';
|
|
468
408
|
}
|
|
469
409
|
|
|
470
|
-
|
|
471
|
-
|
|
410
|
+
_renderColumn(indent, name, field, suffix = '') {
|
|
411
|
+
let line;
|
|
472
412
|
|
|
473
|
-
|
|
413
|
+
switch (field.type) {
|
|
414
|
+
case 'id':
|
|
415
|
+
return `${indent}t.increments('${name}')${suffix};`;
|
|
474
416
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
417
|
+
case 'string':
|
|
418
|
+
line = `t.string('${name}', ${field.max || 255})`;
|
|
419
|
+
break;
|
|
478
420
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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;
|
|
484
460
|
|
|
485
|
-
|
|
486
|
-
|
|
461
|
+
case 'enum':
|
|
462
|
+
line = `t.enum('${name}', ${JSON.stringify(field.enumValues || [])})`;
|
|
463
|
+
break;
|
|
487
464
|
|
|
488
|
-
|
|
465
|
+
case 'uuid':
|
|
466
|
+
line = `t.uuid('${name}')`;
|
|
467
|
+
break;
|
|
489
468
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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};`;
|
|
495
489
|
}
|
|
496
|
-
}
|
|
497
490
|
|
|
498
|
-
|
|
499
|
-
fs.ensureDirSync(path.dirname(this._snapshotPath));
|
|
500
|
-
fs.writeJsonSync(this._snapshotPath, schema, { spaces: 2 });
|
|
501
|
-
}
|
|
491
|
+
// ─── Snapshot ─────────────────────────────────────────────────────────────
|
|
502
492
|
|
|
503
|
-
|
|
493
|
+
_loadSnapshot() {
|
|
494
|
+
try {
|
|
495
|
+
return fs.readJsonSync(this._snapshotPath);
|
|
496
|
+
} catch {
|
|
497
|
+
return {};
|
|
498
|
+
}
|
|
499
|
+
}
|
|
504
500
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
+
}
|
|
508
511
|
}
|
|
509
512
|
|
|
510
513
|
module.exports = ModelInspector;
|