millas 0.2.12-beta-1 → 0.2.12-beta-2

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 (82) hide show
  1. package/package.json +1 -1
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +400 -167
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +309 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +383 -97
  12. package/src/admin/static/admin.css +1341 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +65 -1013
  19. package/src/admin/views/pages/detail.njk +40 -16
  20. package/src/admin/views/pages/form.njk +47 -599
  21. package/src/admin/views/pages/list.njk +145 -62
  22. package/src/admin/views/partials/form-field.njk +53 -0
  23. package/src/admin/views/partials/form-footer.njk +28 -0
  24. package/src/admin/views/partials/form-readonly.njk +114 -0
  25. package/src/admin/views/partials/form-scripts.njk +476 -0
  26. package/src/admin/views/partials/form-widget.njk +296 -0
  27. package/src/admin/views/partials/json-dialog.njk +80 -0
  28. package/src/admin/views/partials/json-editor.njk +37 -0
  29. package/src/admin.zip +0 -0
  30. package/src/auth/Auth.js +18 -2
  31. package/src/auth/AuthUser.js +65 -44
  32. package/src/cli.js +3 -1
  33. package/src/commands/createsuperuser.js +254 -0
  34. package/src/commands/lang.js +589 -0
  35. package/src/commands/migrate.js +154 -81
  36. package/src/commands/serve.js +1 -0
  37. package/src/container/AppInitializer.js +65 -8
  38. package/src/container/MillasApp.js +10 -3
  39. package/src/container/MillasConfig.js +35 -6
  40. package/src/core/admin.js +5 -0
  41. package/src/core/db.js +2 -1
  42. package/src/core/foundation.js +1 -9
  43. package/src/core/lang.js +1 -0
  44. package/src/i18n/I18nServiceProvider.js +91 -0
  45. package/src/i18n/Translator.js +635 -0
  46. package/src/i18n/defaults.js +122 -0
  47. package/src/i18n/index.js +164 -0
  48. package/src/i18n/locales/en.js +55 -0
  49. package/src/i18n/locales/sw.js +48 -0
  50. package/src/logger/formatters/PrettyFormatter.js +101 -65
  51. package/src/migrations/system/0001_users.js +21 -0
  52. package/src/migrations/system/0002_admin_log.js +25 -0
  53. package/src/migrations/system/0003_sessions.js +23 -0
  54. package/src/orm/fields/index.js +210 -188
  55. package/src/orm/migration/DefaultValueParser.js +325 -0
  56. package/src/orm/migration/InteractiveResolver.js +191 -0
  57. package/src/orm/migration/Makemigrations.js +312 -0
  58. package/src/orm/migration/MigrationGraph.js +227 -0
  59. package/src/orm/migration/MigrationRunner.js +202 -108
  60. package/src/orm/migration/MigrationWriter.js +463 -0
  61. package/src/orm/migration/ModelInspector.js +143 -74
  62. package/src/orm/migration/ModelScanner.js +225 -0
  63. package/src/orm/migration/ProjectState.js +213 -0
  64. package/src/orm/migration/RenameDetector.js +175 -0
  65. package/src/orm/migration/SchemaBuilder.js +8 -81
  66. package/src/orm/migration/operations/base.js +57 -0
  67. package/src/orm/migration/operations/column.js +191 -0
  68. package/src/orm/migration/operations/fields.js +252 -0
  69. package/src/orm/migration/operations/index.js +55 -0
  70. package/src/orm/migration/operations/models.js +152 -0
  71. package/src/orm/migration/operations/registry.js +131 -0
  72. package/src/orm/migration/operations/special.js +51 -0
  73. package/src/orm/migration/utils.js +208 -0
  74. package/src/orm/model/Model.js +81 -13
  75. package/src/providers/AdminServiceProvider.js +66 -9
  76. package/src/providers/AuthServiceProvider.js +40 -5
  77. package/src/providers/CacheStorageServiceProvider.js +2 -2
  78. package/src/providers/DatabaseServiceProvider.js +3 -2
  79. package/src/providers/LogServiceProvider.js +4 -1
  80. package/src/providers/MailServiceProvider.js +1 -1
  81. package/src/providers/QueueServiceProvider.js +1 -1
  82. package/src/scaffold/templates.js +77 -21
