millas 0.1.0

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +137 -0
  3. package/bin/millas.js +6 -0
  4. package/package.json +56 -0
  5. package/src/admin/Admin.js +617 -0
  6. package/src/admin/index.js +13 -0
  7. package/src/admin/resources/AdminResource.js +317 -0
  8. package/src/auth/Auth.js +254 -0
  9. package/src/auth/AuthController.js +188 -0
  10. package/src/auth/AuthMiddleware.js +67 -0
  11. package/src/auth/Hasher.js +51 -0
  12. package/src/auth/JwtDriver.js +74 -0
  13. package/src/auth/RoleMiddleware.js +44 -0
  14. package/src/cache/Cache.js +231 -0
  15. package/src/cache/drivers/FileDriver.js +152 -0
  16. package/src/cache/drivers/MemoryDriver.js +158 -0
  17. package/src/cache/drivers/NullDriver.js +27 -0
  18. package/src/cache/index.js +8 -0
  19. package/src/cli.js +27 -0
  20. package/src/commands/make.js +61 -0
  21. package/src/commands/migrate.js +174 -0
  22. package/src/commands/new.js +50 -0
  23. package/src/commands/queue.js +92 -0
  24. package/src/commands/route.js +93 -0
  25. package/src/commands/serve.js +50 -0
  26. package/src/container/Application.js +177 -0
  27. package/src/container/Container.js +281 -0
  28. package/src/container/index.js +13 -0
  29. package/src/controller/Controller.js +367 -0
  30. package/src/errors/HttpError.js +29 -0
  31. package/src/events/Event.js +39 -0
  32. package/src/events/EventEmitter.js +151 -0
  33. package/src/events/Listener.js +46 -0
  34. package/src/events/index.js +15 -0
  35. package/src/index.js +93 -0
  36. package/src/mail/Mail.js +210 -0
  37. package/src/mail/MailMessage.js +196 -0
  38. package/src/mail/TemplateEngine.js +150 -0
  39. package/src/mail/drivers/LogDriver.js +36 -0
  40. package/src/mail/drivers/MailgunDriver.js +84 -0
  41. package/src/mail/drivers/SendGridDriver.js +97 -0
  42. package/src/mail/drivers/SmtpDriver.js +67 -0
  43. package/src/mail/index.js +19 -0
  44. package/src/middleware/AuthMiddleware.js +46 -0
  45. package/src/middleware/CorsMiddleware.js +59 -0
  46. package/src/middleware/LogMiddleware.js +61 -0
  47. package/src/middleware/Middleware.js +36 -0
  48. package/src/middleware/MiddlewarePipeline.js +94 -0
  49. package/src/middleware/ThrottleMiddleware.js +61 -0
  50. package/src/orm/drivers/DatabaseManager.js +135 -0
  51. package/src/orm/fields/index.js +132 -0
  52. package/src/orm/index.js +19 -0
  53. package/src/orm/migration/MigrationRunner.js +216 -0
  54. package/src/orm/migration/ModelInspector.js +338 -0
  55. package/src/orm/migration/SchemaBuilder.js +173 -0
  56. package/src/orm/model/Model.js +371 -0
  57. package/src/orm/query/QueryBuilder.js +197 -0
  58. package/src/providers/AdminServiceProvider.js +40 -0
  59. package/src/providers/AuthServiceProvider.js +53 -0
  60. package/src/providers/CacheStorageServiceProvider.js +71 -0
  61. package/src/providers/DatabaseServiceProvider.js +45 -0
  62. package/src/providers/EventServiceProvider.js +34 -0
  63. package/src/providers/MailServiceProvider.js +51 -0
  64. package/src/providers/ProviderRegistry.js +82 -0
  65. package/src/providers/QueueServiceProvider.js +52 -0
  66. package/src/providers/ServiceProvider.js +45 -0
  67. package/src/queue/Job.js +135 -0
  68. package/src/queue/Queue.js +147 -0
  69. package/src/queue/drivers/DatabaseDriver.js +194 -0
  70. package/src/queue/drivers/SyncDriver.js +72 -0
  71. package/src/queue/index.js +16 -0
  72. package/src/queue/workers/QueueWorker.js +140 -0
  73. package/src/router/MiddlewareRegistry.js +82 -0
  74. package/src/router/Route.js +255 -0
  75. package/src/router/RouteGroup.js +19 -0
  76. package/src/router/RouteRegistry.js +55 -0
  77. package/src/router/Router.js +138 -0
  78. package/src/router/index.js +15 -0
  79. package/src/scaffold/generator.js +34 -0
  80. package/src/scaffold/maker.js +272 -0
  81. package/src/scaffold/templates.js +350 -0
  82. package/src/storage/Storage.js +170 -0
  83. package/src/storage/drivers/LocalDriver.js +215 -0
  84. package/src/storage/index.js +6 -0
