@zero-server/sdk 0.9.1 → 0.9.3
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/LICENSE +21 -21
- package/README.md +460 -443
- package/index.js +414 -412
- package/lib/app.js +1172 -1172
- package/lib/auth/authorize.js +399 -399
- package/lib/auth/enrollment.js +367 -367
- package/lib/auth/index.js +57 -57
- package/lib/auth/jwt.js +731 -731
- package/lib/auth/oauth.js +362 -362
- package/lib/auth/session.js +588 -588
- package/lib/auth/trustedDevice.js +409 -409
- package/lib/auth/twoFactor.js +1150 -1150
- package/lib/auth/webauthn.js +946 -946
- package/lib/body/index.js +14 -14
- package/lib/body/json.js +109 -109
- package/lib/body/multipart.js +440 -440
- package/lib/body/raw.js +71 -71
- package/lib/body/rawBuffer.js +160 -160
- package/lib/body/sendError.js +25 -25
- package/lib/body/text.js +75 -75
- package/lib/body/typeMatch.js +41 -41
- package/lib/body/urlencoded.js +235 -235
- package/lib/cli.js +845 -845
- package/lib/cluster.js +666 -666
- package/lib/debug.js +372 -372
- package/lib/env/index.js +465 -465
- package/lib/errors.js +683 -683
- package/lib/fetch/index.js +256 -256
- package/lib/grpc/balancer.js +378 -378
- package/lib/grpc/call.js +708 -708
- package/lib/grpc/client.js +764 -764
- package/lib/grpc/codec.js +1221 -1221
- package/lib/grpc/credentials.js +398 -398
- package/lib/grpc/frame.js +262 -262
- package/lib/grpc/health.js +287 -287
- package/lib/grpc/index.js +121 -121
- package/lib/grpc/metadata.js +461 -461
- package/lib/grpc/proto.js +821 -821
- package/lib/grpc/reflection.js +590 -590
- package/lib/grpc/server.js +445 -445
- package/lib/grpc/status.js +118 -118
- package/lib/grpc/watch.js +173 -173
- package/lib/http/index.js +10 -10
- package/lib/http/request.js +727 -727
- package/lib/http/response.js +799 -799
- package/lib/lifecycle.js +557 -557
- package/lib/middleware/compress.js +230 -230
- package/lib/middleware/cookieParser.js +237 -237
- package/lib/middleware/cors.js +93 -93
- package/lib/middleware/csrf.js +137 -137
- package/lib/middleware/errorHandler.js +101 -101
- package/lib/middleware/helmet.js +175 -175
- package/lib/middleware/index.js +19 -17
- package/lib/middleware/logger.js +74 -74
- package/lib/middleware/rateLimit.js +88 -88
- package/lib/middleware/requestId.js +53 -53
- package/lib/middleware/static.js +326 -326
- package/lib/middleware/timeout.js +71 -71
- package/lib/middleware/validator.js +255 -255
- package/lib/observe/health.js +326 -326
- package/lib/observe/index.js +50 -50
- package/lib/observe/logger.js +359 -359
- package/lib/observe/metrics.js +805 -805
- package/lib/observe/tracing.js +592 -592
- package/lib/orm/adapters/json.js +290 -290
- package/lib/orm/adapters/memory.js +764 -764
- package/lib/orm/adapters/mongo.js +764 -764
- package/lib/orm/adapters/mysql.js +933 -933
- package/lib/orm/adapters/postgres.js +1144 -1144
- package/lib/orm/adapters/redis.js +1534 -1534
- package/lib/orm/adapters/sql-base.js +212 -212
- package/lib/orm/adapters/sqlite.js +858 -858
- package/lib/orm/audit.js +649 -649
- package/lib/orm/cache.js +394 -394
- package/lib/orm/geo.js +387 -387
- package/lib/orm/index.js +784 -784
- package/lib/orm/migrate.js +432 -432
- package/lib/orm/model.js +1706 -1706
- package/lib/orm/plugin.js +375 -375
- package/lib/orm/procedures.js +836 -836
- package/lib/orm/profiler.js +233 -233
- package/lib/orm/query.js +1772 -1772
- package/lib/orm/replicas.js +241 -241
- package/lib/orm/schema.js +307 -307
- package/lib/orm/search.js +380 -380
- package/lib/orm/seed/data/commerce.js +136 -136
- package/lib/orm/seed/data/internet.js +111 -111
- package/lib/orm/seed/data/locations.js +204 -204
- package/lib/orm/seed/data/names.js +338 -338
- package/lib/orm/seed/data/person.js +128 -128
- package/lib/orm/seed/data/phone.js +211 -211
- package/lib/orm/seed/data/words.js +134 -134
- package/lib/orm/seed/factory.js +178 -178
- package/lib/orm/seed/fake.js +1186 -1186
- package/lib/orm/seed/index.js +18 -18
- package/lib/orm/seed/rng.js +70 -70
- package/lib/orm/seed/seeder.js +124 -124
- package/lib/orm/seed/unique.js +68 -68
- package/lib/orm/snapshot.js +366 -366
- package/lib/orm/tenancy.js +605 -605
- package/lib/orm/views.js +350 -350
- package/lib/router/index.js +436 -436
- package/lib/sse/index.js +8 -8
- package/lib/sse/stream.js +349 -349
- package/lib/ws/connection.js +451 -451
- package/lib/ws/handshake.js +125 -125
- package/lib/ws/index.js +14 -14
- package/lib/ws/room.js +223 -223
- package/package.json +73 -73
- package/types/app.d.ts +223 -223
- package/types/auth.d.ts +520 -520
- package/types/body.d.ts +14 -0
- package/types/cli.d.ts +2 -0
- package/types/cluster.d.ts +75 -75
- package/types/env.d.ts +80 -80
- package/types/errors.d.ts +316 -316
- package/types/fetch.d.ts +43 -43
- package/types/grpc.d.ts +432 -432
- package/types/index.d.ts +384 -384
- package/types/lifecycle.d.ts +60 -60
- package/types/middleware.d.ts +320 -320
- package/types/observe.d.ts +304 -304
- package/types/orm.d.ts +1887 -1887
- package/types/request.d.ts +109 -109
- package/types/response.d.ts +157 -157
- package/types/router.d.ts +78 -78
- package/types/sse.d.ts +78 -78
- package/types/websocket.d.ts +126 -126
package/lib/orm/plugin.js
CHANGED
|
@@ -1,375 +1,375 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module orm/plugin
|
|
3
|
-
* @description Plugin system for the zero-server ORM.
|
|
4
|
-
* Provides a registration API, lifecycle hooks, and
|
|
5
|
-
* a standard interface for extending the framework.
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* const { PluginManager } = require('@zero-server/sdk');
|
|
9
|
-
*
|
|
10
|
-
* // Define a plugin
|
|
11
|
-
* const timestampPlugin = {
|
|
12
|
-
* name: 'timestamps',
|
|
13
|
-
* version: '1.0.0',
|
|
14
|
-
* install(manager, options) {
|
|
15
|
-
* manager.hook('beforeCreate', (model, data) => {
|
|
16
|
-
* data.createdAt = new Date().toISOString();
|
|
17
|
-
* return data;
|
|
18
|
-
* });
|
|
19
|
-
* },
|
|
20
|
-
* };
|
|
21
|
-
*
|
|
22
|
-
* // Register it
|
|
23
|
-
* const plugins = new PluginManager(db);
|
|
24
|
-
* plugins.register(timestampPlugin);
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
const log = require('../debug')('zero:orm:plugin');
|
|
28
|
-
|
|
29
|
-
// -- Plugin Manager ------------------------------------------
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Plugin registration and lifecycle manager.
|
|
33
|
-
* Plugins can hook into ORM events and extend functionality.
|
|
34
|
-
*/
|
|
35
|
-
class PluginManager
|
|
36
|
-
{
|
|
37
|
-
/**
|
|
38
|
-
* @constructor
|
|
39
|
-
* @param {import('./index').Database} [db] - Database instance (optional, can be set later).
|
|
40
|
-
*
|
|
41
|
-
* @example
|
|
42
|
-
* const plugins = new PluginManager(db);
|
|
43
|
-
*/
|
|
44
|
-
constructor(db)
|
|
45
|
-
{
|
|
46
|
-
/** @type {import('./index').Database|null} */
|
|
47
|
-
this.db = db || null;
|
|
48
|
-
|
|
49
|
-
/** @type {Map<string, object>} Registered plugins keyed by name. */
|
|
50
|
-
this._plugins = new Map();
|
|
51
|
-
|
|
52
|
-
/** @type {Map<string, Function[]>} Hook listeners keyed by hook name. */
|
|
53
|
-
this._hooks = new Map();
|
|
54
|
-
|
|
55
|
-
/** @type {Map<string, object>} Plugin options keyed by name. */
|
|
56
|
-
this._options = new Map();
|
|
57
|
-
|
|
58
|
-
/** @type {boolean} Whether the manager has been booted. */
|
|
59
|
-
this._booted = false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// -- Registration ------------------------------------
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Register a plugin.
|
|
66
|
-
*
|
|
67
|
-
* @param {object} plugin - Plugin definition object.
|
|
68
|
-
* @param {string} plugin.name - Unique plugin name.
|
|
69
|
-
* @param {string} [plugin.version] - Plugin version string.
|
|
70
|
-
* @param {Function} plugin.install - Install function `(manager, options) => {}`.
|
|
71
|
-
* @param {Function} [plugin.boot] - Boot function called after all plugins are registered.
|
|
72
|
-
* @param {Function} [plugin.uninstall] - Cleanup function.
|
|
73
|
-
* @param {string[]} [plugin.dependencies] - Required plugin names.
|
|
74
|
-
* @param {object} [options] - Plugin-specific options.
|
|
75
|
-
* @returns {PluginManager} this (for chaining)
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* plugins.register({
|
|
79
|
-
* name: 'soft-delete',
|
|
80
|
-
* install(manager) {
|
|
81
|
-
* manager.hook('beforeDelete', (model, instance) => {
|
|
82
|
-
* // intercept delete
|
|
83
|
-
* });
|
|
84
|
-
* },
|
|
85
|
-
* });
|
|
86
|
-
*/
|
|
87
|
-
register(plugin, options = {})
|
|
88
|
-
{
|
|
89
|
-
if (!plugin || typeof plugin !== 'object')
|
|
90
|
-
{
|
|
91
|
-
throw new Error('Plugin must be an object');
|
|
92
|
-
}
|
|
93
|
-
if (!plugin.name || typeof plugin.name !== 'string')
|
|
94
|
-
{
|
|
95
|
-
throw new Error('Plugin must have a "name" string property');
|
|
96
|
-
}
|
|
97
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(plugin.name))
|
|
98
|
-
{
|
|
99
|
-
throw new Error(`Invalid plugin name: "${plugin.name}"`);
|
|
100
|
-
}
|
|
101
|
-
if (typeof plugin.install !== 'function')
|
|
102
|
-
{
|
|
103
|
-
throw new Error(`Plugin "${plugin.name}" must have an "install" function`);
|
|
104
|
-
}
|
|
105
|
-
if (this._plugins.has(plugin.name))
|
|
106
|
-
{
|
|
107
|
-
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Check dependencies
|
|
111
|
-
if (Array.isArray(plugin.dependencies))
|
|
112
|
-
{
|
|
113
|
-
for (const dep of plugin.dependencies)
|
|
114
|
-
{
|
|
115
|
-
if (!this._plugins.has(dep))
|
|
116
|
-
{
|
|
117
|
-
throw new Error(`Plugin "${plugin.name}" requires "${dep}" which is not registered`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
this._plugins.set(plugin.name, plugin);
|
|
123
|
-
this._options.set(plugin.name, options);
|
|
124
|
-
|
|
125
|
-
// Install immediately
|
|
126
|
-
plugin.install(this, options);
|
|
127
|
-
|
|
128
|
-
log('plugin registered', plugin.name, plugin.version || '');
|
|
129
|
-
return this;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Register multiple plugins at once.
|
|
134
|
-
*
|
|
135
|
-
* @param {...(object|[object, object])} plugins - Plugin objects or [plugin, options] tuples.
|
|
136
|
-
* @returns {PluginManager} this (for chaining)
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* plugins.registerAll(
|
|
140
|
-
* pluginA, // no options
|
|
141
|
-
* [pluginB, { key: 'value' }], // with options
|
|
142
|
-
* );
|
|
143
|
-
*/
|
|
144
|
-
registerAll(...plugins)
|
|
145
|
-
{
|
|
146
|
-
for (const entry of plugins)
|
|
147
|
-
{
|
|
148
|
-
if (Array.isArray(entry))
|
|
149
|
-
{
|
|
150
|
-
this.register(entry[0], entry[1] || {});
|
|
151
|
-
}
|
|
152
|
-
else
|
|
153
|
-
{
|
|
154
|
-
this.register(entry);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return this;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Unregister a plugin by name.
|
|
162
|
-
*
|
|
163
|
-
* @param {string} name - Plugin name.
|
|
164
|
-
* @returns {PluginManager} this (for chaining)
|
|
165
|
-
*/
|
|
166
|
-
unregister(name)
|
|
167
|
-
{
|
|
168
|
-
const plugin = this._plugins.get(name);
|
|
169
|
-
if (!plugin) return this;
|
|
170
|
-
|
|
171
|
-
if (typeof plugin.uninstall === 'function')
|
|
172
|
-
{
|
|
173
|
-
plugin.uninstall(this);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
this._plugins.delete(name);
|
|
177
|
-
this._options.delete(name);
|
|
178
|
-
log('plugin unregistered', name);
|
|
179
|
-
return this;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// -- Lifecycle ---------------------------------------
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Boot all registered plugins.
|
|
186
|
-
* Calls the `boot()` method on each plugin (if defined).
|
|
187
|
-
* Should be called after all plugins are registered.
|
|
188
|
-
*
|
|
189
|
-
* @returns {Promise<PluginManager>} this (for chaining)
|
|
190
|
-
*
|
|
191
|
-
* @example
|
|
192
|
-
* plugins.register(pluginA).register(pluginB);
|
|
193
|
-
* await plugins.boot();
|
|
194
|
-
*/
|
|
195
|
-
async boot()
|
|
196
|
-
{
|
|
197
|
-
if (this._booted) return this;
|
|
198
|
-
|
|
199
|
-
for (const [name, plugin] of this._plugins)
|
|
200
|
-
{
|
|
201
|
-
if (typeof plugin.boot === 'function')
|
|
202
|
-
{
|
|
203
|
-
await plugin.boot(this, this._options.get(name) || {});
|
|
204
|
-
log('plugin booted', name);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
this._booted = true;
|
|
209
|
-
return this;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// -- Hook System -------------------------------------
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Register a hook listener.
|
|
216
|
-
*
|
|
217
|
-
* @param {string} name - Hook name (e.g. 'beforeCreate', 'afterUpdate').
|
|
218
|
-
* @param {Function} callback - Hook callback function.
|
|
219
|
-
* @returns {PluginManager} this (for chaining)
|
|
220
|
-
*
|
|
221
|
-
* @example
|
|
222
|
-
* manager.hook('beforeCreate', (model, data) => {
|
|
223
|
-
* data.slug = slugify(data.title);
|
|
224
|
-
* return data;
|
|
225
|
-
* });
|
|
226
|
-
*/
|
|
227
|
-
hook(name, callback)
|
|
228
|
-
{
|
|
229
|
-
if (typeof callback !== 'function')
|
|
230
|
-
{
|
|
231
|
-
throw new Error('Hook callback must be a function');
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (!this._hooks.has(name))
|
|
235
|
-
{
|
|
236
|
-
this._hooks.set(name, []);
|
|
237
|
-
}
|
|
238
|
-
this._hooks.get(name).push(callback);
|
|
239
|
-
return this;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Remove a hook listener.
|
|
244
|
-
*
|
|
245
|
-
* @param {string} name - Hook name.
|
|
246
|
-
* @param {Function} callback - The exact function reference to remove.
|
|
247
|
-
* @returns {PluginManager} this (for chaining)
|
|
248
|
-
*/
|
|
249
|
-
unhook(name, callback)
|
|
250
|
-
{
|
|
251
|
-
const handlers = this._hooks.get(name);
|
|
252
|
-
if (!handlers) return this;
|
|
253
|
-
|
|
254
|
-
const idx = handlers.indexOf(callback);
|
|
255
|
-
if (idx !== -1) handlers.splice(idx, 1);
|
|
256
|
-
return this;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Execute all listeners for a hook.
|
|
261
|
-
* Listeners run in registration order. If a listener returns a value,
|
|
262
|
-
* that value is passed to the next listener as the payload.
|
|
263
|
-
*
|
|
264
|
-
* @param {string} name - Hook name.
|
|
265
|
-
* @param {...*} args - Arguments to pass to hook callbacks.
|
|
266
|
-
* @returns {Promise<*>} Final transformed value (or last arg if no transforms).
|
|
267
|
-
*
|
|
268
|
-
* @example
|
|
269
|
-
* const data = await manager.runHook('beforeCreate', model, rawData);
|
|
270
|
-
*/
|
|
271
|
-
async runHook(name, ...args)
|
|
272
|
-
{
|
|
273
|
-
const handlers = this._hooks.get(name);
|
|
274
|
-
if (!handlers || handlers.length === 0) return args[args.length - 1];
|
|
275
|
-
|
|
276
|
-
let result = args[args.length - 1];
|
|
277
|
-
for (const fn of handlers)
|
|
278
|
-
{
|
|
279
|
-
const out = await fn(...args.slice(0, -1), result);
|
|
280
|
-
if (out !== undefined) result = out;
|
|
281
|
-
}
|
|
282
|
-
return result;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Check if any listeners exist for a hook.
|
|
287
|
-
*
|
|
288
|
-
* @param {string} name - Hook name.
|
|
289
|
-
* @returns {boolean}
|
|
290
|
-
*/
|
|
291
|
-
hasHook(name)
|
|
292
|
-
{
|
|
293
|
-
const handlers = this._hooks.get(name);
|
|
294
|
-
return !!handlers && handlers.length > 0;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// -- Query -------------------------------------------
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Check if a plugin is registered.
|
|
301
|
-
*
|
|
302
|
-
* @param {string} name - Plugin name.
|
|
303
|
-
* @returns {boolean}
|
|
304
|
-
*/
|
|
305
|
-
has(name)
|
|
306
|
-
{
|
|
307
|
-
return this._plugins.has(name);
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Get a registered plugin by name.
|
|
312
|
-
*
|
|
313
|
-
* @param {string} name - Plugin name.
|
|
314
|
-
* @returns {object|undefined} Plugin object, or undefined.
|
|
315
|
-
*/
|
|
316
|
-
get(name)
|
|
317
|
-
{
|
|
318
|
-
return this._plugins.get(name);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* Get options for a registered plugin.
|
|
323
|
-
*
|
|
324
|
-
* @param {string} name - Plugin name.
|
|
325
|
-
* @returns {object|undefined}
|
|
326
|
-
*/
|
|
327
|
-
getOptions(name)
|
|
328
|
-
{
|
|
329
|
-
return this._options.get(name);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* List all registered plugin names.
|
|
334
|
-
*
|
|
335
|
-
* @returns {string[]}
|
|
336
|
-
*
|
|
337
|
-
* @example
|
|
338
|
-
* plugins.list(); // ['timestamps', 'soft-delete']
|
|
339
|
-
*/
|
|
340
|
-
list()
|
|
341
|
-
{
|
|
342
|
-
return [...this._plugins.keys()];
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Get detailed info about all registered plugins.
|
|
347
|
-
*
|
|
348
|
-
* @returns {Array<{name: string, version: string, hasBootFn: boolean}>}
|
|
349
|
-
*/
|
|
350
|
-
info()
|
|
351
|
-
{
|
|
352
|
-
const result = [];
|
|
353
|
-
for (const [name, plugin] of this._plugins)
|
|
354
|
-
{
|
|
355
|
-
result.push({
|
|
356
|
-
name,
|
|
357
|
-
version: plugin.version || '0.0.0',
|
|
358
|
-
hasBootFn: typeof plugin.boot === 'function',
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
return result;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Number of registered plugins.
|
|
366
|
-
*
|
|
367
|
-
* @type {number}
|
|
368
|
-
*/
|
|
369
|
-
get size()
|
|
370
|
-
{
|
|
371
|
-
return this._plugins.size;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
module.exports = { PluginManager };
|
|
1
|
+
/**
|
|
2
|
+
* @module orm/plugin
|
|
3
|
+
* @description Plugin system for the zero-server ORM.
|
|
4
|
+
* Provides a registration API, lifecycle hooks, and
|
|
5
|
+
* a standard interface for extending the framework.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { PluginManager } = require('@zero-server/sdk');
|
|
9
|
+
*
|
|
10
|
+
* // Define a plugin
|
|
11
|
+
* const timestampPlugin = {
|
|
12
|
+
* name: 'timestamps',
|
|
13
|
+
* version: '1.0.0',
|
|
14
|
+
* install(manager, options) {
|
|
15
|
+
* manager.hook('beforeCreate', (model, data) => {
|
|
16
|
+
* data.createdAt = new Date().toISOString();
|
|
17
|
+
* return data;
|
|
18
|
+
* });
|
|
19
|
+
* },
|
|
20
|
+
* };
|
|
21
|
+
*
|
|
22
|
+
* // Register it
|
|
23
|
+
* const plugins = new PluginManager(db);
|
|
24
|
+
* plugins.register(timestampPlugin);
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const log = require('../debug')('zero:orm:plugin');
|
|
28
|
+
|
|
29
|
+
// -- Plugin Manager ------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Plugin registration and lifecycle manager.
|
|
33
|
+
* Plugins can hook into ORM events and extend functionality.
|
|
34
|
+
*/
|
|
35
|
+
class PluginManager
|
|
36
|
+
{
|
|
37
|
+
/**
|
|
38
|
+
* @constructor
|
|
39
|
+
* @param {import('./index').Database} [db] - Database instance (optional, can be set later).
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const plugins = new PluginManager(db);
|
|
43
|
+
*/
|
|
44
|
+
constructor(db)
|
|
45
|
+
{
|
|
46
|
+
/** @type {import('./index').Database|null} */
|
|
47
|
+
this.db = db || null;
|
|
48
|
+
|
|
49
|
+
/** @type {Map<string, object>} Registered plugins keyed by name. */
|
|
50
|
+
this._plugins = new Map();
|
|
51
|
+
|
|
52
|
+
/** @type {Map<string, Function[]>} Hook listeners keyed by hook name. */
|
|
53
|
+
this._hooks = new Map();
|
|
54
|
+
|
|
55
|
+
/** @type {Map<string, object>} Plugin options keyed by name. */
|
|
56
|
+
this._options = new Map();
|
|
57
|
+
|
|
58
|
+
/** @type {boolean} Whether the manager has been booted. */
|
|
59
|
+
this._booted = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// -- Registration ------------------------------------
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Register a plugin.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} plugin - Plugin definition object.
|
|
68
|
+
* @param {string} plugin.name - Unique plugin name.
|
|
69
|
+
* @param {string} [plugin.version] - Plugin version string.
|
|
70
|
+
* @param {Function} plugin.install - Install function `(manager, options) => {}`.
|
|
71
|
+
* @param {Function} [plugin.boot] - Boot function called after all plugins are registered.
|
|
72
|
+
* @param {Function} [plugin.uninstall] - Cleanup function.
|
|
73
|
+
* @param {string[]} [plugin.dependencies] - Required plugin names.
|
|
74
|
+
* @param {object} [options] - Plugin-specific options.
|
|
75
|
+
* @returns {PluginManager} this (for chaining)
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* plugins.register({
|
|
79
|
+
* name: 'soft-delete',
|
|
80
|
+
* install(manager) {
|
|
81
|
+
* manager.hook('beforeDelete', (model, instance) => {
|
|
82
|
+
* // intercept delete
|
|
83
|
+
* });
|
|
84
|
+
* },
|
|
85
|
+
* });
|
|
86
|
+
*/
|
|
87
|
+
register(plugin, options = {})
|
|
88
|
+
{
|
|
89
|
+
if (!plugin || typeof plugin !== 'object')
|
|
90
|
+
{
|
|
91
|
+
throw new Error('Plugin must be an object');
|
|
92
|
+
}
|
|
93
|
+
if (!plugin.name || typeof plugin.name !== 'string')
|
|
94
|
+
{
|
|
95
|
+
throw new Error('Plugin must have a "name" string property');
|
|
96
|
+
}
|
|
97
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(plugin.name))
|
|
98
|
+
{
|
|
99
|
+
throw new Error(`Invalid plugin name: "${plugin.name}"`);
|
|
100
|
+
}
|
|
101
|
+
if (typeof plugin.install !== 'function')
|
|
102
|
+
{
|
|
103
|
+
throw new Error(`Plugin "${plugin.name}" must have an "install" function`);
|
|
104
|
+
}
|
|
105
|
+
if (this._plugins.has(plugin.name))
|
|
106
|
+
{
|
|
107
|
+
throw new Error(`Plugin "${plugin.name}" is already registered`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Check dependencies
|
|
111
|
+
if (Array.isArray(plugin.dependencies))
|
|
112
|
+
{
|
|
113
|
+
for (const dep of plugin.dependencies)
|
|
114
|
+
{
|
|
115
|
+
if (!this._plugins.has(dep))
|
|
116
|
+
{
|
|
117
|
+
throw new Error(`Plugin "${plugin.name}" requires "${dep}" which is not registered`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this._plugins.set(plugin.name, plugin);
|
|
123
|
+
this._options.set(plugin.name, options);
|
|
124
|
+
|
|
125
|
+
// Install immediately
|
|
126
|
+
plugin.install(this, options);
|
|
127
|
+
|
|
128
|
+
log('plugin registered', plugin.name, plugin.version || '');
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Register multiple plugins at once.
|
|
134
|
+
*
|
|
135
|
+
* @param {...(object|[object, object])} plugins - Plugin objects or [plugin, options] tuples.
|
|
136
|
+
* @returns {PluginManager} this (for chaining)
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* plugins.registerAll(
|
|
140
|
+
* pluginA, // no options
|
|
141
|
+
* [pluginB, { key: 'value' }], // with options
|
|
142
|
+
* );
|
|
143
|
+
*/
|
|
144
|
+
registerAll(...plugins)
|
|
145
|
+
{
|
|
146
|
+
for (const entry of plugins)
|
|
147
|
+
{
|
|
148
|
+
if (Array.isArray(entry))
|
|
149
|
+
{
|
|
150
|
+
this.register(entry[0], entry[1] || {});
|
|
151
|
+
}
|
|
152
|
+
else
|
|
153
|
+
{
|
|
154
|
+
this.register(entry);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return this;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Unregister a plugin by name.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} name - Plugin name.
|
|
164
|
+
* @returns {PluginManager} this (for chaining)
|
|
165
|
+
*/
|
|
166
|
+
unregister(name)
|
|
167
|
+
{
|
|
168
|
+
const plugin = this._plugins.get(name);
|
|
169
|
+
if (!plugin) return this;
|
|
170
|
+
|
|
171
|
+
if (typeof plugin.uninstall === 'function')
|
|
172
|
+
{
|
|
173
|
+
plugin.uninstall(this);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this._plugins.delete(name);
|
|
177
|
+
this._options.delete(name);
|
|
178
|
+
log('plugin unregistered', name);
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// -- Lifecycle ---------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Boot all registered plugins.
|
|
186
|
+
* Calls the `boot()` method on each plugin (if defined).
|
|
187
|
+
* Should be called after all plugins are registered.
|
|
188
|
+
*
|
|
189
|
+
* @returns {Promise<PluginManager>} this (for chaining)
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* plugins.register(pluginA).register(pluginB);
|
|
193
|
+
* await plugins.boot();
|
|
194
|
+
*/
|
|
195
|
+
async boot()
|
|
196
|
+
{
|
|
197
|
+
if (this._booted) return this;
|
|
198
|
+
|
|
199
|
+
for (const [name, plugin] of this._plugins)
|
|
200
|
+
{
|
|
201
|
+
if (typeof plugin.boot === 'function')
|
|
202
|
+
{
|
|
203
|
+
await plugin.boot(this, this._options.get(name) || {});
|
|
204
|
+
log('plugin booted', name);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this._booted = true;
|
|
209
|
+
return this;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// -- Hook System -------------------------------------
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Register a hook listener.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} name - Hook name (e.g. 'beforeCreate', 'afterUpdate').
|
|
218
|
+
* @param {Function} callback - Hook callback function.
|
|
219
|
+
* @returns {PluginManager} this (for chaining)
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* manager.hook('beforeCreate', (model, data) => {
|
|
223
|
+
* data.slug = slugify(data.title);
|
|
224
|
+
* return data;
|
|
225
|
+
* });
|
|
226
|
+
*/
|
|
227
|
+
hook(name, callback)
|
|
228
|
+
{
|
|
229
|
+
if (typeof callback !== 'function')
|
|
230
|
+
{
|
|
231
|
+
throw new Error('Hook callback must be a function');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!this._hooks.has(name))
|
|
235
|
+
{
|
|
236
|
+
this._hooks.set(name, []);
|
|
237
|
+
}
|
|
238
|
+
this._hooks.get(name).push(callback);
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Remove a hook listener.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} name - Hook name.
|
|
246
|
+
* @param {Function} callback - The exact function reference to remove.
|
|
247
|
+
* @returns {PluginManager} this (for chaining)
|
|
248
|
+
*/
|
|
249
|
+
unhook(name, callback)
|
|
250
|
+
{
|
|
251
|
+
const handlers = this._hooks.get(name);
|
|
252
|
+
if (!handlers) return this;
|
|
253
|
+
|
|
254
|
+
const idx = handlers.indexOf(callback);
|
|
255
|
+
if (idx !== -1) handlers.splice(idx, 1);
|
|
256
|
+
return this;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Execute all listeners for a hook.
|
|
261
|
+
* Listeners run in registration order. If a listener returns a value,
|
|
262
|
+
* that value is passed to the next listener as the payload.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} name - Hook name.
|
|
265
|
+
* @param {...*} args - Arguments to pass to hook callbacks.
|
|
266
|
+
* @returns {Promise<*>} Final transformed value (or last arg if no transforms).
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* const data = await manager.runHook('beforeCreate', model, rawData);
|
|
270
|
+
*/
|
|
271
|
+
async runHook(name, ...args)
|
|
272
|
+
{
|
|
273
|
+
const handlers = this._hooks.get(name);
|
|
274
|
+
if (!handlers || handlers.length === 0) return args[args.length - 1];
|
|
275
|
+
|
|
276
|
+
let result = args[args.length - 1];
|
|
277
|
+
for (const fn of handlers)
|
|
278
|
+
{
|
|
279
|
+
const out = await fn(...args.slice(0, -1), result);
|
|
280
|
+
if (out !== undefined) result = out;
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if any listeners exist for a hook.
|
|
287
|
+
*
|
|
288
|
+
* @param {string} name - Hook name.
|
|
289
|
+
* @returns {boolean}
|
|
290
|
+
*/
|
|
291
|
+
hasHook(name)
|
|
292
|
+
{
|
|
293
|
+
const handlers = this._hooks.get(name);
|
|
294
|
+
return !!handlers && handlers.length > 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// -- Query -------------------------------------------
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Check if a plugin is registered.
|
|
301
|
+
*
|
|
302
|
+
* @param {string} name - Plugin name.
|
|
303
|
+
* @returns {boolean}
|
|
304
|
+
*/
|
|
305
|
+
has(name)
|
|
306
|
+
{
|
|
307
|
+
return this._plugins.has(name);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Get a registered plugin by name.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} name - Plugin name.
|
|
314
|
+
* @returns {object|undefined} Plugin object, or undefined.
|
|
315
|
+
*/
|
|
316
|
+
get(name)
|
|
317
|
+
{
|
|
318
|
+
return this._plugins.get(name);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get options for a registered plugin.
|
|
323
|
+
*
|
|
324
|
+
* @param {string} name - Plugin name.
|
|
325
|
+
* @returns {object|undefined}
|
|
326
|
+
*/
|
|
327
|
+
getOptions(name)
|
|
328
|
+
{
|
|
329
|
+
return this._options.get(name);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* List all registered plugin names.
|
|
334
|
+
*
|
|
335
|
+
* @returns {string[]}
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* plugins.list(); // ['timestamps', 'soft-delete']
|
|
339
|
+
*/
|
|
340
|
+
list()
|
|
341
|
+
{
|
|
342
|
+
return [...this._plugins.keys()];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get detailed info about all registered plugins.
|
|
347
|
+
*
|
|
348
|
+
* @returns {Array<{name: string, version: string, hasBootFn: boolean}>}
|
|
349
|
+
*/
|
|
350
|
+
info()
|
|
351
|
+
{
|
|
352
|
+
const result = [];
|
|
353
|
+
for (const [name, plugin] of this._plugins)
|
|
354
|
+
{
|
|
355
|
+
result.push({
|
|
356
|
+
name,
|
|
357
|
+
version: plugin.version || '0.0.0',
|
|
358
|
+
hasBootFn: typeof plugin.boot === 'function',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Number of registered plugins.
|
|
366
|
+
*
|
|
367
|
+
* @type {number}
|
|
368
|
+
*/
|
|
369
|
+
get size()
|
|
370
|
+
{
|
|
371
|
+
return this._plugins.size;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = { PluginManager };
|