@@ -9,40 +9,81 @@ module.exports = function (program) {
9
9
  // ── makemigrations ──────────────────────────────────────────────────────────
10
10
  program
11
11
  .command('makemigrations')
12
- .description('Scan model files, detect schema changes, generate migration files')
13
- .action(async () => {
12
+ .description('Detect model changes and generate migration files (never touches DB)')
13
+ .option('--dry-run', 'Show what would be generated without writing files')
14
+ .option('--noinput', 'Non-interactive mode — fails if dangerous fields need resolution')
15
+ .action(async (options) => {
14
16
  try {
15
- const ctx = getProjectContext();
16
- const ModelInspector = require('../orm/migration/ModelInspector');
17
- const inspector = new ModelInspector(
18
- ctx.modelsPath,
19
- ctx.migrationsPath,
20
- ctx.snapshotPath,
21
- );
22
- const result = await inspector.makeMigrations();
17
+ const ctx = getProjectContext();
18
+ const Mk = require('../orm/migration/Makemigrations');
19
+ const mk = new Mk(ctx.modelsPath, ctx.appMigPath, ctx.systemMigPath, {
20
+ nonInteractive: options.noinput || false,
21
+ });
22
+ const result = await mk.run({ dryRun: options.dryRun });
23
23
 
24
24
  if (result.files.length === 0) {
25
- console.log(chalk.yellow(`\n ${result.message}\n`));
25
+ console.log(chalk.yellow(`\n No changes detected.\n`));
26
26
  } else {
27
- console.log(chalk.green(`\n ✔ ${result.message}`));
28
- result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
29
- console.log(chalk.gray('\n Run: millas migrate to apply these migrations.\n'));
27
+ if (options.dryRun) {
28
+ console.log(chalk.cyan('\n Would generate:'));
29
+ } else {
30
+ console.log(chalk.green('\n Migrations generated:'));
31
+ }
32
+ result.files.forEach(f => console.log(chalk.cyan(` + ${f}`)));
33
+ result.ops.forEach(op => {
34
+ // Match Django's +/-/~ prefix style exactly
35
+ let prefix, label;
36
+ switch (op.type) {
37
+ case 'CreateModel':
38
+ prefix = chalk.green('+'); label = `Create model ${op.table}`; break;
39
+ case 'DeleteModel':
40
+ prefix = chalk.red('-'); label = `Delete model ${op.table}`; break;
41
+ case 'AddField':
42
+ prefix = chalk.green('+'); label = `Add field ${op.column} to ${op.table}`; break;
43
+ case 'RemoveField':
44
+ prefix = chalk.red('-'); label = `Remove field ${op.column} from ${op.table}`; break;
45
+ case 'AlterField':
46
+ prefix = chalk.yellow('~'); label = `Alter field ${op.column} on ${op.table}`; break;
47
+ case 'RenameField':
48
+ prefix = chalk.yellow('~'); label = `Rename field ${op.oldColumn} on ${op.table} to ${op.newColumn}`; break;
49
+ case 'RenameModel':
50
+ prefix = chalk.yellow('~'); label = `Rename model ${op.oldTable} to ${op.newTable}`; break;
51
+ default:
52
+ prefix = chalk.gray(' '); label = op.type;
53
+ }
54
+ console.log(chalk.gray(` ${prefix} ${label}`));
55
+ });
56
+ if (!options.dryRun) {
57
+ console.log(chalk.gray('\n Run: millas migrate to apply.\n'));
58
+ } else {
59
+ console.log();
60
+ }
30
61
  }
31
62
  } catch (err) {
32
63
  bail('makemigrations', err);
33
64
  }
34
- // makemigrations doesn't open a DB connection so no closeDb() needed
35
65
  });
36
66
 
37
67
  // ── migrate ─────────────────────────────────────────────────────────────────
38
68
  program
39
69
  .command('migrate')
40
- .description('Run all pending migrations')
41
- .action(async () => {
70
+ .description('Apply pending migrations in dependency order (never generates migrations)')
71
+ .option('--fake <name>', 'Mark a migration as applied without running it')
72
+ .action(async (options) => {
42
73
  try {
43
74
  const runner = await getRunner();
75
+
76
+ if (options.fake) {
77
+ const [source, name] = options.fake.includes(':')
78
+ ? options.fake.split(':')
79
+ : ['app', options.fake];
80
+ const result = await runner.fake(source, name);
81
+ console.log(chalk.green(`\n Marked "${result.key}" as applied (fake).\n`));
82
+ return;
83
+ }
84
+
44
85
  const result = await runner.migrate();
45
- printMigrationResult(result, 'Ran');
86
+ printResult(result.ran, 'Applying');
46
87
  } catch (err) {
47
88
  bail('migrate', err);
48
89
  } finally {
@@ -50,18 +91,69 @@ module.exports = function (program) {
50
91
  }
51
92
  });
52
93
 
53
- // ── migrate:fresh ────────────────────────────────────────────────────────────
94
+ // ── migrate:plan ─────────────────────────────────────────────────────────────
54
95
  program
55
- .command('migrate:fresh')
56
- .description('Drop ALL tables then re-run every migration from scratch')
96
+ .command('migrate:plan')
97
+ .description('Preview which migrations would run without applying them')
98
+ .action(async () => {
99
+ try {
100
+ const runner = await getRunner();
101
+ const pending = await runner.plan();
102
+
103
+ if (pending.length === 0) {
104
+ console.log(chalk.yellow('\n Nothing to migrate.\n'));
105
+ return;
106
+ }
107
+
108
+ console.log(chalk.cyan('\n Migration plan:\n'));
109
+ pending.forEach(p => {
110
+ const color = p.source === 'system' ? chalk.gray : chalk.cyan;
111
+ console.log(color(` ${p.key}`));
112
+ });
113
+ console.log();
114
+ } catch (err) {
115
+ bail('migrate:plan', err);
116
+ } finally {
117
+ await closeDb();
118
+ }
119
+ });
120
+
121
+ // ── migrate:status ───────────────────────────────────────────────────────────
122
+ program
123
+ .command('migrate:status')
124
+ .description('Show the status of all migrations')
57
125
  .action(async () => {
58
126
  try {
59
- console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
60
127
  const runner = await getRunner();
61
- const result = await runner.fresh();
62
- printMigrationResult(result, 'Ran');
128
+ const rows = await runner.status();
129
+
130
+ if (rows.length === 0) {
131
+ console.log(chalk.yellow('\n No migrations found.\n'));
132
+ return;
133
+ }
134
+
135
+ const colW = Math.max(...rows.map(r => r.key.length)) + 2;
136
+ console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
137
+ console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
138
+
139
+ let lastSource = null;
140
+ for (const row of rows) {
141
+ if (row.source !== lastSource) {
142
+ if (lastSource !== null) console.log();
143
+ lastSource = row.source;
144
+ }
145
+ const status = row.status === 'Applied'
146
+ ? chalk.green(row.status.padEnd(10))
147
+ : chalk.yellow(row.status.padEnd(10));
148
+ const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
149
+ const label = row.source === 'system'
150
+ ? chalk.gray(row.key.padEnd(colW))
151
+ : chalk.cyan(row.key.padEnd(colW));
152
+ console.log(` ${label} ${status} ${batch}`);
153
+ }
154
+ console.log();
63
155
  } catch (err) {
64
- bail('migrate:fresh', err);
156
+ bail('migrate:status', err);
65
157
  } finally {
66
158
  await closeDb();
67
159
  }
@@ -76,7 +168,7 @@ module.exports = function (program) {
76
168
  try {
77
169
  const runner = await getRunner();
78
170
  const result = await runner.rollback(Number(options.steps));
79
- printMigrationResult(result, 'Rolled back');
171
+ printResult(result.rolledBack, 'Reverting');
80
172
  } catch (err) {
81
173
  bail('migrate:rollback', err);
82
174
  } finally {
@@ -84,66 +176,50 @@ module.exports = function (program) {
84
176
  }
85
177
  });
86
178
 
87
- // ── migrate:reset ────────────────────────────────────────────────────────────
179
+ // ── migrate:fresh ────────────────────────────────────────────────────────────
88
180
  program
89
- .command('migrate:reset')
90
- .description('Rollback ALL migrations')
181
+ .command('migrate:fresh')
182
+ .description('Drop all tables and re-run every migration from scratch')
91
183
  .action(async () => {
92
184
  try {
185
+ console.log(chalk.yellow('\n ⚠ Dropping all tables…\n'));
93
186
  const runner = await getRunner();
94
- const result = await runner.reset();
95
- printMigrationResult(result, 'Rolled back');
187
+ const result = await runner.fresh();
188
+ printResult(result.ran, 'Applying');
96
189
  } catch (err) {
97
- bail('migrate:reset', err);
190
+ bail('migrate:fresh', err);
98
191
  } finally {
99
192
  await closeDb();
100
193
  }
101
194
  });
102
195
 
103
- // ── migrate:refresh ──────────────────────────────────────────────────────────
196
+ // ── migrate:reset ────────────────────────────────────────────────────────────
104
197
  program
105
- .command('migrate:refresh')
106
- .description('Rollback all then re-run all migrations')
198
+ .command('migrate:reset')
199
+ .description('Rollback ALL migrations')
107
200
  .action(async () => {
108
201
  try {
109
202
  const runner = await getRunner();
110
- const result = await runner.refresh();
111
- printMigrationResult(result, 'Ran');
203
+ const result = await runner.reset();
204
+ printResult(result.rolledBack, 'Reverting');
112
205
  } catch (err) {
113
- bail('migrate:refresh', err);
206
+ bail('migrate:reset', err);
114
207
  } finally {
115
208
  await closeDb();
116
209
  }
117
210
  });
118
211
 
119
- // ── migrate:status ───────────────────────────────────────────────────────────
212
+ // ── migrate:refresh ──────────────────────────────────────────────────────────
120
213
  program
121
- .command('migrate:status')
122
- .description('Show the status of all migration files')
214
+ .command('migrate:refresh')
215
+ .description('Rollback all then re-run all migrations')
123
216
  .action(async () => {
124
217
  try {
125
218
  const runner = await getRunner();
126
- const rows = await runner.status();
127
-
128
- if (rows.length === 0) {
129
- console.log(chalk.yellow('\n No migration files found.\n'));
130
- return;
131
- }
132
-
133
- const colW = Math.max(...rows.map(r => r.name.length)) + 2;
134
- console.log(`\n ${'Migration'.padEnd(colW)} ${'Status'.padEnd(10)} Batch`);
135
- console.log(chalk.gray(' ' + '─'.repeat(colW + 20)));
136
-
137
- for (const row of rows) {
138
- const status = row.status === 'Ran'
139
- ? chalk.green(row.status.padEnd(10))
140
- : chalk.yellow(row.status.padEnd(10));
141
- const batch = row.batch ? chalk.gray(String(row.batch)) : chalk.gray('—');
142
- console.log(` ${chalk.cyan(row.name.padEnd(colW))} ${status} ${batch}`);
143
- }
144
- console.log();
219
+ const result = await runner.refresh();
220
+ printResult(result.ran, 'Applying');
145
221
  } catch (err) {
146
- bail('migrate:status', err);
222
+ bail('migrate:refresh', err);
147
223
  } finally {
148
224
  await closeDb();
149
225
  }
@@ -193,10 +269,10 @@ module.exports = function (program) {
193
269
  function getProjectContext() {
194
270
  const cwd = process.cwd();
195
271
  return {
196
- migrationsPath: path.join(cwd, 'database/migrations'),
197
- seedersPath: path.join(cwd, 'database/seeders'),
198
- modelsPath: path.join(cwd, 'app/models'),
199
- snapshotPath: path.join(cwd, '.millas/schema.json'),
272
+ appMigPath: path.join(cwd, 'database/migrations'),
273
+ systemMigPath: path.join(__dirname, '../migrations/system'),
274
+ seedersPath: path.join(cwd, 'database/seeders'),
275
+ modelsPath: path.join(cwd, 'app/models'),
200
276
  };
201
277
  }
202
278
 
@@ -215,31 +291,28 @@ async function getRunner() {
215
291
  const MigrationRunner = require('../orm/migration/MigrationRunner');
216
292
  const ctx = getProjectContext();
217
293
  const db = await getDbConnection();
218
- return new MigrationRunner(db, ctx.migrationsPath);
294
+ return new MigrationRunner(db, ctx.appMigPath, ctx.systemMigPath);
219
295
  }
220
296
 
221
- /**
222
- * Destroy all open knex connection pools so the CLI process exits cleanly.
223
- * Without this, knex keeps the event loop alive indefinitely after the
224
- * command finishes, causing the terminal to appear to hang.
225
- */
226
297
  async function closeDb() {
227
298
  try {
228
299
  const DatabaseManager = require('../orm/drivers/DatabaseManager');
229
300
  await DatabaseManager.closeAll();
230
- } catch { /* already closed or never opened — safe to ignore */ }
301
+ } catch {}
231
302
  }
232
303
 
233
- function printMigrationResult(result, verb) {
234
- const list = result.ran || result.rolledBack || [];
235
- if (list.length === 0) {
236
- console.log(chalk.yellow(`\n ${result.message}\n`));
304
+ function printResult(list, verb) {
305
+ if (!list || list.length === 0) {
306
+ console.log(chalk.yellow('\n Nothing to do.\n'));
237
307
  return;
238
308
  }
239
- console.log(chalk.green(`\n ✔ ${result.message}`));
240
- list.forEach(f =>
241
- console.log(chalk.cyan(` ${verb === 'Ran' ? '+' : '-'} ${f}`))
242
- );
309
+ console.log(chalk.green(`\n Running migrations:`));
310
+ for (const entry of list) {
311
+ const label = typeof entry === 'object' ? entry.label || entry.key : entry;
312
+ const source = typeof entry === 'object' ? entry.source : null;
313
+ const color = source === 'system' ? chalk.gray : chalk.cyan;
314
+ console.log(color(` ${verb} ${label}... OK`));
315
+ }
243
316
  console.log();
244
317
  }
245
318
 
@@ -247,4 +320,4 @@ function bail(cmd, err) {
247
320
  console.error(chalk.red(`\n ✖ ${cmd} failed: ${err.message}\n`));
248
321
  if (process.env.DEBUG) console.error(err.stack);
249
322
  closeDb().finally(() => process.exit(1));
250
- }
323
+ }
@@ -85,6 +85,7 @@ class HotReloader {
85
85
  env: {
86
86
  ...extra,
87
87
  MILLAS_CHILD: '1',
88
+ DEBUG: process.env.APP_DEBUG,
88
89
  },
89
90
  stdio: 'inherit',
90
91
  });
@@ -23,12 +23,15 @@ const HttpServer = require('./HttpServer');
23
23
  * bootstrap/app.js → MillasInstance → AppInitialiser.boot()
24
24
  * ├─ builds ExpressAdapter
25
25
  * ├─ builds Application
26
- * ├─ registers providers
26
+ * ├─ registers core providers
27
+ * │ Log → Database → Auth → Admin
28
+ * │ → Cache → Mail → Queue → Events
29
+ * ├─ registers app providers
27
30
  * ├─ registers routes
28
31
  * ├─ registers middleware aliases
29
- * ├─ boots providers
32
+ * ├─ boots all providers
30
33
  * ├─ mounts routes
31
- * ├─ mounts admin (if configured)
34
+ * ├─ mounts admin panel
32
35
  * ├─ mounts fallbacks
33
36
  * └─ starts HttpServer
34
37
  */
@@ -63,6 +66,11 @@ class AppInitializer {
63
66
  // ── Build the Application kernel ─────────────────────────────────────────
64
67
  this._kernel = new Application(this._adapter);
65
68
 
69
+ // Bind basePath so all providers can resolve config/model paths
70
+ // without calling process.cwd() at runtime.
71
+ const basePath = cfg.basePath || process.cwd();
72
+ this._kernel._container.instance('basePath', basePath);
73
+
66
74
  // Core providers (auto-enabled unless disabled in config)
67
75
  const coreProviders = this._buildCoreProviders(cfg);
68
76
  this._kernel.providers([...coreProviders, ...cfg.providers]);
@@ -114,22 +122,39 @@ class AppInitializer {
114
122
  _buildCoreProviders(cfg) {
115
123
  const providers = [];
116
124
  const load = (p) => {
117
- try {
118
- return require(p);
119
- } catch {
120
- return null;
121
- }
125
+ try { return require(p); } catch { return null; }
122
126
  };
123
127
 
128
+ // ── 1. Logging ───────────────────────────────────────────────────────
124
129
  if (cfg.logging !== false) {
125
130
  const p = load('../providers/LogServiceProvider');
126
131
  if (p) providers.push(p);
127
132
  }
133
+
134
+ // ── 2. Database ──────────────────────────────────────────────────────
128
135
  if (cfg.database !== false) {
129
136
  const p = load('../providers/DatabaseServiceProvider');
130
137
  if (p) providers.push(p);
131
138
  }
132
139
 
140
+ // ── 3. Auth — always on unless explicitly disabled ───────────────────
141
+ // Mirrors Django: django.contrib.auth is in INSTALLED_APPS by default.
142
+ // Provides Auth.login/register, JWT middleware, the User model.
143
+ // Requires Database to be booted first.
144
+ if (cfg.auth !== false) {
145
+ const p = load('../providers/AuthServiceProvider');
146
+ if (p) providers.push(p);
147
+ }
148
+
149
+ // ── 4. Admin — on when .withAdmin() was called ───────────────────────
150
+ // Mirrors Django: django.contrib.admin is in INSTALLED_APPS by default.
151
+ // Requires Auth to be booted first (needs the resolved User model).
152
+ if (cfg.admin !== null && cfg.admin !== undefined) {
153
+ const p = load('../providers/AdminServiceProvider');
154
+ if (p) providers.push(p);
155
+ }
156
+
157
+ // ── 5. Cache + Storage ───────────────────────────────────────────────
133
158
  if (cfg.cache !== false || cfg.storage !== false) {
134
159
  const p = load('../providers/CacheStorageServiceProvider');
135
160
  if (p) {
@@ -138,21 +163,53 @@ class AppInitializer {
138
163
  }
139
164
  }
140
165
 
166
+ // ── 6. Mail ──────────────────────────────────────────────────────────
141
167
  if (cfg.mail !== false) {
142
168
  const p = load('../providers/MailServiceProvider');
143
169
  if (p) providers.push(p);
144
170
  }
171
+
172
+ // ── 7. Queue ─────────────────────────────────────────────────────────
145
173
  if (cfg.queue !== false) {
146
174
  const p = load('../providers/QueueServiceProvider');
147
175
  if (p) providers.push(p);
148
176
  }
177
+
178
+ // ── 8. Events ────────────────────────────────────────────────────────
149
179
  if (cfg.events !== false) {
150
180
  const p = load('../providers/EventServiceProvider');
151
181
  if (p) providers.push(p);
152
182
  }
153
183
 
184
+ // ── 9. i18n — opt-in via config/app.js use_i18n: true ───────────────
185
+ // Mirrors Django's USE_I18N = True in settings.py.
186
+ // Booted last so translations are available in all request handlers.
187
+ if (this._resolveI18nEnabled(cfg)) {
188
+ const p = load('../i18n/I18nServiceProvider');
189
+ if (p) providers.push(p);
190
+ }
191
+
154
192
  return providers;
155
193
  }
194
+
195
+ /**
196
+ * Resolve whether i18n should be enabled.
197
+ * Reads use_i18n from config/app.js — the single source of truth.
198
+ *
199
+ * // config/app.js
200
+ * module.exports = {
201
+ * use_i18n: true,
202
+ * locale: 'sw',
203
+ * fallback: 'en',
204
+ * };
205
+ */
206
+ _resolveI18nEnabled(cfg) {
207
+ try {
208
+ const basePath = cfg.basePath || process.cwd();
209
+ const appConfig = require(basePath + '/config/app');
210
+ return appConfig.use_i18n === true;
211
+ } catch { return false; }
212
+ }
156
213
  }
157
214
 
158
215
  module.exports = AppInitializer;
@@ -30,12 +30,19 @@ const MillasConfig = require('./MillasConfig');
30
30
  const Millas = {
31
31
  /**
32
32
  * Start building an application config.
33
- * Returns a MillasConfig chain ending in .create().
33
+ * Equivalent to Millas.config().configure(basePath).
34
34
  *
35
+ * Usage (bootstrap/app.js):
36
+ * module.exports = Millas.configure(__dirname)
37
+ * .withAdmin()
38
+ * .routes(Route => { ... })
39
+ * .create();
40
+ *
41
+ * @param {string} basePath — pass __dirname from bootstrap/app.js
35
42
  * @returns {MillasConfig}
36
43
  */
37
- config() {
38
- return new MillasConfig();
44
+ configure(basePath) {
45
+ return new MillasConfig().configure(basePath);
39
46
  },
40
47
  };
41
48
 
@@ -25,6 +25,11 @@ const AppInitializer = require("./AppInitializer");
25
25
  class MillasConfig {
26
26
  constructor() {
27
27
  this._config = {
28
+ // Absolute path to the project root — passed to all providers
29
+ // so they never need to call process.cwd() at runtime.
30
+ // Set via .configure(__dirname) in bootstrap/app.js.
31
+ basePath: null,
32
+
28
33
  // Service providers
29
34
  providers: [],
30
35
 
@@ -35,13 +40,14 @@ class MillasConfig {
35
40
  middleware: [],
36
41
 
37
42
  // Core service toggles
43
+ logging: true,
38
44
  database: true,
39
- cache: true,
40
- storage: true,
41
- mail: true,
42
- queue: true,
43
- events: true,
44
- logging: true,
45
+ auth: true, // AuthServiceProvider — always on by default
46
+ cache: true,
47
+ storage: true,
48
+ mail: true,
49
+ queue: true,
50
+ events: true,
45
51
 
46
52
  // Admin panel — null means disabled, {} or options object means enabled
47
53
  admin: null,
@@ -57,6 +63,22 @@ class MillasConfig {
57
63
 
58
64
  // ── Chainable config methods ───────────────────────────────────────────────
59
65
 
66
+ /**
67
+ * Set the application base path. Must be the first call.
68
+ * Pass __dirname from bootstrap/app.js — Laravel style.
69
+ *
70
+ * Millas.configure(__dirname)
71
+ *
72
+ * All providers use this to locate config files, models, and routes
73
+ * without relying on process.cwd() at runtime.
74
+ *
75
+ * @param {string} basePath — absolute path to the project root
76
+ */
77
+ configure(basePath) {
78
+ this._config.basePath = basePath;
79
+ return this;
80
+ }
81
+
60
82
  /**
61
83
  * Register application service providers.
62
84
  *
@@ -93,8 +115,15 @@ class MillasConfig {
93
115
  /**
94
116
  * Enable the admin panel.
95
117
  *
118
+ * AuthServiceProvider and AdminServiceProvider are registered
119
+ * automatically — no need to add them to .providers([]).
120
+ *
96
121
  * .withAdmin()
97
122
  * .withAdmin({ prefix: '/cms', title: 'My CMS' })
123
+ *
124
+ * First-time setup:
125
+ * millas migrate # creates users + admin_log + sessions tables
126
+ * millas createsuperuser # creates your first admin user
98
127
  */
99
128
  withAdmin(options = {}) {
100
129
  this._config.admin = options;
@@ -0,0 +1,5 @@
1
+ // ── Admin ─────────────────────────────────────────────────────────
2
+ const {Admin, AdminResource, AdminField, AdminFilter, AdminHooks, AdminInline} = require('../admin');
3
+ module.exports = {
4
+ Admin, AdminResource, AdminField, AdminFilter, AdminHooks, AdminInline
5
+ }
package/src/core/db.js CHANGED
@@ -2,7 +2,8 @@ const {
2
2
  Model,
3
3
  fields
4
4
  } = require("../orm");
5
+ const { migrations } = require("../orm/migration/operations");
5
6
 
6
7
  module.exports = {
7
- Model, fields
8
+ Model, fields,migrations
8
9
  }
@@ -56,12 +56,4 @@ module.exports = {
56
56
  Cache, MemoryDriver, FileDriver, NullDriver, CacheServiceProvider,
57
57
  // Storage
58
58
  Storage, LocalDriver, StorageServiceProvider,
59
- };
60
-
61
- // ── Admin ─────────────────────────────────────────────────────────
62
- const { Admin, AdminResource, AdminField, AdminFilter } = require('../admin');
63
- const AdminServiceProvider = require('../providers/AdminServiceProvider');
64
-
65
- Object.assign(module.exports, {
66
- Admin, AdminResource, AdminField, AdminFilter, AdminServiceProvider,
67
- });
59
+ };
@@ -0,0 +1 @@
1
+ module.exports = require('../i18n')