@@ -0,0 +1,367 @@
1
+ 'use strict';
2
+
3
+ const HttpError = require('../errors/HttpError');
4
+
5
+ /**
6
+ * Controller
7
+ *
8
+ * Base class for all Millas controllers.
9
+ *
10
+ * Provides:
11
+ * - Response helpers : this.ok(), this.created(), this.noContent(),
12
+ * this.badRequest(), this.unauthorized(),
13
+ * this.forbidden(), this.notFound(), this.json()
14
+ * - Request helpers : this.input(), this.param(), this.query(), this.all()
15
+ * - Validation : this.validate()
16
+ * - Pagination helper : this.paginate()
17
+ * - Error throwing : this.abort()
18
+ *
19
+ * Usage:
20
+ * class UserController extends Controller {
21
+ * async index(req, res) {
22
+ * return this.ok(res, { users: [] });
23
+ * }
24
+ * }
25
+ */
26
+ class Controller {
27
+
28
+ // ─── Response Helpers ────────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * 200 OK
32
+ * @param {object} res
33
+ * @param {*} data
34
+ */
35
+ ok(res, data = null) {
36
+ return res.status(200).json(this._envelope(200, data));
37
+ }
38
+
39
+ /**
40
+ * 201 Created
41
+ */
42
+ created(res, data = null) {
43
+ return res.status(201).json(this._envelope(201, data));
44
+ }
45
+
46
+ /**
47
+ * 204 No Content
48
+ */
49
+ noContent(res) {
50
+ return res.status(204).send();
51
+ }
52
+
53
+ /**
54
+ * 200 with a custom JSON payload (no envelope)
55
+ */
56
+ json(res, data, status = 200) {
57
+ return res.status(status).json(data);
58
+ }
59
+
60
+ /**
61
+ * 400 Bad Request
62
+ */
63
+ badRequest(res, message = 'Bad Request', errors = null) {
64
+ return res.status(400).json({
65
+ error: 'Bad Request',
66
+ message,
67
+ status: 400,
68
+ ...(errors && { errors }),
69
+ });
70
+ }
71
+
72
+ /**
73
+ * 401 Unauthorized
74
+ */
75
+ unauthorized(res, message = 'Unauthorized') {
76
+ return res.status(401).json({ error: 'Unauthorized', message, status: 401 });
77
+ }
78
+
79
+ /**
80
+ * 403 Forbidden
81
+ */
82
+ forbidden(res, message = 'Forbidden') {
83
+ return res.status(403).json({ error: 'Forbidden', message, status: 403 });
84
+ }
85
+
86
+ /**
87
+ * 404 Not Found
88
+ */
89
+ notFound(res, message = 'Not Found') {
90
+ return res.status(404).json({ error: 'Not Found', message, status: 404 });
91
+ }
92
+
93
+ /**
94
+ * 422 Unprocessable Entity — validation failed
95
+ */
96
+ unprocessable(res, errors) {
97
+ return res.status(422).json({
98
+ error: 'Unprocessable Entity',
99
+ message: 'Validation failed',
100
+ status: 422,
101
+ errors,
102
+ });
103
+ }
104
+
105
+ /**
106
+ * 500 Internal Server Error
107
+ */
108
+ serverError(res, message = 'Internal Server Error') {
109
+ return res.status(500).json({ error: 'Internal Server Error', message, status: 500 });
110
+ }
111
+
112
+ // ─── Abort (throw HttpError) ─────────────────────────────────────────────────
113
+
114
+ /**
115
+ * Throw an HTTP error — caught by the Router error handler.
116
+ *
117
+ * this.abort(404, 'User not found')
118
+ * this.abort(403)
119
+ */
120
+ abort(status, message) {
121
+ throw new HttpError(status, message);
122
+ }
123
+
124
+ // ─── Request Helpers ─────────────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Get a value from req.body, req.query, or req.params — in that order.
128
+ * Falls back to defaultValue if not found.
129
+ *
130
+ * this.input(req, 'email')
131
+ * this.input(req, 'page', 1)
132
+ */
133
+ input(req, key, defaultValue = null) {
134
+ if (key === undefined) return this.all(req);
135
+ const value =
136
+ (req.body && req.body[key] !== undefined ? req.body[key] : undefined) ??
137
+ (req.query && req.query[key] !== undefined ? req.query[key] : undefined) ??
138
+ (req.params && req.params[key] !== undefined ? req.params[key] : undefined);
139
+ return value !== undefined ? value : defaultValue;
140
+ }
141
+
142
+ /**
143
+ * Get a URL parameter. this.param(req, 'id')
144
+ */
145
+ param(req, key, defaultValue = null) {
146
+ return req.params?.[key] ?? defaultValue;
147
+ }
148
+
149
+ /**
150
+ * Get a query string value. this.query(req, 'page', 1)
151
+ */
152
+ query(req, key, defaultValue = null) {
153
+ if (key === undefined) return req.query || {};
154
+ return req.query?.[key] ?? defaultValue;
155
+ }
156
+
157
+ /**
158
+ * Merge body + query + params into one flat object.
159
+ */
160
+ all(req) {
161
+ return {
162
+ ...req.params,
163
+ ...req.query,
164
+ ...req.body,
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Return only the specified keys from the request.
170
+ *
171
+ * this.only(req, ['name', 'email'])
172
+ */
173
+ only(req, keys = []) {
174
+ const all = this.all(req);
175
+ return keys.reduce((acc, k) => {
176
+ if (k in all) acc[k] = all[k];
177
+ return acc;
178
+ }, {});
179
+ }
180
+
181
+ /**
182
+ * Return all request data except the specified keys.
183
+ *
184
+ * this.except(req, ['password', 'token'])
185
+ */
186
+ except(req, keys = []) {
187
+ const all = this.all(req);
188
+ return Object.fromEntries(
189
+ Object.entries(all).filter(([k]) => !keys.includes(k))
190
+ );
191
+ }
192
+
193
+ // ─── Validation ──────────────────────────────────────────────────────────────
194
+
195
+ /**
196
+ * Validate request data against a set of rules.
197
+ * Throws a 422 HttpError on failure (caught by the router).
198
+ *
199
+ * const data = await this.validate(req, {
200
+ * name: 'required|string|min:2|max:100',
201
+ * email: 'required|email',
202
+ * age: 'optional|number|min:0',
203
+ * });
204
+ *
205
+ * On success returns the validated + sanitised data object.
206
+ */
207
+ async validate(req, rules) {
208
+ const data = this.all(req);
209
+ const errors = {};
210
+
211
+ for (const [field, ruleString] of Object.entries(rules)) {
212
+ const fieldRules = ruleString.split('|').map(r => r.trim());
213
+ const value = data[field];
214
+ const fieldErrors = [];
215
+
216
+ for (const rule of fieldRules) {
217
+ const [ruleName, ruleArg] = rule.split(':');
218
+ const err = this._applyRule(field, value, ruleName, ruleArg);
219
+ if (err) fieldErrors.push(err);
220
+ }
221
+
222
+ if (fieldErrors.length) errors[field] = fieldErrors;
223
+ }
224
+
225
+ if (Object.keys(errors).length > 0) {
226
+ throw new HttpError(422, 'Validation failed', errors);
227
+ }
228
+
229
+ // Return only the validated fields
230
+ const validated = {};
231
+ for (const field of Object.keys(rules)) {
232
+ if (data[field] !== undefined) validated[field] = data[field];
233
+ }
234
+ return validated;
235
+ }
236
+
237
+ _applyRule(field, value, rule, arg) {
238
+ const isEmpty = value === undefined || value === null || value === '';
239
+
240
+ switch (rule) {
241
+ case 'required':
242
+ if (isEmpty) return `${field} is required`;
243
+ break;
244
+
245
+ case 'optional':
246
+ if (isEmpty) return null; // skip further checks if optional and empty
247
+ break;
248
+
249
+ case 'string':
250
+ if (!isEmpty && typeof value !== 'string')
251
+ return `${field} must be a string`;
252
+ break;
253
+
254
+ case 'number':
255
+ if (!isEmpty && (isNaN(Number(value))))
256
+ return `${field} must be a number`;
257
+ break;
258
+
259
+ case 'boolean':
260
+ if (!isEmpty && !['true', 'false', true, false, '1', '0', 1, 0].includes(value))
261
+ return `${field} must be a boolean`;
262
+ break;
263
+
264
+ case 'email': {
265
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
266
+ if (!isEmpty && !emailRegex.test(value))
267
+ return `${field} must be a valid email address`;
268
+ break;
269
+ }
270
+
271
+ case 'min':
272
+ if (!isEmpty) {
273
+ if (typeof value === 'string' && value.length < Number(arg))
274
+ return `${field} must be at least ${arg} characters`;
275
+ if (typeof value === 'number' && value < Number(arg))
276
+ return `${field} must be at least ${arg}`;
277
+ }
278
+ break;
279
+
280
+ case 'max':
281
+ if (!isEmpty) {
282
+ if (typeof value === 'string' && value.length > Number(arg))
283
+ return `${field} must not exceed ${arg} characters`;
284
+ if (typeof value === 'number' && value > Number(arg))
285
+ return `${field} must not exceed ${arg}`;
286
+ }
287
+ break;
288
+
289
+ case 'in': {
290
+ const allowed = arg.split(',');
291
+ if (!isEmpty && !allowed.includes(String(value)))
292
+ return `${field} must be one of: ${allowed.join(', ')}`;
293
+ break;
294
+ }
295
+
296
+ case 'alpha':
297
+ if (!isEmpty && !/^[a-zA-Z]+$/.test(value))
298
+ return `${field} must contain only letters`;
299
+ break;
300
+
301
+ case 'alphanumeric':
302
+ if (!isEmpty && !/^[a-zA-Z0-9]+$/.test(value))
303
+ return `${field} must contain only letters and numbers`;
304
+ break;
305
+
306
+ case 'url':
307
+ try {
308
+ if (!isEmpty) new URL(value);
309
+ } catch {
310
+ return `${field} must be a valid URL`;
311
+ }
312
+ break;
313
+
314
+ case 'confirmed': {
315
+ // Expects a matching field named `${field}_confirmation` in the request
316
+ // We store the raw req data on the instance during validate()
317
+ break;
318
+ }
319
+
320
+ default:
321
+ // Unknown rule — silently ignore (extensible in future)
322
+ break;
323
+ }
324
+
325
+ return null;
326
+ }
327
+
328
+ // ─── Pagination Helper ───────────────────────────────────────────────────────
329
+
330
+ /**
331
+ * Build a pagination envelope for list responses.
332
+ *
333
+ * return this.paginate(res, {
334
+ * data: users,
335
+ * total: 100,
336
+ * page: 2,
337
+ * perPage: 15,
338
+ * });
339
+ */
340
+ paginate(res, { data, total, page = 1, perPage = 15 }) {
341
+ const lastPage = Math.ceil(total / perPage);
342
+ return res.status(200).json({
343
+ data,
344
+ meta: {
345
+ total,
346
+ per_page: perPage,
347
+ current_page: Number(page),
348
+ last_page: lastPage,
349
+ from: (page - 1) * perPage + 1,
350
+ to: Math.min(page * perPage, total),
351
+ },
352
+ });
353
+ }
354
+
355
+ // ─── Internal ────────────────────────────────────────────────────────────────
356
+
357
+ _envelope(status, data) {
358
+ if (data === null) return { status };
359
+ // If data already has a recognised top-level shape, return as-is
360
+ if (typeof data === 'object' && !Array.isArray(data) && ('data' in data || 'message' in data)) {
361
+ return { status, ...data };
362
+ }
363
+ return { status, data };
364
+ }
365
+ }
366
+
367
+ module.exports = Controller;
@@ -0,0 +1,29 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * HttpError
5
+ *
6
+ * A structured error that carries an HTTP status code.
7
+ * Thrown by Controller.abort() and the validation system.
8
+ * Caught automatically by the Router's global error handler.
9
+ *
10
+ * Usage:
11
+ * throw new HttpError(404, 'User not found')
12
+ * throw new HttpError(422, 'Validation failed', { email: ['email is required'] })
13
+ */
14
+ class HttpError extends Error {
15
+ /**
16
+ * @param {number} status HTTP status code
17
+ * @param {string} message Human-readable message
18
+ * @param {object} errors Optional field-level errors (for validation)
19
+ */
20
+ constructor(status = 500, message = 'Internal Server Error', errors = null) {
21
+ super(message);
22
+ this.name = 'HttpError';
23
+ this.status = status;
24
+ this.statusCode = status;
25
+ this.errors = errors;
26
+ }
27
+ }
28
+
29
+ module.exports = HttpError;
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Event
5
+ *
6
+ * Base class for all Millas events.
7
+ *
8
+ * Usage:
9
+ * class UserRegistered extends Event {
10
+ * constructor(user) {
11
+ * super();
12
+ * this.user = user;
13
+ * this.timestamp = new Date();
14
+ * }
15
+ * }
16
+ *
17
+ * // Fire the event
18
+ * await emit(new UserRegistered(user));
19
+ */
20
+ class Event {
21
+ constructor() {
22
+ this._name = this.constructor.name;
23
+ this._timestamp = new Date().toISOString();
24
+ this._stopped = false;
25
+ }
26
+
27
+ /**
28
+ * Stop event propagation — subsequent listeners won't be called.
29
+ */
30
+ stopPropagation() {
31
+ this._stopped = true;
32
+ }
33
+
34
+ get name() { return this._name; }
35
+ get timestamp() { return this._timestamp; }
36
+ get stopped() { return this._stopped; }
37
+ }
38
+
39
+ module.exports = Event;
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * EventEmitter — Millas event bus.
5
+ */
6
+ class EventEmitter {
7
+ constructor() {
8
+ this._listeners = new Map();
9
+ this._wildcards = [];
10
+ this._queue = null;
11
+ }
12
+
13
+ listen(event, listeners) {
14
+ const name = this._name(event);
15
+ if (!this._listeners.has(name)) this._listeners.set(name, []);
16
+ const list = Array.isArray(listeners) ? listeners : [listeners];
17
+ for (const l of list) this._listeners.get(name).push({ handler: l, once: false });
18
+ return this;
19
+ }
20
+
21
+ on(event, fn) { return this.listen(event, fn); }
22
+
23
+ once(event, fn) {
24
+ const name = this._name(event);
25
+ if (!this._listeners.has(name)) this._listeners.set(name, []);
26
+ this._listeners.get(name).push({ handler: fn, once: true });
27
+ return this;
28
+ }
29
+
30
+ off(event, fn) {
31
+ const name = this._name(event);
32
+ if (!this._listeners.has(name)) return this;
33
+ this._listeners.set(name, this._listeners.get(name).filter(l => l.handler !== fn));
34
+ return this;
35
+ }
36
+
37
+ onWildcard(pattern, fn) {
38
+ const rx = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
39
+ this._wildcards.push({ rx, handler: fn });
40
+ return this;
41
+ }
42
+
43
+ async emit(event, data = {}) {
44
+ let ev = event;
45
+ if (typeof event === 'string') {
46
+ const Ev = require('./Event');
47
+ ev = Object.assign(new Ev(), { _name: event, ...data });
48
+ }
49
+ const name = ev._name || ev.constructor?.name || String(event);
50
+ const entries = [...(this._listeners.get(name) || [])];
51
+ const remove = [];
52
+
53
+ for (const entry of entries) {
54
+ if (ev.stopped) break;
55
+ await this._invoke(entry.handler, ev);
56
+ if (entry.once) remove.push(entry);
57
+ }
58
+ if (remove.length) {
59
+ this._listeners.set(name, (this._listeners.get(name) || []).filter(l => !remove.includes(l)));
60
+ }
61
+
62
+ for (const { rx, handler } of this._wildcards) {
63
+ if (ev.stopped) break;
64
+ if (rx.test(name)) await this._invoke(handler, ev);
65
+ }
66
+ return ev;
67
+ }
68
+
69
+ emitAsync(event, data = {}) {
70
+ Promise.resolve(this.emit(event, data)).catch(err =>
71
+ console.error('[EventEmitter] Unhandled error:', err.message)
72
+ );
73
+ }
74
+
75
+ hasListeners(event) { return (this._listeners.get(this._name(event)) || []).length > 0; }
76
+ getListeners(event) { return (this._listeners.get(this._name(event)) || []).map(l => l.handler); }
77
+ removeAll(event) { this._listeners.delete(this._name(event)); return this; }
78
+ flush() { this._listeners.clear(); this._wildcards = []; return this; }
79
+ setQueue(queue) { this._queue = queue; }
80
+
81
+ async _invoke(handler, event) {
82
+ // Listener class (has handle() on prototype)
83
+ if (typeof handler === 'function' && typeof handler.prototype?.handle === 'function') {
84
+ const inst = new handler();
85
+ if (handler.queue && this._queue) {
86
+ const Job = require('../queue/Job');
87
+ const q = this._queue;
88
+ class LJob extends Job {
89
+ async handle() { await inst.handle(event); }
90
+ async failed(e) { if (typeof inst.failed === 'function') await inst.failed(event, e); }
91
+ }
92
+ LJob.queue = handler.queueName || 'default';
93
+ await q.push(new LJob());
94
+ return;
95
+ }
96
+ try { await inst.handle(event); }
97
+ catch (e) {
98
+ if (typeof inst.failed === 'function') await inst.failed(event, e);
99
+ else throw e;
100
+ }
101
+ return;
102
+ }
103
+ // Instantiated listener object
104
+ if (handler && typeof handler === 'object' && typeof handler.handle === 'function') {
105
+ await handler.handle(event); return;
106
+ }
107
+ // Raw function
108
+ if (typeof handler === 'function') { await handler(event); return; }
109
+ throw new Error('Invalid listener: ' + handler);
110
+ }
111
+
112
+ _name(e) {
113
+ if (typeof e === 'string') return e;
114
+ if (typeof e === 'function') return e.name;
115
+ return e?._name || e?.constructor?.name || String(e);
116
+ }
117
+ }
118
+
119
+ // ── Singleton ─────────────────────────────────────────────────────────────────
120
+ const _inst = new EventEmitter();
121
+
122
+ // Standalone emit function — IMPORTANT: does NOT use module.exports.emit
123
+ // to avoid the circular reference where module.exports === _inst so
124
+ // _inst.emit gets overwritten by the wrapper.
125
+ async function emit(event, data) {
126
+ // Call the class method bound to _inst, bypassing any property overwrites
127
+ return EventEmitter.prototype.emit.call(_inst, event, data);
128
+ }
129
+
130
+ // Export a plain wrapper object (NOT the singleton itself) to prevent
131
+ // module.exports.emit from writing back onto _inst.
132
+ module.exports = {
133
+ // Proxy all EventEmitter instance methods to _inst
134
+ listen: (...a) => _inst.listen(...a),
135
+ on: (...a) => _inst.on(...a),
136
+ once: (...a) => _inst.once(...a),
137
+ off: (...a) => _inst.off(...a),
138
+ onWildcard: (...a) => _inst.onWildcard(...a),
139
+ emit: (...a) => EventEmitter.prototype.emit.call(_inst, ...a),
140
+ emitAsync: (...a) => _inst.emitAsync(...a),
141
+ hasListeners:(...a) => _inst.hasListeners(...a),
142
+ getListeners:(...a) => _inst.getListeners(...a),
143
+ removeAll: (...a) => _inst.removeAll(...a),
144
+ flush: () => _inst.flush(),
145
+ setQueue: (q) => _inst.setQueue(q),
146
+ // Named exports
147
+ EventEmitter,
148
+ emit,
149
+ // Expose singleton for advanced use
150
+ _instance: _inst,
151
+ };
@@ -0,0 +1,46 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Listener
5
+ *
6
+ * Base class for all event listeners.
7
+ *
8
+ * Usage:
9
+ * class SendWelcomeEmail extends Listener {
10
+ * static queue = true; // run this listener via the queue (Phase 9)
11
+ *
12
+ * async handle(event) {
13
+ * await Mail.send({
14
+ * to: event.user.email,
15
+ * subject: 'Welcome!',
16
+ * template: 'welcome',
17
+ * data: { name: event.user.name },
18
+ * });
19
+ * }
20
+ * }
21
+ *
22
+ * Register:
23
+ * EventEmitter.listen(UserRegistered, [SendWelcomeEmail, NotifyAdmin]);
24
+ */
25
+ class Listener {
26
+ /**
27
+ * Whether to run this listener via the queue.
28
+ * Set to true for slow operations (email, notifications, etc.)
29
+ */
30
+ static queue = false;
31
+
32
+ /**
33
+ * Handle the event.
34
+ * @param {Event} event
35
+ */
36
+ async handle(event) {
37
+ throw new Error(`${this.constructor.name} must implement handle(event)`);
38
+ }
39
+
40
+ /**
41
+ * Called when the listener fails.
42
+ */
43
+ async failed(event, error) {}
44
+ }
45
+
46
+ module.exports = Listener;
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ const emitterModule = require('./EventEmitter');
4
+ const EventEmitter = emitterModule.EventEmitter
5
+ ? emitterModule // if default export is singleton
6
+ : emitterModule;
7
+ const Event = require('./Event');
8
+ const Listener = require('./Listener');
9
+
10
+ module.exports = {
11
+ EventEmitter: emitterModule,
12
+ Event,
13
+ Listener,
14
+ emit: emitterModule.emit,
15
+ };