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.
Files changed (74) hide show
  1. package/package.json +6 -5
  2. package/src/auth/Auth.js +13 -8
  3. package/src/auth/AuthController.js +45 -134
  4. package/src/auth/AuthMiddleware.js +12 -23
  5. package/src/auth/AuthUser.js +98 -0
  6. package/src/auth/RoleMiddleware.js +7 -17
  7. package/src/cli.js +1 -1
  8. package/src/commands/migrate.js +46 -31
  9. package/src/commands/serve.js +238 -38
  10. package/src/container/AppInitializer.js +158 -0
  11. package/src/container/Application.js +288 -183
  12. package/src/container/HttpServer.js +156 -0
  13. package/src/container/MillasApp.js +23 -280
  14. package/src/container/MillasConfig.js +163 -0
  15. package/src/controller/Controller.js +79 -300
  16. package/src/core/auth.js +9 -0
  17. package/src/core/db.js +8 -0
  18. package/src/core/foundation.js +67 -0
  19. package/src/core/http.js +11 -0
  20. package/src/core/mail.js +6 -0
  21. package/src/core/queue.js +7 -0
  22. package/src/core/validation.js +29 -0
  23. package/src/errors/ErrorRenderer.js +640 -0
  24. package/src/facades/Admin.js +49 -0
  25. package/src/facades/Auth.js +29 -0
  26. package/src/facades/Cache.js +28 -0
  27. package/src/facades/Database.js +43 -0
  28. package/src/facades/Events.js +25 -0
  29. package/src/facades/Facade.js +197 -0
  30. package/src/facades/Http.js +51 -0
  31. package/src/facades/Log.js +32 -0
  32. package/src/facades/Mail.js +35 -0
  33. package/src/facades/Queue.js +30 -0
  34. package/src/facades/Storage.js +25 -0
  35. package/src/facades/Url.js +53 -0
  36. package/src/http/HttpClient.js +673 -0
  37. package/src/http/MillasRequest.js +253 -0
  38. package/src/http/MillasResponse.js +196 -0
  39. package/src/http/RequestContext.js +176 -0
  40. package/src/http/ResponseDispatcher.js +51 -0
  41. package/src/http/UrlGenerator.js +375 -0
  42. package/src/http/WelcomePage.js +273 -0
  43. package/src/http/adapters/ExpressAdapter.js +315 -0
  44. package/src/http/adapters/HttpAdapter.js +168 -0
  45. package/src/http/adapters/index.js +9 -0
  46. package/src/http/helpers.js +164 -0
  47. package/src/http/index.js +13 -0
  48. package/src/index.js +5 -91
  49. package/src/logger/formatters/PrettyFormatter.js +15 -5
  50. package/src/logger/internal.js +76 -0
  51. package/src/logger/patchConsole.js +145 -0
  52. package/src/middleware/CorsMiddleware.js +22 -30
  53. package/src/middleware/LogMiddleware.js +27 -59
  54. package/src/middleware/Middleware.js +24 -15
  55. package/src/middleware/MiddlewarePipeline.js +30 -67
  56. package/src/middleware/MiddlewareRegistry.js +106 -0
  57. package/src/middleware/ThrottleMiddleware.js +22 -26
  58. package/src/orm/fields/index.js +124 -56
  59. package/src/orm/migration/ModelInspector.js +339 -336
  60. package/src/orm/model/Model.js +96 -6
  61. package/src/orm/query/QueryBuilder.js +141 -3
  62. package/src/providers/AuthServiceProvider.js +9 -5
  63. package/src/providers/CacheStorageServiceProvider.js +3 -1
  64. package/src/providers/EventServiceProvider.js +2 -1
  65. package/src/providers/LogServiceProvider.js +88 -17
  66. package/src/providers/MailServiceProvider.js +3 -2
  67. package/src/providers/ProviderRegistry.js +14 -1
  68. package/src/providers/QueueServiceProvider.js +3 -2
  69. package/src/providers/ServiceProvider.js +40 -8
  70. package/src/router/Router.js +121 -222
  71. package/src/scaffold/maker.js +24 -59
  72. package/src/scaffold/templates.js +21 -19
  73. package/src/validation/BaseValidator.js +193 -0
  74. package/src/validation/Validator.js +680 -0
@@ -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
- // Attach lazy relation loaders as callable methods
465
- const relations = this.constructor.relations || {};
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
- // ─── Aggregation ──────────────────────────────────────────────────────────
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
- process.stderr.write(` [millas] Warning: relation "${name}" not defined on ${this._model.name}\n`);
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', 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 available
36
- let UserModel = null;
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 { /* User model optional at boot */ }
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
- * Reads config/logging.js and configures the Log singleton.
21
- * Also registers Log and Logger in the DI container.
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
- * Add to bootstrap/app.js:
24
- * app.providers([LogServiceProvider, ...]);
27
+ * config/logging.js reference:
25
28
  *
26
- * The provider is intentionally ordered first so every other provider
27
- * can use Log during their boot() method.
29
+ * module.exports = {
30
+ * level: 'debug',
31
+ * channels: [{ driver: 'console', format: 'pretty' }],
28
32
  *
29
- * config/logging.js is optional sensible defaults apply if absent.
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', 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
- async boot(container, app) {
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 — use defaults already set in logger/index.js
43
- return;
64
+ // No config file — defaults already applied in logger/index.js
44
65
  }
45
66
 
46
- const channels = this._buildChannels(config);
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 || 'App',
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
- Log.tag('Millas').i(`Logger configured level: ${this._levelName(Log._minLevel)}, channels: ${channels.length}`);
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', 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 register boot lifecycle.
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', 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
- * Providers have two lifecycle hooks:
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 first. Bind things into the container.
12
- * Do NOT use other bindings here they may not exist yet.
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 and set up routes,
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 logger = container.make('Logger');
26
- * logger.info('AppServiceProvider booted');
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
  */