millas 0.2.12-beta-1 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/admin/ActivityLog.js +153 -52
- package/src/admin/Admin.js +516 -199
- package/src/admin/AdminAuth.js +213 -98
- package/src/admin/FormGenerator.js +372 -0
- package/src/admin/HookRegistry.js +256 -0
- package/src/admin/QueryEngine.js +263 -0
- package/src/admin/ViewContext.js +318 -0
- package/src/admin/WidgetRegistry.js +406 -0
- package/src/admin/index.js +17 -0
- package/src/admin/resources/AdminResource.js +393 -97
- package/src/admin/static/admin.css +1422 -0
- package/src/admin/static/date-picker.css +157 -0
- package/src/admin/static/date-picker.js +316 -0
- package/src/admin/static/json-editor.css +649 -0
- package/src/admin/static/json-editor.js +1429 -0
- package/src/admin/static/ui.js +1044 -0
- package/src/admin/views/layouts/base.njk +87 -1046
- package/src/admin/views/pages/detail.njk +56 -21
- package/src/admin/views/pages/error.njk +65 -0
- package/src/admin/views/pages/form.njk +47 -599
- package/src/admin/views/pages/list.njk +270 -62
- package/src/admin/views/partials/form-field.njk +53 -0
- package/src/admin/views/partials/form-footer.njk +28 -0
- package/src/admin/views/partials/form-readonly.njk +114 -0
- package/src/admin/views/partials/form-scripts.njk +480 -0
- package/src/admin/views/partials/form-widget.njk +297 -0
- package/src/admin/views/partials/icons.njk +64 -0
- package/src/admin/views/partials/json-dialog.njk +80 -0
- package/src/admin/views/partials/json-editor.njk +37 -0
- package/src/ai/AIManager.js +954 -0
- package/src/ai/AITokenBudget.js +250 -0
- package/src/ai/PromptGuard.js +216 -0
- package/src/ai/agents.js +218 -0
- package/src/ai/conversation.js +213 -0
- package/src/ai/drivers.js +734 -0
- package/src/ai/files.js +249 -0
- package/src/ai/media.js +303 -0
- package/src/ai/pricing.js +152 -0
- package/src/ai/provider_tools.js +114 -0
- package/src/ai/types.js +356 -0
- package/src/auth/Auth.js +18 -2
- package/src/auth/AuthUser.js +65 -44
- package/src/cli.js +3 -1
- package/src/commands/createsuperuser.js +267 -0
- package/src/commands/lang.js +589 -0
- package/src/commands/migrate.js +154 -81
- package/src/commands/serve.js +3 -4
- package/src/container/AppInitializer.js +101 -20
- package/src/container/Application.js +31 -1
- package/src/container/MillasApp.js +10 -3
- package/src/container/MillasConfig.js +35 -6
- package/src/core/admin.js +5 -0
- package/src/core/db.js +2 -1
- package/src/core/foundation.js +2 -10
- package/src/core/lang.js +1 -0
- package/src/errors/HttpError.js +32 -16
- package/src/facades/AI.js +411 -0
- package/src/facades/Hash.js +67 -0
- package/src/facades/Process.js +144 -0
- package/src/hashing/Hash.js +262 -0
- package/src/http/HtmlEscape.js +162 -0
- package/src/http/MillasRequest.js +63 -7
- package/src/http/MillasResponse.js +70 -4
- package/src/http/ResponseDispatcher.js +21 -27
- package/src/http/SafeFilePath.js +195 -0
- package/src/http/SafeRedirect.js +62 -0
- package/src/http/SecurityBootstrap.js +70 -0
- package/src/http/helpers.js +40 -125
- package/src/http/index.js +10 -1
- package/src/http/middleware/CsrfMiddleware.js +258 -0
- package/src/http/middleware/RateLimiter.js +314 -0
- package/src/http/middleware/SecurityHeaders.js +281 -0
- package/src/i18n/I18nServiceProvider.js +91 -0
- package/src/i18n/Translator.js +643 -0
- package/src/i18n/defaults.js +122 -0
- package/src/i18n/index.js +164 -0
- package/src/i18n/locales/en.js +55 -0
- package/src/i18n/locales/sw.js +48 -0
- package/src/logger/LogRedactor.js +247 -0
- package/src/logger/Logger.js +1 -1
- package/src/logger/formatters/JsonFormatter.js +11 -4
- package/src/logger/formatters/PrettyFormatter.js +103 -65
- package/src/logger/formatters/SimpleFormatter.js +14 -3
- package/src/middleware/ThrottleMiddleware.js +27 -4
- package/src/migrations/system/0001_users.js +21 -0
- package/src/migrations/system/0002_admin_log.js +25 -0
- package/src/migrations/system/0003_sessions.js +23 -0
- package/src/orm/fields/index.js +210 -188
- package/src/orm/migration/DefaultValueParser.js +325 -0
- package/src/orm/migration/InteractiveResolver.js +191 -0
- package/src/orm/migration/Makemigrations.js +312 -0
- package/src/orm/migration/MigrationGraph.js +227 -0
- package/src/orm/migration/MigrationRunner.js +202 -108
- package/src/orm/migration/MigrationWriter.js +463 -0
- package/src/orm/migration/ModelInspector.js +143 -74
- package/src/orm/migration/ModelScanner.js +225 -0
- package/src/orm/migration/ProjectState.js +213 -0
- package/src/orm/migration/RenameDetector.js +175 -0
- package/src/orm/migration/SchemaBuilder.js +8 -81
- package/src/orm/migration/operations/base.js +57 -0
- package/src/orm/migration/operations/column.js +191 -0
- package/src/orm/migration/operations/fields.js +252 -0
- package/src/orm/migration/operations/index.js +55 -0
- package/src/orm/migration/operations/models.js +152 -0
- package/src/orm/migration/operations/registry.js +131 -0
- package/src/orm/migration/operations/special.js +51 -0
- package/src/orm/migration/utils.js +208 -0
- package/src/orm/model/Model.js +81 -13
- package/src/process/Process.js +333 -0
- package/src/providers/AdminServiceProvider.js +66 -9
- package/src/providers/AuthServiceProvider.js +40 -5
- package/src/providers/CacheStorageServiceProvider.js +2 -2
- package/src/providers/DatabaseServiceProvider.js +3 -2
- package/src/providers/LogServiceProvider.js +4 -1
- package/src/providers/MailServiceProvider.js +1 -1
- package/src/providers/QueueServiceProvider.js +1 -1
- package/src/router/MiddlewareRegistry.js +27 -2
- package/src/scaffold/templates.js +80 -21
- package/src/validation/Validator.js +348 -607
package/src/commands/migrate.js
CHANGED
|
@@ -9,40 +9,81 @@ module.exports = function (program) {
|
|
|
9
9
|
// ── makemigrations ──────────────────────────────────────────────────────────
|
|
10
10
|
program
|
|
11
11
|
.command('makemigrations')
|
|
12
|
-
.description('
|
|
13
|
-
.
|
|
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
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
25
|
+
console.log(chalk.yellow(`\n No changes detected.\n`));
|
|
26
26
|
} else {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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('
|
|
41
|
-
.
|
|
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
|
-
|
|
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:
|
|
94
|
+
// ── migrate:plan ─────────────────────────────────────────────────────────────
|
|
54
95
|
program
|
|
55
|
-
.command('migrate:
|
|
56
|
-
.description('
|
|
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
|
|
62
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
179
|
+
// ── migrate:fresh ────────────────────────────────────────────────────────────
|
|
88
180
|
program
|
|
89
|
-
.command('migrate:
|
|
90
|
-
.description('
|
|
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.
|
|
95
|
-
|
|
187
|
+
const result = await runner.fresh();
|
|
188
|
+
printResult(result.ran, 'Applying');
|
|
96
189
|
} catch (err) {
|
|
97
|
-
bail('migrate:
|
|
190
|
+
bail('migrate:fresh', err);
|
|
98
191
|
} finally {
|
|
99
192
|
await closeDb();
|
|
100
193
|
}
|
|
101
194
|
});
|
|
102
195
|
|
|
103
|
-
// ── migrate:
|
|
196
|
+
// ── migrate:reset ────────────────────────────────────────────────────────────
|
|
104
197
|
program
|
|
105
|
-
.command('migrate:
|
|
106
|
-
.description('Rollback
|
|
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.
|
|
111
|
-
|
|
203
|
+
const result = await runner.reset();
|
|
204
|
+
printResult(result.rolledBack, 'Reverting');
|
|
112
205
|
} catch (err) {
|
|
113
|
-
bail('migrate:
|
|
206
|
+
bail('migrate:reset', err);
|
|
114
207
|
} finally {
|
|
115
208
|
await closeDb();
|
|
116
209
|
}
|
|
117
210
|
});
|
|
118
211
|
|
|
119
|
-
// ── migrate:
|
|
212
|
+
// ── migrate:refresh ──────────────────────────────────────────────────────────
|
|
120
213
|
program
|
|
121
|
-
.command('migrate:
|
|
122
|
-
.description('
|
|
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
|
|
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:
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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.
|
|
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 {
|
|
301
|
+
} catch {}
|
|
231
302
|
}
|
|
232
303
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
}
|
package/src/commands/serve.js
CHANGED
|
@@ -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
|
});
|
|
@@ -120,13 +121,11 @@ class HotReloader {
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
_restart(changedFile) {
|
|
123
|
-
|
|
124
|
-
? `\x1b]8;;file://${changedFile}\x07${path.relative(process.cwd(), changedFile)}\x1b]8;;\x07`
|
|
125
|
-
: '';
|
|
124
|
+
|
|
126
125
|
console.warn(
|
|
127
126
|
chalk.yellow('↺') + ' ' +
|
|
128
127
|
chalk.white('Reloading') +
|
|
129
|
-
(
|
|
128
|
+
(changedFile ? chalk.blueBright(' ' + changedFile) : '')
|
|
130
129
|
);
|
|
131
130
|
|
|
132
131
|
this._restarts++;
|
|
@@ -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
|
|
34
|
+
* ├─ mounts admin panel
|
|
32
35
|
* ├─ mounts fallbacks
|
|
33
36
|
* └─ starts HttpServer
|
|
34
37
|
*/
|
|
@@ -46,52 +49,82 @@ class AppInitializer {
|
|
|
46
49
|
* Boot the full application and start the HTTP server.
|
|
47
50
|
* Returns a Promise that resolves once the server is listening.
|
|
48
51
|
*/
|
|
52
|
+
/**
|
|
53
|
+
* Boot the full application and start the HTTP server.
|
|
54
|
+
* Called by millas serve — no changes to the developer API.
|
|
55
|
+
*/
|
|
49
56
|
async boot() {
|
|
57
|
+
await this.bootKernel();
|
|
58
|
+
await this._serve();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Boot the application kernel only — DI container, providers, DB, auth,
|
|
63
|
+
* cache, mail, queue. No HTTP server, no routes, no listen().
|
|
64
|
+
*
|
|
65
|
+
* Used internally by CLI commands (millas migrate, millas createsuperuser, etc.)
|
|
66
|
+
* via MILLAS_CLI_BOOT=1 environment variable. Developers never call this directly.
|
|
67
|
+
*
|
|
68
|
+
* @returns {Application} the booted kernel
|
|
69
|
+
*/
|
|
70
|
+
async bootKernel() {
|
|
50
71
|
const cfg = this._config;
|
|
51
72
|
|
|
52
|
-
// ── Build the HTTP adapter ───────────────────────────────────────────────
|
|
53
73
|
const ExpressAdapter = require('../http/adapters/ExpressAdapter');
|
|
54
74
|
const expressApp = express();
|
|
55
75
|
this._adapter = new ExpressAdapter(expressApp);
|
|
56
76
|
this._adapter.applyBodyParsers();
|
|
57
77
|
|
|
58
|
-
//
|
|
78
|
+
// ── Security — applied before any routes or developer middleware ──────
|
|
79
|
+
// Reads config/app.js for overrides. All protections are on by default:
|
|
80
|
+
// security headers, CSRF, rate limiting, cookie defaults, allowed hosts.
|
|
81
|
+
const SecurityBootstrap = require('../http/SecurityBootstrap');
|
|
82
|
+
const basePath = cfg.basePath || process.cwd();
|
|
83
|
+
const appConfig = SecurityBootstrap.loadConfig(basePath + '/config/app');
|
|
84
|
+
SecurityBootstrap.apply(this._adapter.nativeApp || expressApp, appConfig);
|
|
85
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
59
87
|
for (const mw of (cfg.adapterMiddleware || [])) {
|
|
60
88
|
this._adapter.applyMiddleware(mw);
|
|
61
89
|
}
|
|
62
90
|
|
|
63
|
-
// ── Build the Application kernel ─────────────────────────────────────────
|
|
64
91
|
this._kernel = new Application(this._adapter);
|
|
65
92
|
|
|
66
|
-
|
|
93
|
+
this._kernel._container.instance('basePath', basePath);
|
|
94
|
+
|
|
67
95
|
const coreProviders = this._buildCoreProviders(cfg);
|
|
68
96
|
this._kernel.providers([...coreProviders, ...cfg.providers]);
|
|
69
97
|
|
|
70
|
-
// Named middleware aliases
|
|
71
98
|
for (const {alias, handler} of (cfg.middleware || [])) {
|
|
72
99
|
this._kernel.middleware(alias, handler);
|
|
73
100
|
}
|
|
74
101
|
|
|
75
|
-
// Route definitions
|
|
76
102
|
if (cfg.routes) {
|
|
77
103
|
this._kernel.routes(cfg.routes);
|
|
78
104
|
}
|
|
79
105
|
|
|
80
|
-
// ── Boot providers ───────────────────────────────────────────────────────
|
|
81
106
|
await this._kernel.boot();
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
return this._kernel;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Mount routes and start the HTTP server.
|
|
113
|
+
* Internal — called only by boot(). CLI commands stop after bootKernel().
|
|
114
|
+
*/
|
|
115
|
+
async _serve() {
|
|
116
|
+
const cfg = this._config;
|
|
117
|
+
|
|
84
118
|
if (!process.env.MILLAS_ROUTE_LIST) {
|
|
85
119
|
this._kernel.mountRoutes();
|
|
86
120
|
|
|
87
|
-
// Admin panel — mounted between routes and fallbacks
|
|
88
121
|
if (cfg.admin !== null) {
|
|
89
122
|
try {
|
|
90
123
|
const Admin = require('../admin/Admin');
|
|
91
124
|
if (cfg.admin && Object.keys(cfg.admin).length) {
|
|
92
125
|
Admin.configure(cfg.admin);
|
|
93
126
|
}
|
|
94
|
-
Admin.mount(
|
|
127
|
+
Admin.mount(this._adapter.nativeApp);
|
|
95
128
|
} catch (err) {
|
|
96
129
|
process.stderr.write(`[millas] Admin mount failed: ${err.message}\n`);
|
|
97
130
|
}
|
|
@@ -99,9 +132,8 @@ class AppInitializer {
|
|
|
99
132
|
|
|
100
133
|
this._kernel.mountFallbacks();
|
|
101
134
|
|
|
102
|
-
// ── Start the HTTP server ──────────────────────────────────────────────
|
|
103
135
|
const server = new HttpServer(this._kernel, {
|
|
104
|
-
onStart:
|
|
136
|
+
onStart: cfg.onStart || undefined,
|
|
105
137
|
onShutdown: cfg.onShutdown || undefined,
|
|
106
138
|
});
|
|
107
139
|
|
|
@@ -114,22 +146,39 @@ class AppInitializer {
|
|
|
114
146
|
_buildCoreProviders(cfg) {
|
|
115
147
|
const providers = [];
|
|
116
148
|
const load = (p) => {
|
|
117
|
-
try {
|
|
118
|
-
return require(p);
|
|
119
|
-
} catch {
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
149
|
+
try { return require(p); } catch { return null; }
|
|
122
150
|
};
|
|
123
151
|
|
|
152
|
+
// ── 1. Logging ───────────────────────────────────────────────────────
|
|
124
153
|
if (cfg.logging !== false) {
|
|
125
154
|
const p = load('../providers/LogServiceProvider');
|
|
126
155
|
if (p) providers.push(p);
|
|
127
156
|
}
|
|
157
|
+
|
|
158
|
+
// ── 2. Database ──────────────────────────────────────────────────────
|
|
128
159
|
if (cfg.database !== false) {
|
|
129
160
|
const p = load('../providers/DatabaseServiceProvider');
|
|
130
161
|
if (p) providers.push(p);
|
|
131
162
|
}
|
|
132
163
|
|
|
164
|
+
// ── 3. Auth — always on unless explicitly disabled ───────────────────
|
|
165
|
+
// Mirrors Django: django.contrib.auth is in INSTALLED_APPS by default.
|
|
166
|
+
// Provides Auth.login/register, JWT middleware, the User model.
|
|
167
|
+
// Requires Database to be booted first.
|
|
168
|
+
if (cfg.auth !== false) {
|
|
169
|
+
const p = load('../providers/AuthServiceProvider');
|
|
170
|
+
if (p) providers.push(p);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── 4. Admin — on when .withAdmin() was called ───────────────────────
|
|
174
|
+
// Mirrors Django: django.contrib.admin is in INSTALLED_APPS by default.
|
|
175
|
+
// Requires Auth to be booted first (needs the resolved User model).
|
|
176
|
+
if (cfg.admin !== null && cfg.admin !== undefined) {
|
|
177
|
+
const p = load('../providers/AdminServiceProvider');
|
|
178
|
+
if (p) providers.push(p);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── 5. Cache + Storage ───────────────────────────────────────────────
|
|
133
182
|
if (cfg.cache !== false || cfg.storage !== false) {
|
|
134
183
|
const p = load('../providers/CacheStorageServiceProvider');
|
|
135
184
|
if (p) {
|
|
@@ -138,21 +187,53 @@ class AppInitializer {
|
|
|
138
187
|
}
|
|
139
188
|
}
|
|
140
189
|
|
|
190
|
+
// ── 6. Mail ──────────────────────────────────────────────────────────
|
|
141
191
|
if (cfg.mail !== false) {
|
|
142
192
|
const p = load('../providers/MailServiceProvider');
|
|
143
193
|
if (p) providers.push(p);
|
|
144
194
|
}
|
|
195
|
+
|
|
196
|
+
// ── 7. Queue ─────────────────────────────────────────────────────────
|
|
145
197
|
if (cfg.queue !== false) {
|
|
146
198
|
const p = load('../providers/QueueServiceProvider');
|
|
147
199
|
if (p) providers.push(p);
|
|
148
200
|
}
|
|
201
|
+
|
|
202
|
+
// ── 8. Events ────────────────────────────────────────────────────────
|
|
149
203
|
if (cfg.events !== false) {
|
|
150
204
|
const p = load('../providers/EventServiceProvider');
|
|
151
205
|
if (p) providers.push(p);
|
|
152
206
|
}
|
|
153
207
|
|
|
208
|
+
// ── 9. i18n — opt-in via config/app.js use_i18n: true ───────────────
|
|
209
|
+
// Mirrors Django's USE_I18N = True in settings.py.
|
|
210
|
+
// Booted last so translations are available in all request handlers.
|
|
211
|
+
if (this._resolveI18nEnabled(cfg)) {
|
|
212
|
+
const p = load('../i18n/I18nServiceProvider');
|
|
213
|
+
if (p) providers.push(p);
|
|
214
|
+
}
|
|
215
|
+
|
|
154
216
|
return providers;
|
|
155
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resolve whether i18n should be enabled.
|
|
221
|
+
* Reads use_i18n from config/app.js — the single source of truth.
|
|
222
|
+
*
|
|
223
|
+
* // config/app.js
|
|
224
|
+
* module.exports = {
|
|
225
|
+
* use_i18n: true,
|
|
226
|
+
* locale: 'sw',
|
|
227
|
+
* fallback: 'en',
|
|
228
|
+
* };
|
|
229
|
+
*/
|
|
230
|
+
_resolveI18nEnabled(cfg) {
|
|
231
|
+
try {
|
|
232
|
+
const basePath = cfg.basePath || process.cwd();
|
|
233
|
+
const appConfig = require(basePath + '/config/app');
|
|
234
|
+
return appConfig.use_i18n === true;
|
|
235
|
+
} catch { return false; }
|
|
236
|
+
}
|
|
156
237
|
}
|
|
157
238
|
|
|
158
239
|
module.exports = AppInitializer;
|
|
@@ -98,6 +98,22 @@ class Application {
|
|
|
98
98
|
await this._providers.boot();
|
|
99
99
|
this._booted = true;
|
|
100
100
|
|
|
101
|
+
// Wire cache, db, and storage into AI manager now that providers are booted
|
|
102
|
+
try {
|
|
103
|
+
const ai = this._container.make('ai');
|
|
104
|
+
if (ai) {
|
|
105
|
+
try { const cache = this._container.make('cache'); if (cache) ai.setCache(cache); } catch {}
|
|
106
|
+
try { const db = this._container.make('db'); if (db) ai.setDb(db); } catch {}
|
|
107
|
+
try { const store = this._container.make('storage'); if (store) ai.setStorage(store); } catch {}
|
|
108
|
+
// Attach files and stores API as properties
|
|
109
|
+
try {
|
|
110
|
+
const { AIFilesAPI, AIStoresAPI } = require('../ai/files');
|
|
111
|
+
if (!ai.files) Object.defineProperty(ai, 'files', { get: () => new AIFilesAPI(ai), configurable: true });
|
|
112
|
+
if (!ai.stores) Object.defineProperty(ai, 'stores', { get: () => new AIStoresAPI(ai), configurable: true });
|
|
113
|
+
} catch {}
|
|
114
|
+
}
|
|
115
|
+
} catch { /* ai not registered — skip */ }
|
|
116
|
+
|
|
101
117
|
this._emitSync('platform.booted', {providers: this._providers.list()});
|
|
102
118
|
|
|
103
119
|
return this;
|
|
@@ -287,9 +303,23 @@ class Application {
|
|
|
287
303
|
});
|
|
288
304
|
this._container.instance('url', urlGenerator);
|
|
289
305
|
|
|
306
|
+
const { HashManager } = require('../hashing/Hash');
|
|
307
|
+
const hashManager = new HashManager({ default: 'bcrypt', bcrypt: { rounds: 12 } });
|
|
308
|
+
this._container.instance('hash', hashManager);
|
|
309
|
+
|
|
310
|
+
const ProcessManager = require('../process/Process').ProcessManager;
|
|
311
|
+
this._container.instance('process', new ProcessManager());
|
|
312
|
+
|
|
313
|
+
const { AIManager } = require('../ai/AIManager');
|
|
314
|
+
const basePath = (() => { try { return this._container.make('basePath'); } catch { return process.cwd(); } })();
|
|
315
|
+
let aiConfig = { default: process.env.AI_PROVIDER || 'anthropic', providers: {} };
|
|
316
|
+
try { aiConfig = require(basePath + '/config/ai'); } catch { /* no config — use env vars */ }
|
|
317
|
+
const aiManager = new AIManager(aiConfig);
|
|
318
|
+
this._container.instance('ai', aiManager);
|
|
319
|
+
|
|
290
320
|
|
|
291
321
|
this._mwRegistry.register('cors', new CorsMiddleware());
|
|
292
|
-
this._mwRegistry.register('throttle',
|
|
322
|
+
this._mwRegistry.register('throttle', ThrottleMiddleware);
|
|
293
323
|
this._mwRegistry.register('log', new LogMiddleware());
|
|
294
324
|
this._mwRegistry.register('auth', AuthMiddleware);
|
|
295
325
|
}
|
|
@@ -30,12 +30,19 @@ const MillasConfig = require('./MillasConfig');
|
|
|
30
30
|
const Millas = {
|
|
31
31
|
/**
|
|
32
32
|
* Start building an application config.
|
|
33
|
-
*
|
|
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
|
-
|
|
38
|
-
return new MillasConfig();
|
|
44
|
+
configure(basePath) {
|
|
45
|
+
return new MillasConfig().configure(basePath);
|
|
39
46
|
},
|
|
40
47
|
};
|
|
41
48
|
|