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.
Files changed (74) hide show
  1. package/package.json +6 -5
  2. package/src/auth/Auth.js +13 -8
  3. package/src/auth/AuthController.js +45 -134
  4. package/src/auth/AuthMiddleware.js +12 -23
  5. package/src/auth/AuthUser.js +98 -0
  6. package/src/auth/RoleMiddleware.js +7 -17
  7. package/src/cli.js +1 -1
  8. package/src/commands/migrate.js +46 -31
  9. package/src/commands/serve.js +238 -38
  10. package/src/container/AppInitializer.js +158 -0
  11. package/src/container/Application.js +288 -183
  12. package/src/container/HttpServer.js +156 -0
  13. package/src/container/MillasApp.js +23 -280
  14. package/src/container/MillasConfig.js +163 -0
  15. package/src/controller/Controller.js +79 -300
  16. package/src/core/auth.js +9 -0
  17. package/src/core/db.js +8 -0
  18. package/src/core/foundation.js +67 -0
  19. package/src/core/http.js +11 -0
  20. package/src/core/mail.js +6 -0
  21. package/src/core/queue.js +7 -0
  22. package/src/core/validation.js +29 -0
  23. package/src/errors/ErrorRenderer.js +640 -0
  24. package/src/facades/Admin.js +49 -0
  25. package/src/facades/Auth.js +29 -0
  26. package/src/facades/Cache.js +28 -0
  27. package/src/facades/Database.js +43 -0
  28. package/src/facades/Events.js +25 -0
  29. package/src/facades/Facade.js +197 -0
  30. package/src/facades/Http.js +51 -0
  31. package/src/facades/Log.js +32 -0
  32. package/src/facades/Mail.js +35 -0
  33. package/src/facades/Queue.js +30 -0
  34. package/src/facades/Storage.js +25 -0
  35. package/src/facades/Url.js +53 -0
  36. package/src/http/HttpClient.js +673 -0
  37. package/src/http/MillasRequest.js +253 -0
  38. package/src/http/MillasResponse.js +196 -0
  39. package/src/http/RequestContext.js +176 -0
  40. package/src/http/ResponseDispatcher.js +51 -0
  41. package/src/http/UrlGenerator.js +375 -0
  42. package/src/http/WelcomePage.js +273 -0
  43. package/src/http/adapters/ExpressAdapter.js +315 -0
  44. package/src/http/adapters/HttpAdapter.js +168 -0
  45. package/src/http/adapters/index.js +9 -0
  46. package/src/http/helpers.js +164 -0
  47. package/src/http/index.js +13 -0
  48. package/src/index.js +5 -91
  49. package/src/logger/formatters/PrettyFormatter.js +15 -5
  50. package/src/logger/internal.js +76 -0
  51. package/src/logger/patchConsole.js +145 -0
  52. package/src/middleware/CorsMiddleware.js +22 -30
  53. package/src/middleware/LogMiddleware.js +27 -59
  54. package/src/middleware/Middleware.js +24 -15
  55. package/src/middleware/MiddlewarePipeline.js +30 -67
  56. package/src/middleware/MiddlewareRegistry.js +106 -0
  57. package/src/middleware/ThrottleMiddleware.js +22 -26
  58. package/src/orm/fields/index.js +124 -56
  59. package/src/orm/migration/ModelInspector.js +339 -336
  60. package/src/orm/model/Model.js +96 -6
  61. package/src/orm/query/QueryBuilder.js +141 -3
  62. package/src/providers/AuthServiceProvider.js +9 -5
  63. package/src/providers/CacheStorageServiceProvider.js +3 -1
  64. package/src/providers/EventServiceProvider.js +2 -1
  65. package/src/providers/LogServiceProvider.js +88 -17
  66. package/src/providers/MailServiceProvider.js +3 -2
  67. package/src/providers/ProviderRegistry.js +14 -1
  68. package/src/providers/QueueServiceProvider.js +3 -2
  69. package/src/providers/ServiceProvider.js +40 -8
  70. package/src/router/Router.js +121 -222
  71. package/src/scaffold/maker.js +24 -59
  72. package/src/scaffold/templates.js +21 -19
  73. package/src/validation/BaseValidator.js +193 -0
  74. package/src/validation/Validator.js +680 -0
@@ -1,7 +1,8 @@
1
1
  'use strict';
2
2
 
3
- const fs = require('fs-extra');
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
- constructor(modelsPath, migrationsPath, snapshotPath) {
29
- this._modelsPath = modelsPath;
30
- this._migrationsPath = migrationsPath;
31
- this._snapshotPath = snapshotPath || path.join(process.cwd(), '.millas', 'schema.json');
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
- 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
+ }
48
47
 
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();
52
- const files = [];
48
+ await fs.ensureDir(this._migrationsPath);
53
49
 
54
- for (const diff of diffs) {
55
- const file = await this._generateMigration(diff, ts);
56
- if (file) files.push(file);
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
- // Persist the new baseline — must happen AFTER generating files so
60
- // a crash mid-generation doesn't advance the snapshot prematurely.
61
- this._saveSnapshot(current);
62
-
63
- return { files, message: `Generated ${files.length} migration file(s).` };
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
- return schema;
113
- }
67
+ // ─── Model scanning ───────────────────────────────────────────────────────
114
68
 
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 [];
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
- // Direct class export: module.exports = MyModel
123
- if (typeof exported === 'function') return [exported];
79
+ if (!fs.existsSync(this._modelsPath)) return schema;
124
80
 
