millas 0.2.11 → 0.2.12-beta-1
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 +6 -5
- package/src/auth/Auth.js +13 -8
- package/src/auth/AuthController.js +45 -134
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/AuthUser.js +98 -0
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/cli.js +1 -1
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +238 -38
- package/src/container/AppInitializer.js +158 -0
- package/src/container/Application.js +288 -183
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +23 -280
- package/src/container/MillasConfig.js +163 -0
- package/src/controller/Controller.js +79 -300
- package/src/core/auth.js +9 -0
- package/src/core/db.js +8 -0
- package/src/core/foundation.js +67 -0
- package/src/core/http.js +11 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +29 -0
- package/src/facades/Cache.js +28 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +25 -0
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +51 -0
- package/src/facades/Log.js +32 -0
- package/src/facades/Mail.js +35 -0
- package/src/facades/Queue.js +30 -0
- package/src/facades/Storage.js +25 -0
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -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 +51 -0
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +5 -91
- package/src/logger/formatters/PrettyFormatter.js +15 -5
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +145 -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 +106 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +339 -336
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- package/src/providers/AuthServiceProvider.js +9 -5
- package/src/providers/CacheStorageServiceProvider.js +3 -1
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +88 -17
- package/src/providers/MailServiceProvider.js +3 -2
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/QueueServiceProvider.js +3 -2
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +121 -222
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +21 -19
- package/src/validation/BaseValidator.js +193 -0
- package/src/validation/Validator.js +680 -0
package/src/orm/model/Model.js
CHANGED
|
@@ -412,7 +412,7 @@ class Model {
|
|
|
412
412
|
return new QueryBuilder(this._db(), this).distinct(...cols);
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
/** Start an eager-load chain. */
|
|
415
|
+
/** Start an eager-load chain. Relations inferred from fields are included automatically. */
|
|
416
416
|
static with(...relations) {
|
|
417
417
|
return new QueryBuilder(this._db(), this).with(...relations);
|
|
418
418
|
}
|
|
@@ -461,15 +461,14 @@ class Model {
|
|
|
461
461
|
Object.assign(this, attributes);
|
|
462
462
|
this._original = { ...attributes };
|
|
463
463
|
|
|
464
|
-
//
|
|
465
|
-
|
|
464
|
+
// Use effective relations: explicit static relations PLUS those
|
|
465
|
+
// auto-inferred from ForeignKey / OneToOne / ManyToMany fields.
|
|
466
|
+
const relations = this.constructor._effectiveRelations();
|
|
467
|
+
|
|
466
468
|
for (const [name, rel] of Object.entries(relations)) {
|
|
467
|
-
// Skip if already set by eager load
|
|
468
469
|
if (!(name in this)) {
|
|
469
470
|
this[name] = () => rel.load(this);
|
|
470
471
|
}
|
|
471
|
-
|
|
472
|
-
// Attach pivot manager for BelongsToMany
|
|
473
472
|
const BelongsToMany = require('../relations/BelongsToMany');
|
|
474
473
|
if (rel instanceof BelongsToMany) {
|
|
475
474
|
Object.assign(this[name], rel._pivotManager(this));
|
|
@@ -477,6 +476,97 @@ class Model {
|
|
|
477
476
|
}
|
|
478
477
|
}
|
|
479
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Returns the merged relation map for this model class.
|
|
481
|
+
*
|
|
482
|
+
* Priority (highest → lowest):
|
|
483
|
+
* 1. Explicitly declared `static relations = {}`
|
|
484
|
+
* 2. Auto-inferred from ForeignKey / OneToOne / ManyToMany fields
|
|
485
|
+
*
|
|
486
|
+
* Result is cached per class after first call.
|
|
487
|
+
*/
|
|
488
|
+
static _effectiveRelations() {
|
|
489
|
+
if (this._cachedRelations) return this._cachedRelations;
|
|
490
|
+
|
|
491
|
+
const BelongsTo = require('../relations/BelongsTo');
|
|
492
|
+
const HasOne = require('../relations/HasOne');
|
|
493
|
+
const BelongsToMany = require('../relations/BelongsToMany');
|
|
494
|
+
|
|
495
|
+
// Start with explicitly declared relations
|
|
496
|
+
const merged = { ...(this.relations || {}) };
|
|
497
|
+
|
|
498
|
+
for (const [fieldName, fieldDef] of Object.entries(this.fields || {})) {
|
|
499
|
+
|
|
500
|
+
// ── ForeignKey / OneToOne ────────────────────────────────────────────
|
|
501
|
+
if (fieldDef._isForeignKey) {
|
|
502
|
+
// Infer accessor name:
|
|
503
|
+
// author_id → author
|
|
504
|
+
// author → author (column will be author_id in migration)
|
|
505
|
+
const accessorName = fieldName.endsWith('_id')
|
|
506
|
+
? fieldName.slice(0, -3)
|
|
507
|
+
: fieldName;
|
|
508
|
+
|
|
509
|
+
// Don't overwrite an explicitly declared relation
|
|
510
|
+
if (!merged[accessorName]) {
|
|
511
|
+
const modelRef = fieldDef._fkModelRef;
|
|
512
|
+
const toField = fieldDef._fkToField || 'id';
|
|
513
|
+
const self = this;
|
|
514
|
+
|
|
515
|
+
// self-referential: 'self' means this very model
|
|
516
|
+
const resolveModel = () => {
|
|
517
|
+
if (fieldDef._fkModel === 'self') return self;
|
|
518
|
+
const M = typeof modelRef === 'function' ? modelRef() : modelRef;
|
|
519
|
+
return M;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
if (fieldDef._isOneToOne) {
|
|
523
|
+
// OneToOne: BelongsTo on the declaring side
|
|
524
|
+
merged[accessorName] = new BelongsTo(resolveModel, fieldName.endsWith('_id') ? fieldName : fieldName + '_id', toField);
|
|
525
|
+
} else {
|
|
526
|
+
merged[accessorName] = new BelongsTo(resolveModel, fieldName.endsWith('_id') ? fieldName : fieldName + '_id', toField);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── ManyToMany ────────────────────────────────────────────────────────
|
|
532
|
+
if (fieldDef._isManyToMany && !merged[fieldName]) {
|
|
533
|
+
const thisTableBase = (this.table || this.name.toLowerCase()).replace(/s$/, '');
|
|
534
|
+
const modelRef = fieldDef._fkModelRef;
|
|
535
|
+
|
|
536
|
+
const resolveRelated = () => {
|
|
537
|
+
const M = typeof modelRef === 'function' ? modelRef() : modelRef;
|
|
538
|
+
return M;
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// Infer pivot table: sort both singular table names alphabetically
|
|
542
|
+
const relatedName = typeof fieldDef._fkModel === 'string'
|
|
543
|
+
? fieldDef._fkModel.toLowerCase().replace(/s$/, '')
|
|
544
|
+
: fieldName.replace(/s$/, '');
|
|
545
|
+
|
|
546
|
+
const pivotTable = fieldDef._m2mThrough
|
|
547
|
+
|| [thisTableBase, relatedName].sort().join('_') + 's';
|
|
548
|
+
|
|
549
|
+
const thisFk = thisTableBase + '_id';
|
|
550
|
+
const relatedFk = relatedName + '_id';
|
|
551
|
+
|
|
552
|
+
merged[fieldName] = new BelongsToMany(
|
|
553
|
+
resolveRelated,
|
|
554
|
+
pivotTable,
|
|
555
|
+
thisFk,
|
|
556
|
+
relatedFk,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
this._cachedRelations = merged;
|
|
562
|
+
return merged;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Clear the cached relations (call if fields are modified at runtime). */
|
|
566
|
+
static _clearRelationCache() {
|
|
567
|
+
this._cachedRelations = null;
|
|
568
|
+
}
|
|
569
|
+
|
|
480
570
|
/**
|
|
481
571
|
* Persist changes to this instance.
|
|
482
572
|
* @param {object} data
|
|
@@ -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) {
|
|
@@ -16,7 +16,8 @@ const RoleMiddleware = require('../auth/RoleMiddleware');
|
|
|
16
16
|
*/
|
|
17
17
|
class AuthServiceProvider extends ServiceProvider {
|
|
18
18
|
register(container) {
|
|
19
|
-
container.instance('Auth',
|
|
19
|
+
container.instance('Auth', Auth);
|
|
20
|
+
container.alias('auth', 'Auth');
|
|
20
21
|
container.instance('AuthMiddleware', AuthMiddleware);
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -32,11 +33,14 @@ class AuthServiceProvider extends ServiceProvider {
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
// Load the User model if
|
|
36
|
-
|
|
36
|
+
// Load the app's User model if it exists.
|
|
37
|
+
// Falls back to the built-in AuthUser so Auth always has a model to work with.
|
|
38
|
+
let UserModel;
|
|
37
39
|
try {
|
|
38
40
|
UserModel = require(process.cwd() + '/app/models/User');
|
|
39
|
-
} catch {
|
|
41
|
+
} catch {
|
|
42
|
+
UserModel = require('../auth/AuthUser');
|
|
43
|
+
}
|
|
40
44
|
|
|
41
45
|
// Configure the Auth singleton
|
|
42
46
|
Auth.configure(authConfig, UserModel);
|
|
@@ -50,4 +54,4 @@ class AuthServiceProvider extends ServiceProvider {
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
module.exports = AuthServiceProvider;
|
|
57
|
+
module.exports = AuthServiceProvider;
|
|
@@ -12,6 +12,7 @@ const Storage = require('../storage/Storage');
|
|
|
12
12
|
class CacheServiceProvider extends ServiceProvider {
|
|
13
13
|
register(container) {
|
|
14
14
|
container.instance('Cache', Cache);
|
|
15
|
+
container.alias('cache', 'Cache');
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
async boot() {
|
|
@@ -41,6 +42,7 @@ class CacheServiceProvider extends ServiceProvider {
|
|
|
41
42
|
class StorageServiceProvider extends ServiceProvider {
|
|
42
43
|
register(container) {
|
|
43
44
|
container.instance('Storage', Storage);
|
|
45
|
+
container.alias('storage', 'Storage');
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
async boot() {
|
|
@@ -68,4 +70,4 @@ class StorageServiceProvider extends ServiceProvider {
|
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
module.exports = { CacheServiceProvider, StorageServiceProvider };
|
|
73
|
+
module.exports = { CacheServiceProvider, StorageServiceProvider };
|
|
@@ -19,6 +19,7 @@ const { emit } = require('../events/EventEmitter');
|
|
|
19
19
|
class EventServiceProvider extends ServiceProvider {
|
|
20
20
|
register(container) {
|
|
21
21
|
container.instance('EventEmitter', EventEmitter);
|
|
22
|
+
container.alias('events', 'EventEmitter');
|
|
22
23
|
container.instance('emit', emit);
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -31,4 +32,4 @@ class EventServiceProvider extends ServiceProvider {
|
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
module.exports = EventServiceProvider;
|
|
35
|
+
module.exports = EventServiceProvider;
|
|
@@ -13,47 +13,118 @@ 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',
|
|
42
|
+
container.instance('Log', Log);
|
|
34
43
|
container.instance('Logger', Logger);
|
|
44
|
+
container.alias('log', 'Log');
|
|
45
|
+
container.instance('MillasLog', MillasLog);
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
|
|
48
|
+
/**
|
|
49
|
+
* beforeBoot — runs before any provider's register() call.
|
|
50
|
+
*
|
|
51
|
+
* We configure logging and patch console here so that:
|
|
52
|
+
* - Every register() call in every provider already produces
|
|
53
|
+
* formatted log output
|
|
54
|
+
* - No provider boots without a working logger
|
|
55
|
+
*
|
|
56
|
+
* This is synchronous — no async allowed in beforeBoot.
|
|
57
|
+
* We load config synchronously and apply defaults eagerly.
|
|
58
|
+
*/
|
|
59
|
+
beforeBoot(container) {
|
|
38
60
|
let config = {};
|
|
39
61
|
try {
|
|
40
62
|
config = require(process.cwd() + '/config/logging');
|
|
41
63
|
} catch {
|
|
42
|
-
// No config —
|
|
43
|
-
return;
|
|
64
|
+
// No config file — defaults already applied in logger/index.js
|
|
44
65
|
}
|
|
45
66
|
|
|
46
|
-
|
|
67
|
+
// Store config on the instance so boot() can use it without re-reading
|
|
68
|
+
this._loggingConfig = config;
|
|
47
69
|
|
|
70
|
+
// ── Configure Log from config ─────────────────────────────────────────
|
|
71
|
+
const channels = this._buildChannels(config);
|
|
48
72
|
Log.configure({
|
|
49
73
|
minLevel: this._resolveLevel(config.level ?? config.minLevel),
|
|
50
|
-
defaultTag: config.defaultTag || '
|
|
51
|
-
channel: channels.length === 1
|
|
52
|
-
? channels[0]
|
|
53
|
-
: new StackChannel(channels),
|
|
74
|
+
defaultTag: config.defaultTag || 'SystemOut',
|
|
75
|
+
channel: channels.length === 1 ? channels[0] : new StackChannel(channels),
|
|
54
76
|
});
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
// ── Configure MillasLog (internal framework logger) ───────────────────
|
|
79
|
+
this._configureMillasLog(config.internal);
|
|
80
|
+
|
|
81
|
+
// ── Patch console.* → Log.* ───────────────────────────────────────────
|
|
82
|
+
this._restoreConsole = patchConsole(Log, config.defaultTag || 'SystemOut');
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async boot(container, app) {
|
|
87
|
+
const config = this._loggingConfig || {};
|
|
88
|
+
const intercept = config.interceptConsole !== false;
|
|
89
|
+
|
|
90
|
+
// Store restore fn so tests can call container.make('console.restore')
|
|
91
|
+
if (this._restoreConsole) {
|
|
92
|
+
container.instance('console.restore', this._restoreConsole);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Log.tag('Millas').i(
|
|
96
|
+
// `Logger ready` +
|
|
97
|
+
// ` — level: ${this._levelName(Log._minLevel)}` +
|
|
98
|
+
// `, internal: ${this._levelName(MillasLog._minLevel)}` +
|
|
99
|
+
// `, console: ${intercept ? 'intercepted' : 'native'}`
|
|
100
|
+
// );
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Private ──────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
_configureMillasLog(internalConfig) {
|
|
106
|
+
if (internalConfig === false) {
|
|
107
|
+
MillasLog.configure({ channel: new NullChannel() });
|
|
108
|
+
} else if (internalConfig && typeof internalConfig === 'object') {
|
|
109
|
+
if (internalConfig.channels) {
|
|
110
|
+
const internalChannels = this._buildChannels(internalConfig);
|
|
111
|
+
MillasLog.configure({
|
|
112
|
+
minLevel: this._resolveLevel(internalConfig.level ?? internalConfig.minLevel),
|
|
113
|
+
defaultTag: 'Millas',
|
|
114
|
+
channel: internalChannels.length === 1
|
|
115
|
+
? internalChannels[0]
|
|
116
|
+
: new StackChannel(internalChannels),
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
const fmt = this._buildFormatter(internalConfig.format || 'pretty', internalConfig);
|
|
120
|
+
const level = this._resolveLevel(internalConfig.level ?? internalConfig.minLevel);
|
|
121
|
+
MillasLog.configure({
|
|
122
|
+
minLevel: level,
|
|
123
|
+
defaultTag: 'Millas',
|
|
124
|
+
channel: new ConsoleChannel({ formatter: fmt, minLevel: level }),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
57
128
|
}
|
|
58
129
|
|
|
59
130
|
// ─── Private ──────────────────────────────────────────────────────────────
|
|
@@ -19,7 +19,8 @@ const MailMessage = require('../mail/MailMessage');
|
|
|
19
19
|
*/
|
|
20
20
|
class MailServiceProvider extends ServiceProvider {
|
|
21
21
|
register(container) {
|
|
22
|
-
container.instance('Mail',
|
|
22
|
+
container.instance('Mail', Mail);
|
|
23
|
+
container.alias('mail', 'Mail');
|
|
23
24
|
container.instance('MailMessage', MailMessage);
|
|
24
25
|
}
|
|
25
26
|
|
|
@@ -48,4 +49,4 @@ class MailServiceProvider extends ServiceProvider {
|
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
module.exports = MailServiceProvider;
|
|
52
|
+
module.exports = MailServiceProvider;
|
|
@@ -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') {
|
|
@@ -21,7 +21,8 @@ const { dispatch } = require('../queue/Queue');
|
|
|
21
21
|
*/
|
|
22
22
|
class QueueServiceProvider extends ServiceProvider {
|
|
23
23
|
register(container) {
|
|
24
|
-
container.instance('Queue',
|
|
24
|
+
container.instance('Queue', Queue);
|
|
25
|
+
container.alias('queue', 'Queue');
|
|
25
26
|
container.instance('dispatch', dispatch);
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -49,4 +50,4 @@ class QueueServiceProvider extends ServiceProvider {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
module.exports = QueueServiceProvider;
|
|
53
|
+
module.exports = QueueServiceProvider;
|
|
@@ -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
|
*/
|