millas 0.2.23 → 0.2.24
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/Admin.js +21 -5
- package/src/cli.js +4 -0
- package/src/commands/queue.js +1 -2
- package/src/commands/route.js +1 -1
- package/src/commands/schedule.js +176 -0
- package/src/container/AppInitializer.js +76 -2
- package/src/container/HttpServer.js +18 -10
- package/src/core/foundation.js +2 -0
- package/src/core/scheduler.js +8 -0
- package/src/errors/ErrorRenderer.js +72 -4
- package/src/facades/Database.js +39 -50
- package/src/facades/Schedule.js +22 -0
- package/src/http/SecurityBootstrap.js +15 -4
- package/src/http/middleware/AllowedHostsMiddleware.js +97 -0
- package/src/migrations/system/0004_scheduler_locks.js +31 -0
- package/src/orm/drivers/DatabaseManager.js +82 -0
- package/src/orm/query/QueryBuilder.js +15 -0
- package/src/providers/DatabaseServiceProvider.js +2 -2
- package/src/scheduler/SchedulerLock.js +93 -0
- package/src/scheduler/SchedulerServiceProvider.js +55 -0
- package/src/scheduler/TaskScheduler.js +382 -0
- package/src/scheduler/index.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "millas",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.24",
|
|
4
4
|
"description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
"nodemailer": "^6.9.0",
|
|
52
52
|
"nunjucks": "^3.2.4",
|
|
53
53
|
"ora": "5.4.1",
|
|
54
|
-
"sqlite3": "5.1.6"
|
|
54
|
+
"sqlite3": "5.1.6",
|
|
55
|
+
"node-cron": "^4.2.1"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"express": "^4.18.0",
|
package/src/admin/Admin.js
CHANGED
|
@@ -104,11 +104,27 @@ class Admin {
|
|
|
104
104
|
|
|
105
105
|
_setupNunjucks(expressApp) {
|
|
106
106
|
const viewsDir = path.join(__dirname, 'views');
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
107
|
+
|
|
108
|
+
// Check if nunjucks is already configured by the main app
|
|
109
|
+
const existingEnv = expressApp.get('nunjucksEnvironment');
|
|
110
|
+
let env;
|
|
111
|
+
|
|
112
|
+
if (existingEnv) {
|
|
113
|
+
// Create a new environment that includes both search paths
|
|
114
|
+
const mainAppViewsDir = expressApp.get('views');
|
|
115
|
+
env = nunjucks.configure([viewsDir, mainAppViewsDir], {
|
|
116
|
+
autoescape: true,
|
|
117
|
+
express: expressApp,
|
|
118
|
+
noCache: process.env.NODE_ENV !== 'production',
|
|
119
|
+
});
|
|
120
|
+
} else {
|
|
121
|
+
env = nunjucks.configure(viewsDir, {
|
|
122
|
+
autoescape: true,
|
|
123
|
+
express: expressApp,
|
|
124
|
+
noCache: process.env.NODE_ENV !== 'production',
|
|
125
|
+
});
|
|
126
|
+
expressApp.set('nunjucksEnvironment', env);
|
|
127
|
+
}
|
|
112
128
|
|
|
113
129
|
const resolveFkSlug = (tableName) => {
|
|
114
130
|
if (!tableName) return null;
|
package/src/cli.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// Load .env before anything else so all commands have access to env vars
|
|
4
4
|
require('dotenv').config();
|
|
5
5
|
|
|
6
|
+
// Set CLI mode globally for all commands
|
|
7
|
+
process.env.MILLAS_CLI_MODE = 'true';
|
|
8
|
+
|
|
6
9
|
const { Command } = require('commander');
|
|
7
10
|
const chalk = require('chalk');
|
|
8
11
|
const program = new Command();
|
|
@@ -20,6 +23,7 @@ require('./commands/make')(program);
|
|
|
20
23
|
require('./commands/migrate')(program);
|
|
21
24
|
require('./commands/route')(program);
|
|
22
25
|
require('./commands/queue')(program);
|
|
26
|
+
require('./commands/schedule')(program);
|
|
23
27
|
require('./commands/createsuperuser')(program);
|
|
24
28
|
require('./commands/lang')(program);
|
|
25
29
|
require('./commands/key')(program);
|
package/src/commands/queue.js
CHANGED
|
@@ -26,8 +26,7 @@ module.exports = function (program) {
|
|
|
26
26
|
process.env.MILLAS_ROUTE_LIST = 'true'; // suppress server listen
|
|
27
27
|
let app;
|
|
28
28
|
try {
|
|
29
|
-
|
|
30
|
-
app = bootstrap.app;
|
|
29
|
+
app = await require(bootstrapPath);
|
|
31
30
|
} catch (e) {
|
|
32
31
|
console.error(chalk.red(`\n ✖ Failed to load app: ${e.message}\n`));
|
|
33
32
|
process.exit(1);
|
package/src/commands/route.js
CHANGED
|
@@ -20,7 +20,7 @@ module.exports = function (program) {
|
|
|
20
20
|
|
|
21
21
|
let route;
|
|
22
22
|
try {
|
|
23
|
-
const bootstrap = require(bootstrapPath);
|
|
23
|
+
const bootstrap = await require(bootstrapPath);
|
|
24
24
|
route = bootstrap.route;
|
|
25
25
|
} catch (err) {
|
|
26
26
|
console.error(chalk.red(`\n ✖ Failed to load routes: ${err.message}\n`));
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
|
|
7
|
+
module.exports = function (program) {
|
|
8
|
+
|
|
9
|
+
program
|
|
10
|
+
.command('schedule:list')
|
|
11
|
+
.description('Show all scheduled tasks and their next run times')
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
14
|
+
if (!fs.existsSync(bootstrapPath)) {
|
|
15
|
+
console.error(chalk.red('\n ✖ Not inside a Millas project.\n'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Boot the app to get scheduler
|
|
20
|
+
let app;
|
|
21
|
+
try {
|
|
22
|
+
app = await require(bootstrapPath);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
console.error(chalk.red(`\n ✖ Failed to load app: ${e.message}\n`));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const scheduler = app.make('scheduler');
|
|
29
|
+
const tasks = scheduler.getTasks();
|
|
30
|
+
|
|
31
|
+
console.log();
|
|
32
|
+
if (tasks.length === 0) {
|
|
33
|
+
console.log(chalk.gray(' No scheduled tasks found.'));
|
|
34
|
+
console.log(chalk.gray(' Create routes/schedule.js to define scheduled tasks.'));
|
|
35
|
+
} else {
|
|
36
|
+
console.log(chalk.bold(' Scheduled Tasks\n'));
|
|
37
|
+
|
|
38
|
+
for (const task of tasks) {
|
|
39
|
+
const status = task.isRunning() ? chalk.yellow('RUNNING') : chalk.green('READY');
|
|
40
|
+
const nextRun = task.lastRun
|
|
41
|
+
? `Last: ${task.lastRun.toLocaleString()}`
|
|
42
|
+
: chalk.gray('Never run');
|
|
43
|
+
|
|
44
|
+
console.log(` ${chalk.cyan(task.jobClass.name.padEnd(30))} ${status.padEnd(15)} ${nextRun}`);
|
|
45
|
+
|
|
46
|
+
if (task.cronExpression) {
|
|
47
|
+
console.log(` ${chalk.gray('Cron:')} ${task.cronExpression}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (Object.keys(task.parameters).length > 0) {
|
|
51
|
+
console.log(` ${chalk.gray('Params:')} ${JSON.stringify(task.parameters)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (task.failures.length > 0) {
|
|
55
|
+
const lastFailure = task.failures[task.failures.length - 1];
|
|
56
|
+
console.log(` ${chalk.red('Last failure:')} ${lastFailure.error} (${lastFailure.timestamp.toLocaleString()})`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
process.exit(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
program
|
|
67
|
+
.command('schedule:test <taskName>')
|
|
68
|
+
.description('Run a specific scheduled task immediately for testing')
|
|
69
|
+
.action(async (taskName) => {
|
|
70
|
+
const bootstrapPath = path.resolve(process.cwd(), 'bootstrap/app.js');
|
|
71
|
+
if (!fs.existsSync(bootstrapPath)) {
|
|
72
|
+
console.error(chalk.red('\n ✖ Not inside a Millas project.\n'));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Boot the app
|
|
77
|
+
let app;
|
|
78
|
+
try {
|
|
79
|
+
app = await require(bootstrapPath);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.error(chalk.red(`\n ✖ Failed to load app: ${e.message}\n`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const scheduler = app.make('scheduler');
|
|
86
|
+
const tasks = scheduler.getTasks();
|
|
87
|
+
const task = tasks.find(t => t.jobClass.name === taskName);
|
|
88
|
+
|
|
89
|
+
if (!task) {
|
|
90
|
+
console.error(chalk.red(`\n ✖ Task "${taskName}" not found.\n`));
|
|
91
|
+
console.log(chalk.gray(' Available tasks:'));
|
|
92
|
+
tasks.forEach(t => console.log(chalk.gray(` - ${t.jobClass.name}`)));
|
|
93
|
+
console.log();
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log(chalk.blue(`\n ▶ Running ${taskName}...\n`));
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await scheduler._executeTask(task, new Date());
|
|
101
|
+
console.log(chalk.green(` ✔ ${taskName} completed successfully.\n`));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(chalk.red(` ✖ ${taskName} failed: ${error.message}\n`));
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
process.exit(0);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
program
|
|
111
|
+
.command('make:task <name>')
|
|
112
|
+
.description('Generate a new scheduled task class')
|
|
113
|
+
.action(async (name) => {
|
|
114
|
+
const taskName = name.endsWith('Task') ? name : `${name}Task`;
|
|
115
|
+
const taskPath = path.resolve(process.cwd(), 'app', 'tasks', `${taskName}.js`);
|
|
116
|
+
|
|
117
|
+
// Ensure directory exists
|
|
118
|
+
await fs.ensureDir(path.dirname(taskPath));
|
|
119
|
+
|
|
120
|
+
// Check if file already exists
|
|
121
|
+
if (await fs.pathExists(taskPath)) {
|
|
122
|
+
console.error(chalk.red(`\n ✖ Task ${taskName} already exists.\n`));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Generate task class
|
|
127
|
+
const template = `'use strict';
|
|
128
|
+
|
|
129
|
+
const { Job } = require('millas/core/queue');
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* ${taskName}
|
|
133
|
+
*
|
|
134
|
+
* Scheduled task that runs automatically based on the schedule defined in routes/schedule.js
|
|
135
|
+
*
|
|
136
|
+
* Usage in routes/schedule.js:
|
|
137
|
+
* Schedule.job(${taskName}).daily().at('09:00');
|
|
138
|
+
*/
|
|
139
|
+
class ${taskName} extends Job {
|
|
140
|
+
/**
|
|
141
|
+
* Constructor - DI container will inject dependencies automatically
|
|
142
|
+
*/
|
|
143
|
+
constructor(/* inject dependencies here */) {
|
|
144
|
+
super();
|
|
145
|
+
// Store injected dependencies
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Execute the scheduled task
|
|
150
|
+
*/
|
|
151
|
+
async handle() {
|
|
152
|
+
// Implement your scheduled task logic here
|
|
153
|
+
console.log('${taskName} is running...');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handle task failure (optional)
|
|
158
|
+
*/
|
|
159
|
+
async failed(error) {
|
|
160
|
+
console.error('${taskName} failed:', error.message);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = ${taskName};
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
await fs.writeFile(taskPath, template);
|
|
168
|
+
|
|
169
|
+
console.log(chalk.green(`\n ✔ Task created: ${taskPath}`));
|
|
170
|
+
console.log(chalk.gray('\n Next steps:'));
|
|
171
|
+
console.log(chalk.gray(` 1. Implement the handle() method in ${taskName}`));
|
|
172
|
+
console.log(chalk.gray(` 2. Add the task to routes/schedule.js:`));
|
|
173
|
+
console.log(chalk.gray(` Schedule.job(${taskName}).daily().at('09:00');`));
|
|
174
|
+
console.log();
|
|
175
|
+
});
|
|
176
|
+
};
|
|
@@ -58,6 +58,7 @@ class AppInitializer {
|
|
|
58
58
|
if (!process.env.MILLAS_CLI_MODE) {
|
|
59
59
|
await this._serve();
|
|
60
60
|
}
|
|
61
|
+
|
|
61
62
|
return this._kernel;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -107,6 +108,12 @@ class AppInitializer {
|
|
|
107
108
|
|
|
108
109
|
this._kernel._container.instance('basePath', basePath);
|
|
109
110
|
|
|
111
|
+
// ── View engine ─────────────────────────────────────────────────────────────────
|
|
112
|
+
// Auto-configure Nunjucks as the default template engine.
|
|
113
|
+
// Looks for views/ in the project root. Zero config required.
|
|
114
|
+
// Disable via config/app.js: { views: false }
|
|
115
|
+
this._setupViewEngine(expressApp, basePath, appConfig);
|
|
116
|
+
|
|
110
117
|
// ── Static file serving ───────────────────────────────────────────────
|
|
111
118
|
// Auto-serve each storage disk that has a baseUrl configured.
|
|
112
119
|
// Mirrors Laravel's public disk serving — zero config required.
|
|
@@ -182,6 +189,65 @@ class AppInitializer {
|
|
|
182
189
|
|
|
183
190
|
// ── Internal ───────────────────────────────────────────────────────────────
|
|
184
191
|
|
|
192
|
+
_setupViewEngine(expressApp, basePath, appConfig) {
|
|
193
|
+
const path = require('path');
|
|
194
|
+
const fs = require('fs');
|
|
195
|
+
|
|
196
|
+
// Opt-out: views: false in config/app.js
|
|
197
|
+
if (appConfig.views === false) return;
|
|
198
|
+
|
|
199
|
+
const viewsConfig = appConfig.views || {};
|
|
200
|
+
const viewsDir = path.isAbsolute(viewsConfig.path || '')
|
|
201
|
+
? viewsConfig.path
|
|
202
|
+
: path.join(basePath, viewsConfig.path || 'resources/views');
|
|
203
|
+
|
|
204
|
+
// Don't configure if views directory doesn't exist
|
|
205
|
+
if (!fs.existsSync(viewsDir)) return;
|
|
206
|
+
|
|
207
|
+
// Serve public/ directory for CSS, JS, images used in views
|
|
208
|
+
const publicDir = path.join(basePath, viewsConfig.public || 'public');
|
|
209
|
+
if (fs.existsSync(publicDir)) {
|
|
210
|
+
expressApp.use(express.static(publicDir));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const engine = viewsConfig.engine || 'nunjucks';
|
|
214
|
+
|
|
215
|
+
if (engine === 'nunjucks') {
|
|
216
|
+
// nunjucks is a core Millas dependency — always available
|
|
217
|
+
const nunjucks = require('nunjucks');
|
|
218
|
+
|
|
219
|
+
// Check if nunjucks is already configured for this express app
|
|
220
|
+
const existingEnv = expressApp.get('nunjucksEnvironment');
|
|
221
|
+
let env;
|
|
222
|
+
|
|
223
|
+
if (existingEnv) {
|
|
224
|
+
// Admin panel will reconfigure with multiple search paths
|
|
225
|
+
env = existingEnv;
|
|
226
|
+
} else {
|
|
227
|
+
env = nunjucks.configure(viewsDir, {
|
|
228
|
+
autoescape: viewsConfig.autoescape ?? true,
|
|
229
|
+
watch: viewsConfig.watch ?? (process.env.NODE_ENV !== 'production'),
|
|
230
|
+
noCache: viewsConfig.noCache ?? (process.env.NODE_ENV !== 'production'),
|
|
231
|
+
throwOnUndefined: viewsConfig.throwOnUndefined ?? false,
|
|
232
|
+
express: expressApp,
|
|
233
|
+
});
|
|
234
|
+
// Store reference for later use
|
|
235
|
+
expressApp.set('nunjucksEnvironment', env);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Support both .html and .njk extensions
|
|
239
|
+
expressApp.set('view engine', 'html');
|
|
240
|
+
expressApp.engine('html', env.render.bind(env));
|
|
241
|
+
expressApp.engine('njk', env.render.bind(env));
|
|
242
|
+
expressApp.set('views', viewsDir);
|
|
243
|
+
|
|
244
|
+
} else {
|
|
245
|
+
// Custom engine — user must configure it themselves via .use()
|
|
246
|
+
expressApp.set('views', viewsDir);
|
|
247
|
+
expressApp.set('view engine', engine);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
185
251
|
_serveStorageDisks(expressApp, basePath) {
|
|
186
252
|
const express = require('express');
|
|
187
253
|
const path = require('path');
|
|
@@ -286,10 +352,18 @@ class AppInitializer {
|
|
|
286
352
|
if (p) providers.push(p);
|
|
287
353
|
}
|
|
288
354
|
|
|
289
|
-
// ── 9.
|
|
355
|
+
// ── 9. Scheduler — always on unless explicitly disabled ──────────────
|
|
356
|
+
// Built-in task scheduler that runs alongside the HTTP server.
|
|
357
|
+
// Automatically loads and executes scheduled tasks from routes/schedule.js.
|
|
358
|
+
if (cfg.scheduler !== false) {
|
|
359
|
+
const p = load('../scheduler/SchedulerServiceProvider');
|
|
360
|
+
if (p) providers.push(p);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ── 10. i18n — opt-in via config/app.js use_i18n: true ──────────────
|
|
290
364
|
// Mirrors Django's USE_I18N = True in settings.py.
|
|
291
365
|
// Booted last so translations are available in all request handlers.
|
|
292
|
-
// ──
|
|
366
|
+
// ── 10. Encryption — always on (APP_KEY drives it) ────────────────────
|
|
293
367
|
// Mirrors Laravel: the encrypter is always bound so Crypt / Encrypt
|
|
294
368
|
// facades work out of the box. If APP_KEY is absent a clear error is
|
|
295
369
|
// thrown on first use, not at boot — apps without encryption still start.
|
|
@@ -134,17 +134,25 @@ class HttpServer {
|
|
|
134
134
|
|
|
135
135
|
_handleSignals() {
|
|
136
136
|
const shutdown = async (signal) => {
|
|
137
|
+
process.stdout.write(`\n Shutting down (${signal})…\n`);
|
|
138
|
+
|
|
139
|
+
if (typeof this._options.onShutdown === 'function') {
|
|
140
|
+
try {
|
|
141
|
+
await this._options.onShutdown();
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error('Shutdown error:', err.message);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Stop scheduler if it exists
|
|
148
|
+
try {
|
|
149
|
+
const scheduler = this._app._container.make('scheduler');
|
|
150
|
+
await scheduler.stop();
|
|
151
|
+
} catch {
|
|
152
|
+
// Scheduler not registered or already stopped
|
|
153
|
+
}
|
|
154
|
+
|
|
137
155
|
process.exit(0);
|
|
138
|
-
// process.stdout.write(`\n Shutting down (${signal})…\n`);
|
|
139
|
-
//
|
|
140
|
-
// if (typeof this._options.onShutdown === 'function') {
|
|
141
|
-
// try {
|
|
142
|
-
// await this._options.onShutdown();
|
|
143
|
-
// } catch {
|
|
144
|
-
// }
|
|
145
|
-
// }
|
|
146
|
-
//
|
|
147
|
-
// await this._app.shutdown(0);
|
|
148
156
|
};
|
|
149
157
|
|
|
150
158
|
process.once('SIGTERM', () => shutdown("SIGTERM"));
|
package/src/core/foundation.js
CHANGED
|
@@ -55,11 +55,12 @@ class ErrorRenderer {
|
|
|
55
55
|
* Render an error to the response — JSON or HTML based on Accept header.
|
|
56
56
|
*/
|
|
57
57
|
static render(err, req, res) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
try {
|
|
59
|
+
const status = err.status || err.statusCode || 500;
|
|
60
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
61
|
+
const wantsHtml = ErrorRenderer._wantsHtml(req);
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
res.status(status);
|
|
63
64
|
|
|
64
65
|
if (!wantsHtml) {
|
|
65
66
|
// ── JSON response ──────────────────────────────────────────────────────
|
|
@@ -73,6 +74,31 @@ class ErrorRenderer {
|
|
|
73
74
|
return res.json(body);
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
|
|
78
|
+
// Check for custom error view in resources/views/errors/
|
|
79
|
+
// e.g. resources/views/errors/404.html, resources/views/errors/500.html
|
|
80
|
+
const customView = ErrorRenderer._findCustomErrorView(status);
|
|
81
|
+
if (customView) {
|
|
82
|
+
try {
|
|
83
|
+
return res.render(customView, {
|
|
84
|
+
status,
|
|
85
|
+
message: err.message,
|
|
86
|
+
error: err,
|
|
87
|
+
isDev,
|
|
88
|
+
statusTitle: ErrorRenderer._statusTitle(status),
|
|
89
|
+
}, (renderErr, html) => {
|
|
90
|
+
if (renderErr) {
|
|
91
|
+
// View engine error — fall back to built-in pages
|
|
92
|
+
if (!isDev || status < 500 && !err._forceDebug) {
|
|
93
|
+
return res.send(ErrorRenderer._renderSimple(status, err.message));
|
|
94
|
+
}
|
|
95
|
+
return res.send(ErrorRenderer._renderDebug(err, req, status));
|
|
96
|
+
}
|
|
97
|
+
res.send(html);
|
|
98
|
+
});
|
|
99
|
+
} catch { /* view engine not configured — fall through */ }
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
if (!isDev || status < 500 && !err._forceDebug) {
|
|
77
103
|
// ── Production / 4xx HTML ──────────────────────────────────────────────
|
|
78
104
|
return res.send(ErrorRenderer._renderSimple(status, err.message));
|
|
@@ -80,10 +106,32 @@ class ErrorRenderer {
|
|
|
80
106
|
|
|
81
107
|
// ── Development HTML — full debug page ────────────────────────────────────
|
|
82
108
|
res.send(ErrorRenderer._renderDebug(err, req, status));
|
|
109
|
+
}catch (e){
|
|
110
|
+
console.log(e)
|
|
111
|
+
res.send("something went wrong")
|
|
112
|
+
}
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
// ─── HTML renderers ────────────────────────────────────────────────────────
|
|
86
116
|
|
|
117
|
+
static _findCustomErrorView(status) {
|
|
118
|
+
const fs = require('fs');
|
|
119
|
+
const path = require('path');
|
|
120
|
+
const viewsDir = path.join(process.cwd(), 'resources', 'views', 'errors');
|
|
121
|
+
const candidates = [
|
|
122
|
+
path.join(viewsDir, `${status}.html`),
|
|
123
|
+
path.join(viewsDir, `${status}.njk`),
|
|
124
|
+
path.join(viewsDir, 'error.html'),
|
|
125
|
+
path.join(viewsDir, 'error.njk'),
|
|
126
|
+
];
|
|
127
|
+
for (const candidate of candidates) {
|
|
128
|
+
if (fs.existsSync(candidate)) {
|
|
129
|
+
return `errors/${require('path').basename(candidate)}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
87
135
|
static _renderSimple(status, message) {
|
|
88
136
|
const title = ErrorRenderer._statusTitle(status);
|
|
89
137
|
return `<!DOCTYPE html>
|
|
@@ -139,6 +187,24 @@ class ErrorRenderer {
|
|
|
139
187
|
'Uptime': `${Math.floor(process.uptime())}s`,
|
|
140
188
|
};
|
|
141
189
|
|
|
190
|
+
// Host details for EINVALIDHOST errors
|
|
191
|
+
const hostDetailsHtml = err._hostDetails ? `
|
|
192
|
+
<!-- Host Details -->
|
|
193
|
+
<div class="section">
|
|
194
|
+
<div class="section-header" onclick="toggle(this)">
|
|
195
|
+
<span>Host Configuration</span>
|
|
196
|
+
<span class="toggle">▾</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="section-body">
|
|
199
|
+
<table class="info-table">
|
|
200
|
+
<tr><td>Received Host</td><td style="font-family:monospace;color:var(--red)">${_esc(err._hostDetails.receivedHost)}</td></tr>
|
|
201
|
+
<tr><td>Allowed Hosts</td><td><pre class="json-val">${_esc(JSON.stringify(err._hostDetails.allowedHosts, null, 2))}</pre></td></tr>
|
|
202
|
+
<tr><td>Environment</td><td>${_esc(err._hostDetails.environment)}</td></tr>
|
|
203
|
+
${err._hostDetails.suggestion ? `<tr><td>Suggestion</td><td style="color:var(--blue)">${_esc(err._hostDetails.suggestion)}</td></tr>` : ''}
|
|
204
|
+
</table>
|
|
205
|
+
</div>
|
|
206
|
+
</div>` : '';
|
|
207
|
+
|
|
142
208
|
return `<!DOCTYPE html>
|
|
143
209
|
<html lang="en">
|
|
144
210
|
<head>
|
|
@@ -466,6 +532,8 @@ class ErrorRenderer {
|
|
|
466
532
|
</div>
|
|
467
533
|
</div>
|
|
468
534
|
|
|
535
|
+
${hostDetailsHtml}
|
|
536
|
+
|
|
469
537
|
${err.errors ? `
|
|
470
538
|
<!-- Validation errors -->
|
|
471
539
|
<div class="section">
|
package/src/facades/Database.js
CHANGED
|
@@ -1,64 +1,53 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const { createFacade } = require('./Facade');
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Database facade —
|
|
6
|
+
* Database facade — Laravel-style DB access.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
+
* const { Database } = require('millas');
|
|
10
|
+
* // or
|
|
9
11
|
* const Database = require('millas/facades/Database');
|
|
10
12
|
*
|
|
11
13
|
* // Raw SQL
|
|
12
|
-
* const
|
|
14
|
+
* const users = await Database.select('SELECT * FROM users WHERE id = ?', [1]);
|
|
15
|
+
* await Database.insert('INSERT INTO users (name, email) VALUES (?, ?)', ['John', 'john@example.com']);
|
|
16
|
+
* await Database.update('UPDATE users SET name = ? WHERE id = ?', ['Jane', 1]);
|
|
17
|
+
* await Database.delete('DELETE FROM users WHERE id = ?', [1]);
|
|
13
18
|
*
|
|
14
|
-
* //
|
|
15
|
-
* const
|
|
19
|
+
* // Query Builder (most common)
|
|
20
|
+
* const users = await Database.table('users').get();
|
|
21
|
+
* const user = await Database.table('users').where('id', 1).first();
|
|
22
|
+
* await Database.table('users').insert({ name: 'John', email: 'john@example.com' });
|
|
23
|
+
* await Database.table('users').where('id', 1).update({ name: 'Jane' });
|
|
24
|
+
* await Database.table('users').where('id', 1).delete();
|
|
16
25
|
*
|
|
17
|
-
* //
|
|
18
|
-
*
|
|
26
|
+
* // Transactions
|
|
27
|
+
* await Database.transaction(async (trx) => {
|
|
28
|
+
* await trx.table('accounts').update({ balance: 100 });
|
|
29
|
+
* await trx.table('transactions').insert({ amount: 100 });
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Multiple connections
|
|
33
|
+
* await Database.connection('mysql').table('users').get();
|
|
34
|
+
*
|
|
35
|
+
* // Raw expressions
|
|
36
|
+
* await Database.table('users').select(Database.raw('COUNT(*) as total')).first();
|
|
37
|
+
*
|
|
38
|
+
* @class
|
|
39
|
+
* @property {function(string): *} table - Query builder for a table
|
|
40
|
+
* @property {function(string, array=): Promise<*>} raw - Execute raw SQL
|
|
41
|
+
* @property {function(string, array=): Promise<*>} select - SELECT query
|
|
42
|
+
* @property {function(string, array=): Promise<*>} insert - INSERT query
|
|
43
|
+
* @property {function(string, array=): Promise<*>} update - UPDATE query
|
|
44
|
+
* @property {function(string, array=): Promise<*>} delete - DELETE query
|
|
45
|
+
* @property {function(function): Promise<*>} transaction - Run queries in transaction
|
|
46
|
+
* @property {function(string=): *} connection - Get named connection
|
|
47
|
+
*
|
|
48
|
+
* @see src/orm/drivers/DatabaseManager.js
|
|
19
49
|
*/
|
|
20
|
-
class Database {
|
|
21
|
-
static _resolveInstance() {
|
|
22
|
-
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
23
|
-
return DatabaseManager.connection();
|
|
24
|
-
}
|
|
50
|
+
class Database extends createFacade('db') {
|
|
25
51
|
}
|
|
26
52
|
|
|
27
|
-
|
|
28
|
-
module.exports = new Proxy(Database, {
|
|
29
|
-
get(target, prop) {
|
|
30
|
-
// Let real static members through
|
|
31
|
-
if (prop in target || prop === 'then' || prop === 'catch') {
|
|
32
|
-
return target[prop];
|
|
33
|
-
}
|
|
34
|
-
if (typeof prop === 'symbol') return target[prop];
|
|
35
|
-
|
|
36
|
-
// Special case: raw() — normalize result across dialects
|
|
37
|
-
if (prop === 'raw') {
|
|
38
|
-
return async (sql, bindings) => {
|
|
39
|
-
const db = Database._resolveInstance();
|
|
40
|
-
const result = await db.raw(sql, bindings);
|
|
41
|
-
// Postgres returns { rows: [...], command, rowCount, ... }
|
|
42
|
-
// SQLite/MySQL return [rows, fields] or just rows
|
|
43
|
-
if (result && result.rows) return result.rows;
|
|
44
|
-
if (Array.isArray(result)) return result[0] ?? result;
|
|
45
|
-
return result;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Special case: connection(name) returns a named knex instance
|
|
50
|
-
if (prop === 'connection') {
|
|
51
|
-
return (name) => {
|
|
52
|
-
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
53
|
-
return DatabaseManager.connection(name || null);
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Proxy everything else to the default knex connection
|
|
58
|
-
return (...args) => {
|
|
59
|
-
const db = Database._resolveInstance();
|
|
60
|
-
if (typeof db[prop] !== 'function') return db[prop];
|
|
61
|
-
return db[prop](...args);
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
});
|
|
53
|
+
module.exports = Database;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Facade = require('./Facade');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Schedule Facade
|
|
7
|
+
*
|
|
8
|
+
* Provides easy access to the task scheduler for defining scheduled tasks.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { Schedule } = require('millas/facades');
|
|
12
|
+
*
|
|
13
|
+
* Schedule.job(SendEmailJob).daily().at('09:00');
|
|
14
|
+
* Schedule.job(CleanupJob).hourly();
|
|
15
|
+
*/
|
|
16
|
+
class Schedule extends Facade {
|
|
17
|
+
static getAccessor() {
|
|
18
|
+
return 'scheduler';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = Schedule;
|