125
- // Named export object: module.exports = { MyModel, AnotherModel }
126
- if (typeof exported === 'object') {
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
- 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
- */
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
- return result;
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
- // ─── Diffing ──────────────────────────────────────────────────────────────
119
+ // Direct class export: module.exports = MyModel
120
+ if (typeof exported === 'function') return [exported];
189
121
 
190
- _diff(current, snapshot) {
191
- 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
+ }
192
145
 
193
- // New tables (model added / first run)
194
- for (const table of Object.keys(current)) {
195
- if (!snapshot[table]) {
196
- diffs.push({ type: 'create_table', table, fields: current[table] });
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
- // Dropped tables (model file removed)
201
- for (const table of Object.keys(snapshot)) {
202
- if (!current[table]) {
203
- diffs.push({ type: 'drop_table', table, fields: snapshot[table] });
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
- // Column-level changes on existing tables
208
- for (const table of Object.keys(current)) {
209
- if (!snapshot[table]) continue; // handled above as create_table
185
+ // ─── Diffing ──────────────────────────────────────────────────────────────
210
186
 
211
- const curr = current[table];
212
- const prev = snapshot[table];
187
+ _diff(current, snapshot) {
188
+ const diffs = [];
213
189
 
214
- // Added columns
215
- for (const col of Object.keys(curr)) {
216
- if (!prev[col]) {
217
- 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
+ }
218
195
  }
219
- }
220
196
 
221
- // Removed columns
222
- for (const col of Object.keys(prev)) {
223
- if (!curr[col]) {
224
- 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
+ }
225
202
  }
226
- }
227
-
228
- // Changed columns compare each attribute individually for stability
229
- for (const col of Object.keys(curr)) {
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
- });
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
- return diffs;
244
- }
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;
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
- return true;
256
- }
257
-
258
- // ─── Migration generation ─────────────────────────────────────────────────
259
-
260
- async _generateMigration(diff, ts) {
261
- const name = this._diffToName(diff);
262
- const fileName = `${ts}_${name}.js`;
263
- const filePath = path.join(this._migrationsPath, fileName);
264
-
265
- const content = this._renderMigration(diff, name);
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
- _renderMigration(diff, name) {
282
- 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) {
283
286
 
284
- case 'create_table':
285
- return `'use strict';
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
- case 'drop_table':
306
- return `'use strict';
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
- case 'add_column':
327
- return `'use strict';
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
- case 'drop_column':
349
- return `'use strict';
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
- case 'alter_column':
371
- return `'use strict';
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
- default:
394
- 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
+ }
395
399
  }
396
- }
397
400
 
398
- _renderColumns(fields) {
399
- if (!fields || Object.keys(fields).length === 0) {
400
- return ' t.increments(\'id\');\n t.timestamps();\n';
401
- }
402
- return Object.entries(fields)
403
- .map(([name, field]) => this._renderColumn(' ', name, field))
404
- .join('\n') + '\n';
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
- if (field.nullable) line += '.nullable()';
471
- else if (field.type !== 'id') line += '.notNullable()';
410
+ _renderColumn(indent, name, field, suffix = '') {
411
+ let line;
472
412
 
473
- if (field.unique) line += '.unique()';
413
+ switch (field.type) {
414
+ case 'id':
415
+ return `${indent}t.increments('${name}')${suffix};`;
474
416
 
475
- if (field.default !== null && field.default !== undefined) {
476
- line += `.defaultTo(${JSON.stringify(field.default)})`;
477
- }
417
+ case 'string':
418
+ line = `t.string('${name}', ${field.max || 255})`;
419
+ break;
478
420
 
479
- if (field.references) {
480
- line += `.references('${field.references.column}')` +
481
- `.inTable('${field.references.table}')` +
482
- `.onDelete('CASCADE')`;
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
- return `${indent}${line}${suffix};`;
486
- }
461
+ case 'enum':
462
+ line = `t.enum('${name}', ${JSON.stringify(field.enumValues || [])})`;
463
+ break;
487
464
 
488
- // ─── Snapshot ─────────────────────────────────────────────────────────────
465
+ case 'uuid':
466
+ line = `t.uuid('${name}')`;
467
+ break;
489
468
 
490
- _loadSnapshot() {
491
- try {
492
- return fs.readJsonSync(this._snapshotPath);
493
- } catch {
494
- 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};`;
495
489
  }
496
- }
497
490
 
498
- _saveSnapshot(schema) {
499
- fs.ensureDirSync(path.dirname(this._snapshotPath));
500
- fs.writeJsonSync(this._snapshotPath, schema, { spaces: 2 });
501
- }
491
+ // ─── Snapshot ─────────────────────────────────────────────────────────────
502
492
 
503
- // ─── Helpers ──────────────────────────────────────────────────────────────
493
+ _loadSnapshot() {
494
+ try {
495
+ return fs.readJsonSync(this._snapshotPath);
496
+ } catch {
497
+ return {};
498
+ }
499
+ }
504
500
 
505
- _timestamp() {
506
- return new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14);
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;