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.
Files changed (52) hide show
  1. package/package.json +3 -16
  2. package/src/auth/Auth.js +13 -8
  3. package/src/auth/AuthController.js +3 -1
  4. package/src/auth/AuthUser.js +98 -0
  5. package/src/cli.js +1 -1
  6. package/src/commands/serve.js +81 -110
  7. package/src/container/AppInitializer.js +158 -0
  8. package/src/container/Application.js +278 -253
  9. package/src/container/HttpServer.js +156 -0
  10. package/src/container/MillasApp.js +23 -280
  11. package/src/container/MillasConfig.js +163 -0
  12. package/src/core/auth.js +9 -0
  13. package/src/core/db.js +8 -0
  14. package/src/core/foundation.js +67 -0
  15. package/src/core/http.js +11 -0
  16. package/src/core/mail.js +6 -0
  17. package/src/core/queue.js +7 -0
  18. package/src/core/validation.js +29 -0
  19. package/src/facades/Admin.js +1 -1
  20. package/src/facades/Auth.js +22 -39
  21. package/src/facades/Cache.js +21 -10
  22. package/src/facades/Database.js +1 -1
  23. package/src/facades/Events.js +18 -17
  24. package/src/facades/Facade.js +197 -0
  25. package/src/facades/Http.js +42 -45
  26. package/src/facades/Log.js +25 -49
  27. package/src/facades/Mail.js +27 -32
  28. package/src/facades/Queue.js +22 -15
  29. package/src/facades/Storage.js +18 -10
  30. package/src/facades/Url.js +53 -0
  31. package/src/http/HttpClient.js +673 -0
  32. package/src/http/ResponseDispatcher.js +18 -111
  33. package/src/http/UrlGenerator.js +375 -0
  34. package/src/http/WelcomePage.js +273 -0
  35. package/src/http/adapters/ExpressAdapter.js +315 -0
  36. package/src/http/adapters/HttpAdapter.js +168 -0
  37. package/src/http/adapters/index.js +9 -0
  38. package/src/index.js +5 -144
  39. package/src/logger/formatters/PrettyFormatter.js +15 -5
  40. package/src/logger/internal.js +2 -2
  41. package/src/logger/patchConsole.js +91 -81
  42. package/src/middleware/MiddlewareRegistry.js +62 -82
  43. package/src/orm/migration/ModelInspector.js +339 -340
  44. package/src/providers/AuthServiceProvider.js +9 -5
  45. package/src/providers/CacheStorageServiceProvider.js +3 -1
  46. package/src/providers/EventServiceProvider.js +2 -1
  47. package/src/providers/LogServiceProvider.js +3 -2
  48. package/src/providers/MailServiceProvider.js +3 -2
  49. package/src/providers/QueueServiceProvider.js +3 -2
  50. package/src/router/Router.js +119 -152
  51. package/src/scaffold/templates.js +8 -7
  52. package/src/facades/Validation.js +0 -69
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs-extra');
4
- const path = require('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
- constructor(modelsPath, migrationsPath, snapshotPath) {
30
- this._modelsPath = modelsPath;
31
- this._migrationsPath = migrationsPath;
32
- this._snapshotPath = snapshotPath || path.join(process.cwd(), '.millas', 'schema.json');
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
- await fs.ensureDir(this._migrationsPath);
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
- // 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 = [];
48
+ await fs.ensureDir(this._migrationsPath);
54
49
 
55
- for (const diff of diffs) {
56
- const file = await this._generateMigration(diff, ts);
57
- if (file) files.push(file);
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
- // 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).` };
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
- return schema;
117
- }
67
+ // ─── Model scanning ───────────────────────────────────────────────────────
118
68
 
119
- /**
120
- * Given a module export (class, plain object, or anything), return an
121
- * array of class/function values that might be Model subclasses.
122
- */
123
- _extractClasses(exported) {
124
- if (!exported) return [];
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
- // Direct class export: module.exports = MyModel
127
- if (typeof exported === 'function') return [exported];
79
+ if (!fs.existsSync(this._modelsPath)) return schema;
128
80
 
129
- // Named export object: module.exports = { MyModel, AnotherModel }
130
- if (typeof exported === 'object') {
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
- return [];
135
- }
136
-
137
- /**
138
- * A class qualifies as a Millas Model if:
139
- * - It is a function (class)
140
- * - It has a static `fields` property that is a non-null object
141
- *
142
- * We intentionally do NOT do `instanceof` checks so the inspector
143
- * works even when the user imports Model from a different resolution
144
- * path than the one this file was loaded from.
145
- */
146
- _isMillasModel(cls) {
147
- if (typeof cls !== 'function') return false;
148
- if (!cls.fields || typeof cls.fields !== 'object') return false;
149
- // Must have at least one field
150
- return Object.keys(cls.fields).length > 0;
151
- }
152
-
153
- /**
154
- * Derive the table name from the model class or fall back to the file name.
155
- */
156
- _resolveTable(ModelClass, fileName) {
157
- // Explicitly set static table = '...'
158
- if (typeof ModelClass.table === 'string' && ModelClass.table) {
159
- return ModelClass.table;
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
- return result;
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
- // ─── Diffing ──────────────────────────────────────────────────────────────
119
+ // Direct class export: module.exports = MyModel
120
+ if (typeof exported === 'function') return [exported];
193
121
 
194
- _diff(current, snapshot) {
195
- const diffs = [];
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
- // New tables (model added / first run)
198
- for (const table of Object.keys(current)) {
199
- if (!snapshot[table]) {
200
- diffs.push({ type: 'create_table', table, fields: current[table] });
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
- // Dropped tables (model file removed)
205
- for (const table of Object.keys(snapshot)) {
206
- if (!current[table]) {
207
- diffs.push({ type: 'drop_table', table, fields: snapshot[table] });
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
- // Column-level changes on existing tables
212
- for (const table of Object.keys(current)) {
213
- if (!snapshot[table]) continue; // handled above as create_table
185
+ // ─── Diffing ──────────────────────────────────────────────────────────────
214
186
 
215
- const curr = current[table];
216
- const prev = snapshot[table];
187
+ _diff(current, snapshot) {
188
+ const diffs = [];
217
189
 
218
- // Added columns
219
- for (const col of Object.keys(curr)) {
220
- if (!prev[col]) {
221
- diffs.push({ type: 'add_column', table, column: col, field: curr[col] });
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
- // Removed columns
226
- for (const col of Object.keys(prev)) {
227
- if (!curr[col]) {
228
- diffs.push({ type: 'drop_column', table, column: col, field: prev[col] });
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
- // Changed columns compare each attribute individually for stability
233
- for (const col of Object.keys(curr)) {
234
- if (!prev[col]) continue; // new column — already handled above
235
- if (!this._fieldsEqual(curr[col], prev[col])) {
236
- diffs.push({
237
- type: 'alter_column',
238
- table,
239
- column: col,
240
- field: curr[col],
241
- previous: prev[col],
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
- return diffs;
248
- }
249
-
250
- /**
251
- * Stable field equality check that ignores key-ordering differences
252
- * which can appear when objects are reconstituted from JSON.
253
- */
254
- _fieldsEqual(a, b) {
255
- const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
256
- for (const k of keys) {
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
- return true;
260
- }
261
-
262
- // ─── Migration generation ─────────────────────────────────────────────────
263
-
264
- async _generateMigration(diff, ts) {
265
- const name = this._diffToName(diff);
266
- const fileName = `${ts}_${name}.js`;
267
- const filePath = path.join(this._migrationsPath, fileName);
268
-
269
- const content = this._renderMigration(diff, name);
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
- _renderMigration(diff, name) {
286
- switch (diff.type) {
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
- case 'create_table':
289
- return `'use strict';
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
- case 'drop_table':
310
- return `'use strict';
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
- case 'add_column':
331
- return `'use strict';
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
- case 'drop_column':
353
- return `'use strict';
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
- case 'alter_column':
375
- return `'use strict';
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
- default:
398
- return `'use strict';\nmodule.exports = { async up(db) {}, async down(db) {} };\n`;
396
+ default:
397
+ return `'use strict';\nmodule.exports = { async up(db) {}, async down(db) {} };\n`;
398
+ }
399
399
  }
400
- }
401
400
 
402
- _renderColumns(fields) {
403
- if (!fields || Object.keys(fields).length === 0) {
404
- return ' t.increments(\'id\');\n t.timestamps();\n';
405
- }
406
- return Object.entries(fields)
407
- .map(([name, field]) => this._renderColumn(' ', name, field))
408
- .join('\n') + '\n';
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
- if (field.nullable) line += '.nullable()';
475
- else if (field.type !== 'id') line += '.notNullable()';
410
+ _renderColumn(indent, name, field, suffix = '') {
411
+ let line;
476
412
 
477
- if (field.unique) line += '.unique()';
413
+ switch (field.type) {
414
+ case 'id':
415
+ return `${indent}t.increments('${name}')${suffix};`;
478
416
 
479
- if (field.default !== null && field.default !== undefined) {
480
- line += `.defaultTo(${JSON.stringify(field.default)})`;
481
- }
417
+ case 'string':
418
+ line = `t.string('${name}', ${field.max || 255})`;
419
+ break;
482
420
 
483
- if (field.references) {
484
- line += `.references('${field.references.column}')` +
485
- `.inTable('${field.references.table}')` +
486
- `.onDelete('CASCADE')`;
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
- return `${indent}${line}${suffix};`;
490
- }
461
+ case 'enum':
462
+ line = `t.enum('${name}', ${JSON.stringify(field.enumValues || [])})`;
463
+ break;
491
464
 
492
- // ─── Snapshot ─────────────────────────────────────────────────────────────
465
+ case 'uuid':
466
+ line = `t.uuid('${name}')`;
467
+ break;
493
468
 
494
- _loadSnapshot() {
495
- try {
496
- return fs.readJsonSync(this._snapshotPath);
497
- } catch {
498
- return {};
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
- _saveSnapshot(schema) {
503
- fs.ensureDirSync(path.dirname(this._snapshotPath));
504
- fs.writeJsonSync(this._snapshotPath, schema, { spaces: 2 });
505
- }
491
+ // ─── Snapshot ─────────────────────────────────────────────────────────────
506
492
 
507
- // ─── Helpers ──────────────────────────────────────────────────────────────
493
+ _loadSnapshot() {
494
+ try {
495
+ return fs.readJsonSync(this._snapshotPath);
496
+ } catch {
497
+ return {};
498
+ }
499
+ }
508
500
 
509
- _timestamp() {
510
- return new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14);
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;