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,188 @@
1
+ 'use strict';
2
+
3
+ const Controller = require('../controller/Controller');
4
+ const Auth = require('./Auth');
5
+
6
+ /**
7
+ * AuthController
8
+ *
9
+ * Drop-in authentication controller.
10
+ * Register its routes in routes/api.js:
11
+ *
12
+ * const AuthController = require('millas/src/auth/AuthController');
13
+ *
14
+ * Route.post('/auth/register', AuthController, 'register');
15
+ * Route.post('/auth/login', AuthController, 'login');
16
+ * Route.post('/auth/logout', AuthController, 'logout');
17
+ * Route.get('/auth/me', AuthController, 'me');
18
+ * Route.post('/auth/refresh', AuthController, 'refresh');
19
+ * Route.post('/auth/forgot-password', AuthController, 'forgotPassword');
20
+ * Route.post('/auth/reset-password', AuthController, 'resetPassword');
21
+ *
22
+ * Or use the convenience helper:
23
+ * Route.auth() — registers all routes above under /auth
24
+ */
25
+ class AuthController extends Controller {
26
+
27
+ /**
28
+ * POST /auth/register
29
+ * Body: { name, email, password, password_confirmation? }
30
+ */
31
+ async register(req, res) {
32
+ const data = await this.validate(req, {
33
+ name: 'required|string|min:2|max:100',
34
+ email: 'required|email',
35
+ password: 'required|string|min:8',
36
+ });
37
+
38
+ const user = await Auth.register(data);
39
+ const token = Auth.issueToken(user);
40
+
41
+ return this.created(res, {
42
+ message: 'Registration successful',
43
+ user: this._safeUser(user),
44
+ token,
45
+ });
46
+ }
47
+
48
+ /**
49
+ * POST /auth/login
50
+ * Body: { email, password }
51
+ */
52
+ async login(req, res) {
53
+ const { email, password } = await this.validate(req, {
54
+ email: 'required|email',
55
+ password: 'required|string',
56
+ });
57
+
58
+ const { user, token, refreshToken } = await Auth.login(email, password);
59
+
60
+ return this.ok(res, {
61
+ message: 'Login successful',
62
+ user: this._safeUser(user),
63
+ token,
64
+ refresh_token: refreshToken,
65
+ });
66
+ }
67
+
68
+ /**
69
+ * POST /auth/logout
70
+ * Header: Authorization: Bearer <token>
71
+ *
72
+ * JWT is stateless — logout just instructs the client to discard the token.
73
+ * Phase 11 (cache) will add token blocklisting.
74
+ */
75
+ async logout(req, res) {
76
+ return this.ok(res, { message: 'Logged out successfully' });
77
+ }
78
+
79
+ /**
80
+ * GET /auth/me
81
+ * Header: Authorization: Bearer <token>
82
+ */
83
+ async me(req, res) {
84
+ try {
85
+ const user = await Auth.userOrFail(req);
86
+ return this.ok(res, { user: this._safeUser(user) });
87
+ } catch (err) {
88
+ if (err.status === 401) return this.unauthorized(res, err.message);
89
+ return this.unauthorized(res, 'Unauthenticated');
90
+ }
91
+ }
92
+
93
+ /**
94
+ * POST /auth/refresh
95
+ * Body: { refresh_token }
96
+ */
97
+ async refresh(req, res) {
98
+ const { refresh_token } = await this.validate(req, {
99
+ refresh_token: 'required|string',
100
+ });
101
+
102
+ // Verify the refresh token
103
+ let payload;
104
+ try {
105
+ payload = Auth.verify(refresh_token);
106
+ } catch {
107
+ return this.unauthorized(res, 'Invalid or expired refresh token');
108
+ }
109
+
110
+ const user = await Auth.user({ headers: { authorization: `Bearer ${refresh_token}` } });
111
+ if (!user) return this.unauthorized(res, 'User not found');
112
+
113
+ const newToken = Auth.issueToken(user);
114
+ const newRefreshToken = Auth.issueToken(user, { expiresIn: '30d' });
115
+
116
+ return this.ok(res, {
117
+ token: newToken,
118
+ refresh_token: newRefreshToken,
119
+ });
120
+ }
121
+
122
+ /**
123
+ * POST /auth/forgot-password
124
+ * Body: { email }
125
+ */
126
+ async forgotPassword(req, res) {
127
+ const { email } = await this.validate(req, {
128
+ email: 'required|email',
129
+ });
130
+
131
+ // Always return 200 to prevent email enumeration
132
+ try {
133
+ const user = await Auth.user({ headers: {} });
134
+ if (user) {
135
+ const resetToken = Auth.generateResetToken(user);
136
+ // Phase 8: Mail.send({ to: email, template: 'password-reset', data: { resetToken } })
137
+ // For now, return token in dev mode only
138
+ if (process.env.APP_ENV !== 'production') {
139
+ return this.ok(res, {
140
+ message: 'Reset link sent (dev mode — token exposed)',
141
+ reset_token: resetToken,
142
+ });
143
+ }
144
+ }
145
+ } catch { /* silent */ }
146
+
147
+ return this.ok(res, {
148
+ message: 'If that email exists, a reset link has been sent.',
149
+ });
150
+ }
151
+
152
+ /**
153
+ * POST /auth/reset-password
154
+ * Body: { token, password }
155
+ */
156
+ async resetPassword(req, res) {
157
+ const { token, password } = await this.validate(req, {
158
+ token: 'required|string',
159
+ password: 'required|string|min:8',
160
+ });
161
+
162
+ const payload = Auth.verifyResetToken(token);
163
+ const user = await Auth.user({
164
+ headers: { authorization: `Bearer ${token}` },
165
+ });
166
+
167
+ if (!user || user.email !== payload.email) {
168
+ return this.badRequest(res, 'Invalid reset token');
169
+ }
170
+
171
+ const hashed = await Auth.hashPassword(password);
172
+ await user.update({ password: hashed });
173
+
174
+ return this.ok(res, { message: 'Password reset successfully' });
175
+ }
176
+
177
+ // ─── Internal ─────────────────────────────────────────────────────────────
178
+
179
+ _safeUser(user) {
180
+ if (!user) return null;
181
+ const obj = user.toJSON ? user.toJSON() : { ...user };
182
+ delete obj.password;
183
+ delete obj.remember_token;
184
+ return obj;
185
+ }
186
+ }
187
+
188
+ module.exports = AuthController;
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const Middleware = require('../middleware/Middleware');
4
+ const HttpError = require('../errors/HttpError');
5
+ const Auth = require('./Auth');
6
+
7
+ /**
8
+ * AuthMiddleware
9
+ *
10
+ * Guards routes from unauthenticated access using JWT.
11
+ *
12
+ * Reads the Bearer token from the Authorization header,
13
+ * verifies it, loads the user from the database,
14
+ * and attaches them to req.user.
15
+ *
16
+ * Throws 401 if:
17
+ * - No Authorization header
18
+ * - Token is malformed / expired
19
+ * - User no longer exists in DB
20
+ *
21
+ * Register in bootstrap/app.js:
22
+ * app.middleware('auth', AuthMiddleware)
23
+ *
24
+ * Apply to routes:
25
+ * Route.prefix('/api').middleware(['auth']).group(() => { ... })
26
+ */
27
+ class AuthMiddleware extends Middleware {
28
+ async handle(req, res, next) {
29
+ const header = req.headers['authorization'];
30
+
31
+ if (!header) {
32
+ throw new HttpError(401, 'Authorization header missing');
33
+ }
34
+
35
+ if (!header.startsWith('Bearer ')) {
36
+ throw new HttpError(401, 'Invalid authorization format. Use: Bearer <token>');
37
+ }
38
+
39
+ const token = header.slice(7);
40
+ if (!token) {
41
+ throw new HttpError(401, 'Token is empty');
42
+ }
43
+
44
+ // Verify token — throws 401 if expired or invalid
45
+ const payload = Auth.verify(token);
46
+
47
+ // Load user from DB
48
+ let user;
49
+ try {
50
+ user = await Auth.user(req);
51
+ } catch {
52
+ throw new HttpError(401, 'Authentication service not configured');
53
+ }
54
+ if (!user) {
55
+ throw new HttpError(401, 'User not found or has been deleted');
56
+ }
57
+
58
+ // Attach to request
59
+ req.user = user;
60
+ req.token = token;
61
+ req.tokenPayload = payload;
62
+
63
+ next();
64
+ }
65
+ }
66
+
67
+ module.exports = AuthMiddleware;
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const bcrypt = require('bcryptjs');
4
+
5
+ /**
6
+ * Hasher
7
+ *
8
+ * Wraps bcrypt for password hashing and verification.
9
+ *
10
+ * Usage:
11
+ * const hash = await Hasher.make('my-password');
12
+ * const ok = await Hasher.check('my-password', hash);
13
+ */
14
+ class Hasher {
15
+ constructor(rounds = 12) {
16
+ this.rounds = rounds;
17
+ }
18
+
19
+ /**
20
+ * Hash a plain-text password.
21
+ * @param {string} plain
22
+ * @returns {Promise<string>}
23
+ */
24
+ async make(plain) {
25
+ return bcrypt.hash(String(plain), this.rounds);
26
+ }
27
+
28
+ /**
29
+ * Verify a plain-text password against a hash.
30
+ * @param {string} plain
31
+ * @param {string} hash
32
+ * @returns {Promise<boolean>}
33
+ */
34
+ async check(plain, hash) {
35
+ if (!plain || !hash) return false;
36
+ return bcrypt.compare(String(plain), String(hash));
37
+ }
38
+
39
+ /**
40
+ * Determine if a value needs to be re-hashed
41
+ * (e.g. rounds have changed).
42
+ */
43
+ needsRehash(hash) {
44
+ const rounds = bcrypt.getRounds(hash);
45
+ return rounds !== this.rounds;
46
+ }
47
+ }
48
+
49
+ // Singleton with default rounds
50
+ module.exports = new Hasher(12);
51
+ module.exports.Hasher = Hasher;
@@ -0,0 +1,74 @@
1
+ 'use strict';
2
+
3
+ const jwt = require('jsonwebtoken');
4
+
5
+ /**
6
+ * JwtDriver
7
+ *
8
+ * Issues and verifies JSON Web Tokens.
9
+ *
10
+ * Config (config/auth.js):
11
+ * guards: {
12
+ * jwt: {
13
+ * driver: 'jwt',
14
+ * secret: process.env.APP_KEY,
15
+ * expiresIn: '7d',
16
+ * }
17
+ * }
18
+ */
19
+ class JwtDriver {
20
+ constructor(config = {}) {
21
+ this.secret = config.secret || process.env.APP_KEY || 'millas-secret-change-me';
22
+ this.expiresIn = config.expiresIn || '7d';
23
+ this.algorithm = config.algorithm || 'HS256';
24
+ }
25
+
26
+ /**
27
+ * Sign a payload and return a token string.
28
+ * @param {object} payload — data to encode (e.g. { id, email, role })
29
+ * @param {object} options — override expiresIn etc.
30
+ */
31
+ sign(payload, options = {}) {
32
+ return jwt.sign(payload, this.secret, {
33
+ algorithm: this.algorithm,
34
+ expiresIn: options.expiresIn || this.expiresIn,
35
+ ...options,
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Verify a token and return the decoded payload.
41
+ * Throws if expired or invalid.
42
+ * @param {string} token
43
+ * @returns {object} decoded payload
44
+ */
45
+ verify(token) {
46
+ return jwt.verify(token, this.secret, { algorithms: [this.algorithm] });
47
+ }
48
+
49
+ /**
50
+ * Decode a token WITHOUT verifying the signature.
51
+ * Useful for inspecting expired tokens.
52
+ * @param {string} token
53
+ * @returns {object|null}
54
+ */
55
+ decode(token) {
56
+ return jwt.decode(token);
57
+ }
58
+
59
+ /**
60
+ * Sign a short-lived token for password resets.
61
+ */
62
+ signResetToken(payload) {
63
+ return this.sign(payload, { expiresIn: '1h' });
64
+ }
65
+
66
+ /**
67
+ * Sign a refresh token with a longer lifetime.
68
+ */
69
+ signRefreshToken(payload) {
70
+ return this.sign(payload, { expiresIn: '30d' });
71
+ }
72
+ }
73
+
74
+ module.exports = JwtDriver;
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const Middleware = require('../middleware/Middleware');
4
+ const HttpError = require('../errors/HttpError');
5
+
6
+ /**
7
+ * RoleMiddleware
8
+ *
9
+ * Restricts route access to users with specific roles.
10
+ * Must be used AFTER AuthMiddleware (requires req.user).
11
+ *
12
+ * Usage:
13
+ * middlewareRegistry.register('admin', new RoleMiddleware(['admin']));
14
+ * middlewareRegistry.register('staff', new RoleMiddleware(['admin', 'staff']));
15
+ *
16
+ * Route.prefix('/admin').middleware(['auth', 'admin']).group(() => { ... });
17
+ */
18
+ class RoleMiddleware extends Middleware {
19
+ /**
20
+ * @param {string[]} roles — list of allowed role values
21
+ */
22
+ constructor(roles = []) {
23
+ super();
24
+ this.roles = Array.isArray(roles) ? roles : [roles];
25
+ }
26
+
27
+ async handle(req, res, next) {
28
+ if (!req.user) {
29
+ throw new HttpError(401, 'Unauthenticated — run AuthMiddleware before RoleMiddleware');
30
+ }
31
+
32
+ const userRole = req.user.role || null;
33
+
34
+ if (!userRole || !this.roles.includes(userRole)) {
35
+ throw new HttpError(403,
36
+ `Access denied. Required role: ${this.roles.join(' or ')}`
37
+ );
38
+ }
39
+
40
+ next();
41
+ }
42
+ }
43
+
44
+ module.exports = RoleMiddleware;
@@ -0,0 +1,231 @@
1
+ 'use strict';
2
+
3
+ const MemoryDriver = require('./drivers/MemoryDriver');
4
+
5
+ /**
6
+ * Cache
7
+ *
8
+ * The primary caching facade.
9
+ *
10
+ * Usage:
11
+ * const { Cache } = require('millas/src');
12
+ *
13
+ * await Cache.set('user:1', user, 300); // cache for 5 minutes
14
+ * const user = await Cache.get('user:1'); // retrieve
15
+ * await Cache.delete('user:1');
16
+ *
17
+ * // remember() — get or compute
18
+ * const posts = await Cache.remember('posts:all', 60, async () => {
19
+ * return Post.all();
20
+ * });
21
+ *
22
+ * // Tags — group related keys
23
+ * await Cache.tags('users').set('user:1', user, 300);
24
+ * await Cache.tags('users').flush(); // clear all user keys
25
+ */
26
+ class Cache {
27
+ constructor() {
28
+ this._driver = null;
29
+ this._config = null;
30
+ this._prefix = '';
31
+ }
32
+
33
+ // ─── Configuration ─────────────────────────────────────────────────────────
34
+
35
+ configure(config) {
36
+ this._config = config;
37
+ this._prefix = config.prefix || '';
38
+ this._driver = null; // reset so driver is rebuilt
39
+ }
40
+
41
+ // ─── Core API ──────────────────────────────────────────────────────────────
42
+
43
+ /**
44
+ * Store a value in the cache.
45
+ * @param {string} key
46
+ * @param {*} value — must be JSON-serialisable
47
+ * @param {number} ttl — seconds (0 = forever)
48
+ */
49
+ async set(key, value, ttl = 0) {
50
+ return this._getDriver().set(this._k(key), value, ttl);
51
+ }
52
+
53
+ /**
54
+ * Retrieve a value. Returns null if missing or expired.
55
+ */
56
+ async get(key, defaultValue = null) {
57
+ const val = await this._getDriver().get(this._k(key));
58
+ return val !== null ? val : defaultValue;
59
+ }
60
+
61
+ /**
62
+ * Check if a key exists and is not expired.
63
+ */
64
+ async has(key) {
65
+ return this._getDriver().has(this._k(key));
66
+ }
67
+
68
+ /**
69
+ * Delete a key.
70
+ */
71
+ async delete(key) {
72
+ return this._getDriver().delete(this._k(key));
73
+ }
74
+
75
+ /**
76
+ * Delete all keys matching a prefix pattern.
77
+ */
78
+ async deletePattern(prefix) {
79
+ return this._getDriver().deletePattern(this._k(prefix));
80
+ }
81
+
82
+ /**
83
+ * Flush the entire cache.
84
+ */
85
+ async flush() {
86
+ return this._getDriver().flush();
87
+ }
88
+
89
+ /**
90
+ * Get or compute and cache.
91
+ *
92
+ * const data = await Cache.remember('key', 300, () => fetchExpensiveData());
93
+ */
94
+ async remember(key, ttl, fn) {
95
+ return this._getDriver().remember(this._k(key), ttl, fn);
96
+ }
97
+
98
+ /**
99
+ * Get once and delete — useful for flash messages / one-time tokens.
100
+ */
101
+ async pull(key) {
102
+ const value = await this.get(key);
103
+ if (value !== null) await this.delete(key);
104
+ return value;
105
+ }
106
+
107
+ /**
108
+ * Increment a numeric value.
109
+ */
110
+ async increment(key, amount = 1) {
111
+ return this._getDriver().increment(this._k(key), amount);
112
+ }
113
+
114
+ /**
115
+ * Decrement a numeric value.
116
+ */
117
+ async decrement(key, amount = 1) {
118
+ return this._getDriver().decrement(this._k(key), amount);
119
+ }
120
+
121
+ /**
122
+ * Add only if key does not exist. Returns true if stored, false if skipped.
123
+ */
124
+ async add(key, value, ttl = 0) {
125
+ return this._getDriver().add(this._k(key), value, ttl);
126
+ }
127
+
128
+ /**
129
+ * Retrieve multiple keys at once.
130
+ * Returns { key: value|null, ... }
131
+ */
132
+ async getMany(keys) {
133
+ return this._getDriver().getMany(keys.map(k => this._k(k)));
134
+ }
135
+
136
+ /**
137
+ * Set multiple key/value pairs at once.
138
+ */
139
+ async setMany(entries, ttl = 0) {
140
+ const prefixed = Object.fromEntries(
141
+ Object.entries(entries).map(([k, v]) => [this._k(k), v])
142
+ );
143
+ return this._getDriver().setMany(prefixed, ttl);
144
+ }
145
+
146
+ /**
147
+ * Cache with tag grouping — lets you flush groups of related keys.
148
+ *
149
+ * await Cache.tags('users').set('user:1', user, 300);
150
+ * await Cache.tags('users', 'posts').flush(); // flush all tagged keys
151
+ */
152
+ tags(...tagNames) {
153
+ return new TaggedCache(this, tagNames.flat());
154
+ }
155
+
156
+ /**
157
+ * Return the underlying driver instance.
158
+ */
159
+ driver() {
160
+ return this._getDriver();
161
+ }
162
+
163
+ // ─── Internal ──────────────────────────────────────────────────────────────
164
+
165
+ _k(key) {
166
+ return this._prefix ? `${this._prefix}:${key}` : String(key);
167
+ }
168
+
169
+ _getDriver() {
170
+ if (this._driver) return this._driver;
171
+
172
+ const name = this._config?.default || process.env.CACHE_DRIVER || 'memory';
173
+ const conf = this._config?.drivers?.[name] || {};
174
+
175
+ switch (name) {
176
+ case 'file': {
177
+ const FileDriver = require('./drivers/FileDriver');
178
+ this._driver = new FileDriver(conf);
179
+ break;
180
+ }
181
+ case 'null': {
182
+ const NullDriver = require('./drivers/NullDriver');
183
+ this._driver = new NullDriver();
184
+ break;
185
+ }
186
+ case 'memory':
187
+ default:
188
+ this._driver = new MemoryDriver();
189
+ }
190
+
191
+ return this._driver;
192
+ }
193
+ }
194
+
195
+ // ── TaggedCache ───────────────────────────────────────────────────────────────
196
+
197
+ /**
198
+ * TaggedCache
199
+ *
200
+ * Wraps a Cache instance and prefixes every key with a tag namespace.
201
+ * Flush all keys for a tag group with .flush().
202
+ */
203
+ class TaggedCache {
204
+ constructor(cache, tags) {
205
+ this._cache = cache;
206
+ this._tags = tags;
207
+ this._prefix = tags.join(':');
208
+ }
209
+
210
+ _k(key) { return `tag:${this._prefix}:${key}`; }
211
+
212
+ async set(key, value, ttl = 0) { return this._cache.set(this._k(key), value, ttl); }
213
+ async get(key, def = null) { return this._cache.get(this._k(key), def); }
214
+ async has(key) { return this._cache.has(this._k(key)); }
215
+ async delete(key) { return this._cache.delete(this._k(key)); }
216
+ async remember(key, ttl, fn) { return this._cache.remember(this._k(key), ttl, fn); }
217
+ async pull(key) { return this._cache.pull(this._k(key)); }
218
+
219
+ /**
220
+ * Flush all keys belonging to these tags.
221
+ */
222
+ async flush() {
223
+ return this._cache.deletePattern(`tag:${this._prefix}:`);
224
+ }
225
+ }
226
+
227
+ // Singleton
228
+ const cache = new Cache();
229
+ module.exports = cache;
230
+ module.exports.Cache = Cache;
231
+ module.exports.TaggedCache = TaggedCache;