millas 0.2.11 → 0.2.12-beta
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 +17 -3
- package/src/auth/AuthController.js +42 -133
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +266 -37
- package/src/container/Application.js +88 -8
- package/src/controller/Controller.js +79 -300
- package/src/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +46 -0
- package/src/facades/Cache.js +17 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +24 -0
- package/src/facades/Http.js +54 -0
- package/src/facades/Log.js +56 -0
- package/src/facades/Mail.js +40 -0
- package/src/facades/Queue.js +23 -0
- package/src/facades/Storage.js +17 -0
- package/src/facades/Validation.js +69 -0
- package/src/http/MillasRequest.js +253 -0
- package/src/http/MillasResponse.js +196 -0
- package/src/http/RequestContext.js +176 -0
- package/src/http/ResponseDispatcher.js +144 -0
- package/src/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +55 -2
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +135 -0
- package/src/middleware/CorsMiddleware.js +22 -30
- package/src/middleware/LogMiddleware.js +27 -59
- package/src/middleware/Middleware.js +24 -15
- package/src/middleware/MiddlewarePipeline.js +30 -67
- package/src/middleware/MiddlewareRegistry.js +126 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +7 -3
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- package/src/providers/LogServiceProvider.js +88 -18
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +155 -223
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +13 -12
- package/src/validation/BaseValidator.js +193 -0
- package/src/validation/Validator.js +680 -0
package/src/index.js
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
// ── HTTP Layer ────────────────────────────────────────────────────
|
|
4
|
+
const {
|
|
5
|
+
MillasRequest,
|
|
6
|
+
MillasResponse,
|
|
7
|
+
ResponseDispatcher,
|
|
8
|
+
jsonify,
|
|
9
|
+
view,
|
|
10
|
+
redirect,
|
|
11
|
+
text,
|
|
12
|
+
file,
|
|
13
|
+
empty,
|
|
14
|
+
abort,
|
|
15
|
+
notFound,
|
|
16
|
+
unauthorized,
|
|
17
|
+
forbidden,
|
|
18
|
+
} = require('./http/index');
|
|
19
|
+
const RequestContext = require('./http/RequestContext');
|
|
20
|
+
|
|
21
|
+
// ── Validation ────────────────────────────────────────────────────
|
|
22
|
+
const {
|
|
23
|
+
Validator,
|
|
24
|
+
BaseValidator,
|
|
25
|
+
StringValidator,
|
|
26
|
+
EmailValidator,
|
|
27
|
+
NumberValidator,
|
|
28
|
+
BooleanValidator,
|
|
29
|
+
DateValidator,
|
|
30
|
+
ArrayValidator,
|
|
31
|
+
ObjectValidator,
|
|
32
|
+
FileValidator,
|
|
33
|
+
string,
|
|
34
|
+
email,
|
|
35
|
+
number,
|
|
36
|
+
boolean,
|
|
37
|
+
date,
|
|
38
|
+
array,
|
|
39
|
+
object: objectField,
|
|
40
|
+
file: fileField,
|
|
41
|
+
} = require('./validation/Validator');
|
|
42
|
+
const MillasApp = require('./container/MillasApp');
|
|
43
|
+
// ── HTTP Layer (old) ──────────────────────────────────────────────
|
|
4
44
|
const Controller = require('./controller/Controller');
|
|
5
45
|
const Middleware = require('./middleware/Middleware');
|
|
6
46
|
const MiddlewarePipeline = require('./middleware/MiddlewarePipeline');
|
|
@@ -12,7 +52,6 @@ const HttpError = require('./errors/HttpError');
|
|
|
12
52
|
// ── DI Container ─────────────────────────────────────────────────
|
|
13
53
|
const Container = require('./container/Container');
|
|
14
54
|
const Application = require('./container/Application');
|
|
15
|
-
const MillasApp = require('./container/MillasApp');
|
|
16
55
|
const ServiceProvider = require('./providers/ServiceProvider');
|
|
17
56
|
const ProviderRegistry = require('./providers/ProviderRegistry');
|
|
18
57
|
|
|
@@ -61,11 +100,25 @@ const Storage = require('./storage/Storage');
|
|
|
61
100
|
const LocalDriver = require('./storage/drivers/LocalDriver');
|
|
62
101
|
|
|
63
102
|
module.exports = {
|
|
103
|
+
// Millas App
|
|
104
|
+
MillasApp,
|
|
105
|
+
// ── Millas HTTP layer ──────────────────────────────────────────
|
|
106
|
+
MillasRequest, MillasResponse, ResponseDispatcher, RequestContext,
|
|
107
|
+
jsonify, view, redirect, text, send_file:file, empty,
|
|
108
|
+
abort, notFound, unauthorized, forbidden,
|
|
109
|
+
// ── Validation ────────────────────────────────────────────────
|
|
110
|
+
Validator,
|
|
111
|
+
BaseValidator,
|
|
112
|
+
StringValidator, EmailValidator, NumberValidator, BooleanValidator,
|
|
113
|
+
DateValidator, ArrayValidator, ObjectValidator, FileValidator,
|
|
114
|
+
string, email, number, boolean, date, array,
|
|
115
|
+
object: objectField,
|
|
116
|
+
file: fileField,
|
|
64
117
|
// HTTP
|
|
65
118
|
Controller, Middleware, MiddlewarePipeline,
|
|
66
119
|
CorsMiddleware, ThrottleMiddleware, LogMiddleware, HttpError,
|
|
67
120
|
// DI
|
|
68
|
-
Container, Application,
|
|
121
|
+
Container, Application, ServiceProvider, ProviderRegistry,
|
|
69
122
|
// ORM
|
|
70
123
|
Model, fields, QueryBuilder, DatabaseManager, SchemaBuilder,
|
|
71
124
|
MigrationRunner, ModelInspector, DatabaseServiceProvider,
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* src/logger/internal.js
|
|
5
|
+
*
|
|
6
|
+
* MillasLog — the Millas framework's own internal logger.
|
|
7
|
+
*
|
|
8
|
+
* This is separate from the user-facing `Log` singleton. It is used by:
|
|
9
|
+
* - ORM (queries, relation warnings, migration runner)
|
|
10
|
+
* - Admin panel
|
|
11
|
+
* - Queue worker
|
|
12
|
+
* - Any other framework-internal code
|
|
13
|
+
*
|
|
14
|
+
* It always works — no provider, no config, no try/catch needed at call sites.
|
|
15
|
+
* It starts in a sensible default state (WARN+ to console) and is upgraded
|
|
16
|
+
* by LogServiceProvider once the app boots and config/logging.js is read.
|
|
17
|
+
*
|
|
18
|
+
* ── Usage inside framework code ────────────────────────────────────────────
|
|
19
|
+
*
|
|
20
|
+
* const MillasLog = require('../logger/internal');
|
|
21
|
+
*
|
|
22
|
+
* MillasLog.d('ORM', 'Running query', { table: 'users' });
|
|
23
|
+
* MillasLog.w('ORM', 'Relation not defined', { model: 'Post', name: 'tags' });
|
|
24
|
+
* MillasLog.e('Migration', 'Failed to run migration', error);
|
|
25
|
+
*
|
|
26
|
+
* ── Configuring from config/logging.js ─────────────────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* module.exports = {
|
|
29
|
+
* // ... app channels ...
|
|
30
|
+
*
|
|
31
|
+
* internal: {
|
|
32
|
+
* level: 'debug', // show all ORM/framework logs (default: 'warn')
|
|
33
|
+
* format: 'pretty', // pretty | simple | json
|
|
34
|
+
* },
|
|
35
|
+
* };
|
|
36
|
+
*
|
|
37
|
+
* ── Disabling internal logs entirely ───────────────────────────────────────
|
|
38
|
+
*
|
|
39
|
+
* internal: false
|
|
40
|
+
*
|
|
41
|
+
* ── Writing to a separate file ─────────────────────────────────────────────
|
|
42
|
+
*
|
|
43
|
+
* internal: {
|
|
44
|
+
* level: 'debug',
|
|
45
|
+
* channels: [
|
|
46
|
+
* { driver: 'console', format: 'pretty', level: 'warn' },
|
|
47
|
+
* { driver: 'file', format: 'simple', path: 'storage/logs', prefix: 'millas-internal', level: 'debug' },
|
|
48
|
+
* ],
|
|
49
|
+
* },
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
const Logger = require('./Logger');
|
|
53
|
+
const { LEVELS } = require('./levels');
|
|
54
|
+
const ConsoleChannel = require('./channels/ConsoleChannel');
|
|
55
|
+
const PrettyFormatter = require('./formatters/PrettyFormatter');
|
|
56
|
+
const { NullChannel } = require('./channels/index');
|
|
57
|
+
|
|
58
|
+
// ── Create the MillasLog singleton ───────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
const MillasLog = new Logger();
|
|
61
|
+
|
|
62
|
+
// Default: WARN and above, pretty-formatted to console.
|
|
63
|
+
// This means normal app runs are quiet — you only see warnings and errors
|
|
64
|
+
// from the framework itself unless you opt in to lower levels.
|
|
65
|
+
MillasLog.configure({
|
|
66
|
+
defaultTag: 'Millas',
|
|
67
|
+
minLevel: LEVELS.WARN,
|
|
68
|
+
channel: new ConsoleChannel({
|
|
69
|
+
formatter: new PrettyFormatter({
|
|
70
|
+
colour: process.stdout.isTTY !== false,
|
|
71
|
+
}),
|
|
72
|
+
minLevel: LEVELS.WARN,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
module.exports = MillasLog;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { LEVELS } = require('./levels');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* patchConsole(Log, defaultTag)
|
|
7
|
+
*
|
|
8
|
+
* Replaces the global console.* methods so that all console output
|
|
9
|
+
* in the app goes through the configured Log channels — same formatting,
|
|
10
|
+
* same level filtering, same file output.
|
|
11
|
+
*
|
|
12
|
+
* Safe: ConsoleChannel writes via process.stdout.write / process.stderr.write,
|
|
13
|
+
* never via console.*, so there is no recursion risk.
|
|
14
|
+
*
|
|
15
|
+
* Called once from LogServiceProvider.boot() when interceptConsole is enabled.
|
|
16
|
+
* Returns a restore() function that puts the originals back.
|
|
17
|
+
*
|
|
18
|
+
* Level mapping:
|
|
19
|
+
* console.log → Log.i (INFO)
|
|
20
|
+
* console.info → Log.i (INFO)
|
|
21
|
+
* console.warn → Log.w (WARN)
|
|
22
|
+
* console.error → Log.e (ERROR)
|
|
23
|
+
* console.debug → Log.d (DEBUG)
|
|
24
|
+
* console.trace → Log.v (VERBOSE)
|
|
25
|
+
* console.dir → Log.d (DEBUG)
|
|
26
|
+
*/
|
|
27
|
+
function patchConsole(Log, defaultTag = 'App') {
|
|
28
|
+
// Save originals — restore() puts these back
|
|
29
|
+
const originals = {
|
|
30
|
+
log: console.log.bind(console),
|
|
31
|
+
info: console.info.bind(console),
|
|
32
|
+
warn: console.warn.bind(console),
|
|
33
|
+
error: console.error.bind(console),
|
|
34
|
+
debug: console.debug.bind(console),
|
|
35
|
+
trace: console.trace.bind(console),
|
|
36
|
+
dir: console.dir.bind(console),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Build a dispatcher for a given level
|
|
40
|
+
function make(level) {
|
|
41
|
+
return function (...args) {
|
|
42
|
+
const { message, context, error } = parse(args);
|
|
43
|
+
Log._emit({
|
|
44
|
+
level,
|
|
45
|
+
tag: defaultTag,
|
|
46
|
+
message: message || '',
|
|
47
|
+
context,
|
|
48
|
+
error,
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
pid: process.pid,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log = make(LEVELS.INFO);
|
|
56
|
+
console.info = make(LEVELS.INFO);
|
|
57
|
+
console.warn = make(LEVELS.WARN);
|
|
58
|
+
console.error = make(LEVELS.ERROR);
|
|
59
|
+
console.debug = make(LEVELS.DEBUG);
|
|
60
|
+
console.trace = make(LEVELS.VERBOSE);
|
|
61
|
+
console.dir = (obj) => Log._emit({ level: LEVELS.DEBUG, tag: defaultTag, message: '', context: obj, error: undefined, timestamp: new Date().toISOString(), pid: process.pid });
|
|
62
|
+
|
|
63
|
+
return function restore() {
|
|
64
|
+
Object.assign(console, originals);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Argument parser ───────────────────────────────────────────────────────────
|
|
69
|
+
//
|
|
70
|
+
// Handles the main shapes people pass to console.*:
|
|
71
|
+
//
|
|
72
|
+
// console.log('message')
|
|
73
|
+
// console.log('message', { ctx })
|
|
74
|
+
// console.log('message', error)
|
|
75
|
+
// console.error(error)
|
|
76
|
+
// console.log({ obj })
|
|
77
|
+
// console.log('x:', 42) → message: 'x: 42'
|
|
78
|
+
// console.log('a', 'b', 'c') → message: 'a b c'
|
|
79
|
+
|
|
80
|
+
function parse(args) {
|
|
81
|
+
if (args.length === 0) {
|
|
82
|
+
return { message: '', context: undefined, error: undefined };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (args.length === 1) {
|
|
86
|
+
const a = args[0];
|
|
87
|
+
if (a instanceof Error) return { message: a.message, context: undefined, error: a };
|
|
88
|
+
if (typeof a === 'object' && a !== null) return { message: '', context: a, error: undefined };
|
|
89
|
+
return { message: String(a), context: undefined, error: undefined };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const [first, ...rest] = args;
|
|
93
|
+
|
|
94
|
+
// First arg is an Error
|
|
95
|
+
if (first instanceof Error) {
|
|
96
|
+
return { message: first.message, context: rest.length ? rest : undefined, error: first };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// First arg is a string message
|
|
100
|
+
if (typeof first === 'string') {
|
|
101
|
+
// Single extra arg
|
|
102
|
+
if (rest.length === 1) {
|
|
103
|
+
const r = rest[0];
|
|
104
|
+
if (r instanceof Error) return { message: first, context: undefined, error: r };
|
|
105
|
+
if (typeof r === 'object' && r !== null) return { message: first, context: r, error: undefined };
|
|
106
|
+
// Scalar extra: append to message (console.log('count:', 42))
|
|
107
|
+
return { message: first + ' ' + String(r), context: undefined, error: undefined };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Multiple extra args — find a trailing Error, collect the rest as context
|
|
111
|
+
const lastArg = rest[rest.length - 1];
|
|
112
|
+
if (lastArg instanceof Error) {
|
|
113
|
+
const ctx = rest.slice(0, -1);
|
|
114
|
+
return { message: first, context: ctx.length ? ctx : undefined, error: lastArg };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// All strings/scalars — join into message
|
|
118
|
+
if (rest.every(r => typeof r !== 'object' || r === null)) {
|
|
119
|
+
return { message: [first, ...rest].map(String).join(' '), context: undefined, error: undefined };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Mixed — put extras in context
|
|
123
|
+
return { message: first, context: rest, error: undefined };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// First arg is an object
|
|
127
|
+
if (typeof first === 'object' && first !== null) {
|
|
128
|
+
return { message: '', context: first, error: undefined };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Fallback — join everything as a string
|
|
132
|
+
return { message: args.map(String).join(' '), context: undefined, error: undefined };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = patchConsole;
|
|
@@ -1,58 +1,50 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const Middleware
|
|
3
|
+
const Middleware = require('./Middleware');
|
|
4
|
+
const MillasResponse = require('../http/MillasResponse');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* CorsMiddleware
|
|
7
8
|
*
|
|
8
|
-
* Adds
|
|
9
|
-
*
|
|
10
|
-
* Register:
|
|
11
|
-
* middlewareRegistry.register('cors', CorsMiddleware);
|
|
12
|
-
*
|
|
13
|
-
* Configure in bootstrap/app.js:
|
|
14
|
-
* new CorsMiddleware({
|
|
15
|
-
* origins: ['https://myapp.com'],
|
|
16
|
-
* methods: ['GET', 'POST'],
|
|
17
|
-
* })
|
|
18
|
-
*
|
|
19
|
-
* Or register the class directly for default permissive settings.
|
|
9
|
+
* Adds CORS headers. Uses the new ctx signature.
|
|
20
10
|
*/
|
|
21
11
|
class CorsMiddleware extends Middleware {
|
|
22
12
|
constructor(options = {}) {
|
|
23
13
|
super();
|
|
24
14
|
this.origins = options.origins || ['*'];
|
|
25
|
-
this.methods = options.methods || ['GET',
|
|
26
|
-
this.headers = options.headers || ['Content-Type',
|
|
15
|
+
this.methods = options.methods || ['GET','POST','PUT','PATCH','DELETE','OPTIONS'];
|
|
16
|
+
this.headers = options.headers || ['Content-Type','Authorization','X-Requested-With'];
|
|
27
17
|
this.credentials = options.credentials ?? false;
|
|
28
18
|
this.maxAge = options.maxAge || 86400;
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
async handle(req
|
|
32
|
-
const origin = req.
|
|
21
|
+
async handle({ req }, next) {
|
|
22
|
+
const origin = req.header('origin');
|
|
33
23
|
|
|
34
|
-
//
|
|
24
|
+
// Build headers map
|
|
25
|
+
const h = {};
|
|
35
26
|
if (this.origins.includes('*')) {
|
|
36
|
-
|
|
27
|
+
h['Access-Control-Allow-Origin'] = '*';
|
|
37
28
|
} else if (origin && this.origins.includes(origin)) {
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
h['Access-Control-Allow-Origin'] = origin;
|
|
30
|
+
h['Vary'] = 'Origin';
|
|
40
31
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
res.setHeader('Access-Control-Max-Age', String(this.maxAge));
|
|
45
|
-
|
|
32
|
+
h['Access-Control-Allow-Methods'] = this.methods.join(', ');
|
|
33
|
+
h['Access-Control-Allow-Headers'] = this.headers.join(', ');
|
|
34
|
+
h['Access-Control-Max-Age'] = String(this.maxAge);
|
|
46
35
|
if (this.credentials) {
|
|
47
|
-
|
|
36
|
+
h['Access-Control-Allow-Credentials'] = 'true';
|
|
48
37
|
}
|
|
49
38
|
|
|
50
|
-
//
|
|
39
|
+
// Preflight — short-circuit with 204
|
|
51
40
|
if (req.method === 'OPTIONS') {
|
|
52
|
-
return
|
|
41
|
+
return MillasResponse.empty(204).withHeaders(h);
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
|
|
44
|
+
// Proceed — but we still need headers on the eventual response.
|
|
45
|
+
// Store on raw req so ResponseDispatcher can pick them up.
|
|
46
|
+
req.raw._corsHeaders = h;
|
|
47
|
+
return next();
|
|
56
48
|
}
|
|
57
49
|
}
|
|
58
50
|
|
|
@@ -5,31 +5,8 @@ const Middleware = require('./Middleware');
|
|
|
5
5
|
/**
|
|
6
6
|
* LogMiddleware
|
|
7
7
|
*
|
|
8
|
-
* Django-style HTTP request
|
|
9
|
-
* Uses the
|
|
10
|
-
*
|
|
11
|
-
* Log levels per status:
|
|
12
|
-
* 2xx, 3xx → INFO (green)
|
|
13
|
-
* 4xx → WARN (yellow)
|
|
14
|
-
* 5xx → ERROR (red)
|
|
15
|
-
* slow req → WARN (with slow=true in context)
|
|
16
|
-
*
|
|
17
|
-
* Output:
|
|
18
|
-
* I HTTP GET /api/users 200 4ms
|
|
19
|
-
* W HTTP POST /api/login 401 12ms
|
|
20
|
-
* E HTTP GET /api/crash 500 3ms
|
|
21
|
-
*
|
|
22
|
-
* Usage (register as middleware):
|
|
23
|
-
* app.use(new LogMiddleware().handle.bind(new LogMiddleware()));
|
|
24
|
-
*
|
|
25
|
-
* Or use Log.requestMiddleware() directly for more options.
|
|
26
|
-
*
|
|
27
|
-
* Options:
|
|
28
|
-
* silent — suppress all output (default: false)
|
|
29
|
-
* includeQuery — include query string in URL (default: false)
|
|
30
|
-
* includeIp — include client IP (default: true)
|
|
31
|
-
* slowThreshold — warn if response > Nms (default: 1000)
|
|
32
|
-
* skip — function(req, res) => bool — skip matching routes
|
|
8
|
+
* Django-style HTTP request logging via MillasLog.
|
|
9
|
+
* Uses the new ctx signature: handle({ req }, next)
|
|
33
10
|
*/
|
|
34
11
|
class LogMiddleware extends Middleware {
|
|
35
12
|
constructor(options = {}) {
|
|
@@ -41,46 +18,37 @@ class LogMiddleware extends Middleware {
|
|
|
41
18
|
this.skip = options.skip ?? null;
|
|
42
19
|
}
|
|
43
20
|
|
|
44
|
-
async handle(req
|
|
21
|
+
async handle({ req }, next) {
|
|
45
22
|
if (this.silent) return next();
|
|
46
|
-
if (typeof this.skip === 'function' && this.skip(req
|
|
47
|
-
|
|
48
|
-
// Lazy-require Log so this file loads even before LogServiceProvider runs
|
|
49
|
-
let Log;
|
|
50
|
-
try {
|
|
51
|
-
Log = require('../logger/index').Log;
|
|
52
|
-
} catch {
|
|
53
|
-
return next();
|
|
54
|
-
}
|
|
23
|
+
if (typeof this.skip === 'function' && this.skip(req)) return next();
|
|
55
24
|
|
|
56
25
|
const start = Date.now();
|
|
57
|
-
const { LEVELS } = require('../logger/levels');
|
|
58
|
-
|
|
59
|
-
res.on('finish', () => {
|
|
60
|
-
const ms = Date.now() - start;
|
|
61
|
-
const status = res.statusCode;
|
|
62
|
-
const method = req.method;
|
|
63
|
-
let url = req.path || req.url || '/';
|
|
64
|
-
|
|
65
|
-
if (this.includeQuery && req.url && req.url.includes('?')) {
|
|
66
|
-
url = req.url;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Level based on status + response time
|
|
70
|
-
let level;
|
|
71
|
-
if (status >= 500) level = LEVELS.ERROR;
|
|
72
|
-
else if (status >= 400) level = LEVELS.WARN;
|
|
73
|
-
else if (ms > this.slowThreshold) level = LEVELS.WARN;
|
|
74
|
-
else level = LEVELS.INFO;
|
|
75
|
-
|
|
76
|
-
const ctx = { status, ms };
|
|
77
|
-
if (this.includeIp) ctx.ip = req.ip || req.connection?.remoteAddress;
|
|
78
|
-
if (ms > this.slowThreshold) ctx.slow = true;
|
|
79
26
|
|
|
80
|
-
|
|
27
|
+
// We can't hook into the response here since next() returns undefined.
|
|
28
|
+
// Instead attach a finish listener to the raw Express res.
|
|
29
|
+
req.raw.res.on('finish', () => {
|
|
30
|
+
try {
|
|
31
|
+
const MillasLog = require('../logger/internal');
|
|
32
|
+
const { LEVELS } = require('../logger/levels');
|
|
33
|
+
const ms = Date.now() - start;
|
|
34
|
+
const status = req.raw.res.statusCode;
|
|
35
|
+
let url = req.path;
|
|
36
|
+
if (this.includeQuery && req.raw.url?.includes('?')) url = req.raw.url;
|
|
37
|
+
|
|
38
|
+
let level = status >= 500 ? LEVELS.ERROR
|
|
39
|
+
: status >= 400 ? LEVELS.WARN
|
|
40
|
+
: ms > this.slowThreshold ? LEVELS.WARN
|
|
41
|
+
: LEVELS.INFO;
|
|
42
|
+
|
|
43
|
+
const ctx = { status, ms };
|
|
44
|
+
if (this.includeIp) ctx.ip = req.ip;
|
|
45
|
+
if (ms > this.slowThreshold) ctx.slow = true;
|
|
46
|
+
|
|
47
|
+
MillasLog._log(level, 'HTTP', `${req.method} ${url} ${status} ${ms}ms`, ctx);
|
|
48
|
+
} catch {}
|
|
81
49
|
});
|
|
82
50
|
|
|
83
|
-
next();
|
|
51
|
+
return next();
|
|
84
52
|
}
|
|
85
53
|
}
|
|
86
54
|
|
|
@@ -4,33 +4,42 @@
|
|
|
4
4
|
* Middleware
|
|
5
5
|
*
|
|
6
6
|
* Base class for all Millas middleware.
|
|
7
|
-
* Subclasses must implement handle(req, res, next).
|
|
8
7
|
*
|
|
9
|
-
*
|
|
8
|
+
* Middleware receives a RequestContext and a next() function.
|
|
9
|
+
* Destructure exactly what you need from the context — same as route handlers.
|
|
10
|
+
*
|
|
10
11
|
* class AuthMiddleware extends Middleware {
|
|
11
|
-
* async handle(
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* async handle({ headers, user }, next) {
|
|
13
|
+
* if (!headers.authorization) {
|
|
14
|
+
* return jsonify({ error: 'Unauthorized' }, { status: 401 });
|
|
15
|
+
* }
|
|
16
|
+
* return next();
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* class LogMiddleware extends Middleware {
|
|
21
|
+
* async handle({ req }, next) {
|
|
22
|
+
* Log.i('HTTP', `${req.method} ${req.path}`);
|
|
23
|
+
* return next();
|
|
14
24
|
* }
|
|
15
25
|
* }
|
|
16
26
|
*/
|
|
17
27
|
class Middleware {
|
|
18
28
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @
|
|
22
|
-
* @param {import('express').Response} res
|
|
23
|
-
* @param {Function} next
|
|
29
|
+
* @param {import('../http/RequestContext')} ctx
|
|
30
|
+
* @param {Function} next
|
|
31
|
+
* @returns {import('../http/MillasResponse')|Promise<import('../http/MillasResponse')>}
|
|
24
32
|
*/
|
|
25
|
-
async handle(
|
|
26
|
-
throw new Error(`${this.constructor.name} must implement handle(
|
|
33
|
+
async handle(ctx, next) {
|
|
34
|
+
throw new Error(`${this.constructor.name} must implement handle(ctx, next).`);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
|
-
* Called after the response is
|
|
31
|
-
*
|
|
38
|
+
* Called after the response is dispatched.
|
|
39
|
+
* @param {import('../http/RequestContext')} ctx
|
|
40
|
+
* @param {import('../http/MillasResponse')} response
|
|
32
41
|
*/
|
|
33
|
-
async terminate(
|
|
42
|
+
async terminate(ctx, response) {}
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
module.exports = Middleware;
|
|
@@ -1,93 +1,56 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const MillasRequest = require('../http/MillasRequest');
|
|
4
|
+
const RequestContext = require('../http/RequestContext');
|
|
5
|
+
const MillasResponse = require('../http/MillasResponse');
|
|
6
|
+
const ResponseDispatcher = require('../http/ResponseDispatcher');
|
|
7
|
+
|
|
3
8
|
/**
|
|
4
9
|
* MiddlewarePipeline
|
|
5
10
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Each handler can be:
|
|
10
|
-
* - A Middleware subclass (instantiated automatically)
|
|
11
|
-
* - An already-instantiated Middleware object
|
|
12
|
-
* - A raw Express function (req, res, next) => {}
|
|
11
|
+
* Runs an ordered list of middleware instances against a request.
|
|
12
|
+
* Used for programmatic pipelines outside of the router (e.g. queue webhooks).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* const pipeline = new MiddlewarePipeline([AuthMiddleware, LogMiddleware]);
|
|
16
|
-
* app.use(pipeline.compose());
|
|
14
|
+
* Each middleware receives a RequestContext and a next() function.
|
|
17
15
|
*/
|
|
18
16
|
class MiddlewarePipeline {
|
|
19
|
-
constructor(
|
|
20
|
-
this.
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Add a handler to the end of the pipeline.
|
|
25
|
-
*/
|
|
26
|
-
pipe(handler) {
|
|
27
|
-
this._handlers.push(handler);
|
|
28
|
-
return this;
|
|
17
|
+
constructor(middlewares = []) {
|
|
18
|
+
this._middlewares = middlewares;
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*/
|
|
34
|
-
prepend(handler) {
|
|
35
|
-
this._handlers.unshift(handler);
|
|
21
|
+
add(middleware) {
|
|
22
|
+
this._middlewares.push(middleware);
|
|
36
23
|
return this;
|
|
37
24
|
}
|
|
38
25
|
|
|
39
26
|
/**
|
|
40
|
-
*
|
|
27
|
+
* Run the pipeline against an Express req/res.
|
|
28
|
+
* @param {object} expressReq
|
|
29
|
+
* @param {object} expressRes
|
|
30
|
+
* @param {object|null} container
|
|
41
31
|
*/
|
|
42
|
-
|
|
43
|
-
const
|
|
32
|
+
async run(expressReq, expressRes, container = null) {
|
|
33
|
+
const millaReq = new MillasRequest(expressReq);
|
|
34
|
+
const ctx = new RequestContext(millaReq, container);
|
|
44
35
|
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
const dispatch = async (index) => {
|
|
37
|
+
if (index >= this._middlewares.length) return null;
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const fn = fns[i];
|
|
39
|
+
const mw = this._middlewares[index];
|
|
40
|
+
const next = () => dispatch(index + 1);
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
dispatch(i + 1);
|
|
56
|
-
});
|
|
57
|
-
// Handle async middleware that returns a Promise
|
|
58
|
-
if (result && typeof result.catch === 'function') {
|
|
59
|
-
result.catch(next);
|
|
60
|
-
}
|
|
61
|
-
} catch (err) {
|
|
62
|
-
next(err);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
42
|
+
const instance = typeof mw === 'function' && mw.prototype?.handle
|
|
43
|
+
? new mw()
|
|
44
|
+
: mw;
|
|
65
45
|
|
|
66
|
-
|
|
46
|
+
return instance.handle(ctx, next);
|
|
67
47
|
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Resolve a handler to a plain (req, res, next) => {} function.
|
|
72
|
-
*/
|
|
73
|
-
_resolve(handler) {
|
|
74
|
-
// Raw Express function
|
|
75
|
-
if (typeof handler === 'function' && !(handler.prototype instanceof require('./Middleware'))) {
|
|
76
|
-
return handler;
|
|
77
|
-
}
|
|
78
48
|
|
|
79
|
-
|
|
80
|
-
if (typeof handler === 'function' && handler.prototype instanceof require('./Middleware')) {
|
|
81
|
-
const instance = new handler();
|
|
82
|
-
return (req, res, next) => instance.handle(req, res, next);
|
|
83
|
-
}
|
|
49
|
+
const response = await dispatch(0);
|
|
84
50
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return (req, res, next) => handler.handle(req, res, next);
|
|
51
|
+
if (response && MillasResponse.isResponse(response) && !expressRes.headersSent) {
|
|
52
|
+
ResponseDispatcher.dispatch(response, expressRes);
|
|
88
53
|
}
|
|
89
|
-
|
|
90
|
-
throw new Error(`Invalid middleware: ${handler}. Must be a Middleware class, instance, or function.`);
|
|
91
54
|
}
|
|
92
55
|
}
|
|
93
56
|
|