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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const LookupParser = require('./LookupParser');
|
|
4
4
|
const { AggregateExpression } = require('./Aggregates');
|
|
5
|
+
const MillasLog = require('../../logger/internal');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* QueryBuilder
|
|
@@ -81,6 +82,21 @@ class QueryBuilder {
|
|
|
81
82
|
return this;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Bypass all global scopes for this query.
|
|
87
|
+
* Useful when you intentionally need an unfiltered view of the table.
|
|
88
|
+
*
|
|
89
|
+
* User.withoutGlobalScope().get() // skips tenant filter, active-only, etc.
|
|
90
|
+
*/
|
|
91
|
+
withoutGlobalScope() {
|
|
92
|
+
// Re-build the base query directly from knex, bypassing _db() where
|
|
93
|
+
// globalScopes are applied.
|
|
94
|
+
const DatabaseManager = require('../drivers/DatabaseManager');
|
|
95
|
+
const db = DatabaseManager.connection(this._model.connection || null);
|
|
96
|
+
this._query = db(this._model.table);
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
|
|
84
100
|
// ─── Scopes ───────────────────────────────────────────────────────────────
|
|
85
101
|
|
|
86
102
|
/**
|
|
@@ -117,7 +133,38 @@ class QueryBuilder {
|
|
|
117
133
|
return this;
|
|
118
134
|
}
|
|
119
135
|
|
|
120
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Eager-load a COUNT of related rows onto each result.
|
|
138
|
+
*
|
|
139
|
+
* Post.withCount('comments').get()
|
|
140
|
+
* // each post gets .comments_count
|
|
141
|
+
*
|
|
142
|
+
* Post.withCount('comments', 'likes').get()
|
|
143
|
+
* Post.withCount({ comments: q => q.where('approved', true) }).get()
|
|
144
|
+
*/
|
|
145
|
+
withCount(...relations) {
|
|
146
|
+
for (const rel of relations.flat()) {
|
|
147
|
+
if (typeof rel === 'string') {
|
|
148
|
+
this._withs.push({ name: rel, constraint: null, aggregate: 'count' });
|
|
149
|
+
} else if (typeof rel === 'object') {
|
|
150
|
+
for (const [name, constraint] of Object.entries(rel)) {
|
|
151
|
+
this._withs.push({ name, constraint, aggregate: 'count' });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Eager-load a SUM of a related column onto each result.
|
|
160
|
+
*
|
|
161
|
+
* User.withSum('orders', 'amount').get()
|
|
162
|
+
* // each user gets .orders_sum_amount
|
|
163
|
+
*/
|
|
164
|
+
withSum(relation, column) {
|
|
165
|
+
this._withs.push({ name: relation, constraint: null, aggregate: 'sum', aggregateColumn: column });
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
121
168
|
|
|
122
169
|
/**
|
|
123
170
|
* Add per-row computed columns.
|
|
@@ -336,10 +383,18 @@ class QueryBuilder {
|
|
|
336
383
|
|
|
337
384
|
const relations = this._model.relations || {};
|
|
338
385
|
|
|
339
|
-
for (const { name, constraint } of this._withs) {
|
|
386
|
+
for (const { name, constraint, aggregate, aggregateColumn } of this._withs) {
|
|
387
|
+
|
|
388
|
+
// ── withCount / withSum ──────────────────────────────────────────────
|
|
389
|
+
if (aggregate) {
|
|
390
|
+
await this._eagerAggregate(instances, name, aggregate, aggregateColumn, constraint, relations);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── Normal eager load ─────────────────────────────────────────────────
|
|
340
395
|
const rel = relations[name];
|
|
341
396
|
if (!rel) {
|
|
342
|
-
|
|
397
|
+
MillasLog.w('ORM', `Relation "${name}" not defined on ${this._model.name} — skipping eager load`);
|
|
343
398
|
continue;
|
|
344
399
|
}
|
|
345
400
|
|
|
@@ -347,6 +402,89 @@ class QueryBuilder {
|
|
|
347
402
|
}
|
|
348
403
|
}
|
|
349
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Load an aggregate (COUNT / SUM) of a relation onto each instance
|
|
407
|
+
* without pulling back full related rows.
|
|
408
|
+
*/
|
|
409
|
+
async _eagerAggregate(instances, relName, aggFn, aggColumn, constraint, relations) {
|
|
410
|
+
const rel = relations[relName];
|
|
411
|
+
if (!rel) {
|
|
412
|
+
MillasLog.w('ORM', `Relation "${relName}" not defined on ${this._model.name} — cannot compute ${aggFn}`);
|
|
413
|
+
// Zero-fill the attribute
|
|
414
|
+
const attr = aggFn === 'sum'
|
|
415
|
+
? `${relName}_sum_${aggColumn}`
|
|
416
|
+
: `${relName}_count`;
|
|
417
|
+
for (const i of instances) i[attr] = 0;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const attrName = aggFn === 'sum'
|
|
422
|
+
? `${relName}_sum_${aggColumn}`
|
|
423
|
+
: `${relName}_count`;
|
|
424
|
+
|
|
425
|
+
// We only support HasMany / BelongsToMany for aggregate loads for now.
|
|
426
|
+
// For those, the foreignKey / pivot info is on the rel object.
|
|
427
|
+
const HasMany = require('../relations/HasMany');
|
|
428
|
+
const BelongsToMany = require('../relations/BelongsToMany');
|
|
429
|
+
|
|
430
|
+
const localKey = rel._localKey || this._model.primaryKey || 'id';
|
|
431
|
+
const keys = [...new Set(instances.map(i => i[localKey]).filter(v => v != null))];
|
|
432
|
+
|
|
433
|
+
if (!keys.length) {
|
|
434
|
+
for (const i of instances) i[attrName] = 0;
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const related = rel._related;
|
|
439
|
+
let q;
|
|
440
|
+
|
|
441
|
+
if (rel instanceof HasMany) {
|
|
442
|
+
const fk = rel._foreignKey;
|
|
443
|
+
q = related._db()
|
|
444
|
+
.select(`${fk} as _owner_id`)
|
|
445
|
+
.whereIn(fk, keys);
|
|
446
|
+
|
|
447
|
+
if (aggFn === 'count') {
|
|
448
|
+
q = q.count(`${related.primaryKey || 'id'} as _agg_val`).groupBy(fk);
|
|
449
|
+
} else {
|
|
450
|
+
q = q.sum(`${aggColumn} as _agg_val`).groupBy(fk);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
} else if (rel instanceof BelongsToMany) {
|
|
454
|
+
const pivot = rel._pivotTable;
|
|
455
|
+
const fk = rel._foreignPivotKey;
|
|
456
|
+
const rk = rel._relatedPivotKey;
|
|
457
|
+
q = related._db()
|
|
458
|
+
.join(pivot, `${related.table}.${related.primaryKey || 'id'}`, '=', `${pivot}.${rk}`)
|
|
459
|
+
.select(`${pivot}.${fk} as _owner_id`)
|
|
460
|
+
.whereIn(`${pivot}.${fk}`, keys);
|
|
461
|
+
|
|
462
|
+
if (aggFn === 'count') {
|
|
463
|
+
q = q.count(`${related.table}.${related.primaryKey || 'id'} as _agg_val`).groupBy(`${pivot}.${fk}`);
|
|
464
|
+
} else {
|
|
465
|
+
q = q.sum(`${related.table}.${aggColumn} as _agg_val`).groupBy(`${pivot}.${fk}`);
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
// Unsupported relation type — zero-fill
|
|
469
|
+
for (const i of instances) i[attrName] = 0;
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (constraint) {
|
|
474
|
+
const QB = require('./QueryBuilder');
|
|
475
|
+
const qb = new QB(q, related);
|
|
476
|
+
constraint(qb);
|
|
477
|
+
q = qb._query;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const rows = await q;
|
|
481
|
+
const map = new Map(rows.map(r => [r._owner_id, Number(r._agg_val ?? 0)]));
|
|
482
|
+
|
|
483
|
+
for (const instance of instances) {
|
|
484
|
+
instance[attrName] = map.get(instance[localKey]) ?? 0;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
350
488
|
// ─── Internal where dispatcher ────────────────────────────────────────────
|
|
351
489
|
|
|
352
490
|
_applyWhere(method, column, operatorOrValue, value) {
|
|
@@ -13,47 +13,117 @@ const {
|
|
|
13
13
|
NullChannel,
|
|
14
14
|
StackChannel,
|
|
15
15
|
} = require('../logger/index');
|
|
16
|
+
const MillasLog = require('../logger/internal');
|
|
17
|
+
const patchConsole = require('../logger/patchConsole');
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* LogServiceProvider
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
+
* On boot:
|
|
23
|
+
* 1. Configures Log (app logger) from config/logging.js
|
|
24
|
+
* 2. Configures MillasLog (internal framework logger) from config.internal
|
|
25
|
+
* 3. Patches console.* to route through Log (unless interceptConsole: false)
|
|
22
26
|
*
|
|
23
|
-
*
|
|
24
|
-
* app.providers([LogServiceProvider, ...]);
|
|
27
|
+
* config/logging.js reference:
|
|
25
28
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
29
|
+
* module.exports = {
|
|
30
|
+
* level: 'debug',
|
|
31
|
+
* channels: [{ driver: 'console', format: 'pretty' }],
|
|
28
32
|
*
|
|
29
|
-
*
|
|
33
|
+
* // Opt out of console patching:
|
|
34
|
+
* interceptConsole: false,
|
|
35
|
+
*
|
|
36
|
+
* // Framework-internal logs (ORM, migrations, admin):
|
|
37
|
+
* internal: { level: 'warn' },
|
|
38
|
+
* };
|
|
30
39
|
*/
|
|
31
40
|
class LogServiceProvider extends ServiceProvider {
|
|
32
41
|
register(container) {
|
|
33
|
-
container.instance('Log',
|
|
34
|
-
container.instance('Logger',
|
|
42
|
+
container.instance('Log', Log);
|
|
43
|
+
container.instance('Logger', Logger);
|
|
44
|
+
container.instance('MillasLog', MillasLog);
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
/**
|
|
48
|
+
* beforeBoot — runs before any provider's register() call.
|
|
49
|
+
*
|
|
50
|
+
* We configure logging and patch console here so that:
|
|
51
|
+
* - Every register() call in every provider already produces
|
|
52
|
+
* formatted log output
|
|
53
|
+
* - No provider boots without a working logger
|
|
54
|
+
*
|
|
55
|
+
* This is synchronous — no async allowed in beforeBoot.
|
|
56
|
+
* We load config synchronously and apply defaults eagerly.
|
|
57
|
+
*/
|
|
58
|
+
beforeBoot(container) {
|
|
38
59
|
let config = {};
|
|
39
60
|
try {
|
|
40
61
|
config = require(process.cwd() + '/config/logging');
|
|
41
62
|
} catch {
|
|
42
|
-
// No config —
|
|
43
|
-
return;
|
|
63
|
+
// No config file — defaults already applied in logger/index.js
|
|
44
64
|
}
|
|
45
65
|
|
|
46
|
-
|
|
66
|
+
// Store config on the instance so boot() can use it without re-reading
|
|
67
|
+
this._loggingConfig = config;
|
|
47
68
|
|
|
69
|
+
// ── Configure Log from config ─────────────────────────────────────────
|
|
70
|
+
const channels = this._buildChannels(config);
|
|
48
71
|
Log.configure({
|
|
49
72
|
minLevel: this._resolveLevel(config.level ?? config.minLevel),
|
|
50
|
-
defaultTag: config.defaultTag || '
|
|
51
|
-
channel: channels.length === 1
|
|
52
|
-
? channels[0]
|
|
53
|
-
: new StackChannel(channels),
|
|
73
|
+
defaultTag: config.defaultTag || 'SystemOut',
|
|
74
|
+
channel: channels.length === 1 ? channels[0] : new StackChannel(channels),
|
|
54
75
|
});
|
|
55
76
|
|
|
56
|
-
|
|
77
|
+
// ── Configure MillasLog (internal framework logger) ───────────────────
|
|
78
|
+
this._configureMillasLog(config.internal);
|
|
79
|
+
|
|
80
|
+
// ── Patch console.* → Log.* ───────────────────────────────────────────
|
|
81
|
+
this._restoreConsole = patchConsole(Log, config.defaultTag || 'SystemOut');
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async boot(container, app) {
|
|
86
|
+
const config = this._loggingConfig || {};
|
|
87
|
+
const intercept = config.interceptConsole !== false;
|
|
88
|
+
|
|
89
|
+
// Store restore fn so tests can call container.make('console.restore')
|
|
90
|
+
if (this._restoreConsole) {
|
|
91
|
+
container.instance('console.restore', this._restoreConsole);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Log.tag('Millas').i(
|
|
95
|
+
// `Logger ready` +
|
|
96
|
+
// ` — level: ${this._levelName(Log._minLevel)}` +
|
|
97
|
+
// `, internal: ${this._levelName(MillasLog._minLevel)}` +
|
|
98
|
+
// `, console: ${intercept ? 'intercepted' : 'native'}`
|
|
99
|
+
// );
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
_configureMillasLog(internalConfig) {
|
|
105
|
+
if (internalConfig === false) {
|
|
106
|
+
MillasLog.configure({ channel: new NullChannel() });
|
|
107
|
+
} else if (internalConfig && typeof internalConfig === 'object') {
|
|
108
|
+
if (internalConfig.channels) {
|
|
109
|
+
const internalChannels = this._buildChannels(internalConfig);
|
|
110
|
+
MillasLog.configure({
|
|
111
|
+
minLevel: this._resolveLevel(internalConfig.level ?? internalConfig.minLevel),
|
|
112
|
+
defaultTag: 'Millas',
|
|
113
|
+
channel: internalChannels.length === 1
|
|
114
|
+
? internalChannels[0]
|
|
115
|
+
: new StackChannel(internalChannels),
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
const fmt = this._buildFormatter(internalConfig.format || 'pretty', internalConfig);
|
|
119
|
+
const level = this._resolveLevel(internalConfig.level ?? internalConfig.minLevel);
|
|
120
|
+
MillasLog.configure({
|
|
121
|
+
minLevel: level,
|
|
122
|
+
defaultTag: 'Millas',
|
|
123
|
+
channel: new ConsoleChannel({ formatter: fmt, minLevel: level }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
57
127
|
}
|
|
58
128
|
|
|
59
129
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
@@ -53,9 +53,22 @@ class ProviderRegistry {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Run the full
|
|
56
|
+
* Run the full provider lifecycle in order:
|
|
57
|
+
* Phase 0 — beforeBoot (synchronous, no bindings yet — for global setup)
|
|
58
|
+
* Phase 1 — register (synchronous, bind into container)
|
|
59
|
+
* Phase 2 — boot (async-safe, all bindings exist)
|
|
57
60
|
*/
|
|
58
61
|
async boot() {
|
|
62
|
+
// Phase 0: beforeBoot — runs before ANY register() call.
|
|
63
|
+
// Synchronous only. Used for global setup that must happen first
|
|
64
|
+
// (e.g. LogServiceProvider patches console here so all register()
|
|
65
|
+
// calls already produce formatted output).
|
|
66
|
+
for (const provider of this._providers) {
|
|
67
|
+
if (typeof provider.beforeBoot === 'function') {
|
|
68
|
+
provider.beforeBoot(this._container);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
// Phase 1: register all bindings
|
|
60
73
|
for (const provider of this._providers) {
|
|
61
74
|
if (typeof provider.register === 'function') {
|
|
@@ -5,37 +5,69 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Base class for all Millas service providers.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
8
|
+
* ── Lifecycle hooks (called in this order) ─────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* beforeBoot(container)
|
|
11
|
+
* Called FIRST — before any provider's register() runs.
|
|
12
|
+
* Use for setup that must happen before everything else:
|
|
13
|
+
* - Configuring logging (so register() calls get formatted output)
|
|
14
|
+
* - Patching globals (console, process handlers)
|
|
15
|
+
* - Reading config files that other providers depend on
|
|
16
|
+
* The container exists but has NO bindings yet.
|
|
17
|
+
* Must be synchronous — async is not supported here.
|
|
9
18
|
*
|
|
10
19
|
* register(container)
|
|
11
|
-
* Called
|
|
12
|
-
*
|
|
20
|
+
* Called after ALL beforeBoot() hooks have run.
|
|
21
|
+
* Bind things into the container (singletons, factories, instances).
|
|
22
|
+
* Do NOT resolve other bindings here — they may not exist yet.
|
|
23
|
+
* Must be synchronous.
|
|
13
24
|
*
|
|
14
25
|
* boot(container, app)
|
|
15
26
|
* Called after ALL providers have registered.
|
|
16
|
-
* Safe to resolve other bindings
|
|
17
|
-
* event listeners, middleware, etc.
|
|
27
|
+
* Safe to resolve other bindings, set up routes, register
|
|
28
|
+
* event listeners, mount middleware, etc.
|
|
29
|
+
* Async-safe — await is fine here.
|
|
30
|
+
*
|
|
31
|
+
* ── Example ────────────────────────────────────────────────────────────────
|
|
18
32
|
*
|
|
19
|
-
* Usage:
|
|
20
33
|
* class AppServiceProvider extends ServiceProvider {
|
|
34
|
+
* beforeBoot(container) {
|
|
35
|
+
* // Runs before any register() — good for global setup
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
21
38
|
* register(container) {
|
|
22
39
|
* container.singleton('UserService', UserService);
|
|
23
40
|
* }
|
|
41
|
+
*
|
|
24
42
|
* async boot(container, app) {
|
|
25
|
-
* const
|
|
26
|
-
*
|
|
43
|
+
* const db = container.make('db');
|
|
44
|
+
* await db.migrate();
|
|
27
45
|
* }
|
|
28
46
|
* }
|
|
29
47
|
*/
|
|
30
48
|
class ServiceProvider {
|
|
49
|
+
/**
|
|
50
|
+
* Called before any provider's register() runs.
|
|
51
|
+
* Container exists but has no bindings yet.
|
|
52
|
+
* Must be synchronous.
|
|
53
|
+
*
|
|
54
|
+
* @param {import('./Container')} container
|
|
55
|
+
*/
|
|
56
|
+
beforeBoot(container) {}
|
|
57
|
+
|
|
31
58
|
/**
|
|
32
59
|
* Register bindings into the container.
|
|
60
|
+
* Called after all beforeBoot() hooks have run.
|
|
61
|
+
* Must be synchronous.
|
|
62
|
+
*
|
|
33
63
|
* @param {import('./Container')} container
|
|
34
64
|
*/
|
|
35
65
|
register(container) {}
|
|
36
66
|
|
|
37
67
|
/**
|
|
38
68
|
* Bootstrap services after all providers have registered.
|
|
69
|
+
* Safe to resolve other bindings. Async-safe.
|
|
70
|
+
*
|
|
39
71
|
* @param {import('./Container')} container
|
|
40
72
|
* @param {import('express').Application} app
|
|
41
73
|
*/
|