@aetherframework/database 1.1.1 → 1.1.2
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/examples/mysql-test-pressure.js +1530 -0
- package/examples/test-direct.js +116 -0
- package/examples/transaction_example.js +127 -0
- package/package.json +3 -1
- package/src/DatabaseManager.js +565 -0
- package/src/core/ConnectionManager.js +351 -0
- package/src/core/DatabaseFactory.js +188 -0
- package/src/core/MongoQueryBuilder.js +576 -0
- package/src/core/PluginManager.js +968 -0
- package/src/core/QueryBuilder.js +4394 -0
- package/src/core/TransactionManager.js +40 -0
- package/src/drivers/clickhouse-driver.js +272 -0
- package/src/drivers/index.js +273 -0
- package/src/drivers/mongodb-driver.js +87 -0
- package/src/drivers/mssql-driver.js +117 -0
- package/src/drivers/mysql-driver.js +169 -0
- package/src/drivers/oracle-driver.js +101 -0
- package/src/drivers/postgres-driver.js +234 -0
- package/src/drivers/redis-driver.js +52 -0
- package/src/drivers/sqlite-driver.js +67 -0
- package/src/middleware/connection-pool.js +455 -0
- package/src/middleware/performance-monitor.js +652 -0
- package/src/middleware/query-cache.js +500 -0
- package/src/middleware/query-logger.js +262 -0
- package/src/plugins/AuditPlugin.js +447 -0
- package/src/plugins/BasePlugin.js +418 -0
- package/src/plugins/BatchOperationPlugin.js +165 -0
- package/src/plugins/CachePlugin.js +407 -0
- package/src/plugins/CtePlugin.js +523 -0
- package/src/plugins/DistributedPlugin.js +543 -0
- package/src/plugins/EncryptionPlugin.js +211 -0
- package/src/plugins/FullTextSearchPlugin.js +164 -0
- package/src/plugins/GeospatialPlugin.js +219 -0
- package/src/plugins/GraphQLPlugin.js +162 -0
- package/src/plugins/HookPlugin.js +211 -0
- package/src/plugins/JsonPlugin.js +366 -0
- package/src/plugins/OptimisticLockPlugin.js +374 -0
- package/src/plugins/PerformancePlugin.js +175 -0
- package/src/plugins/ResiliencePlugin.js +114 -0
- package/src/plugins/ShardingPlugin.js +227 -0
- package/src/plugins/SoftDeletePlugin.js +258 -0
- package/src/plugins/SyncPlugin.js +373 -0
- package/src/plugins/VersioningPlugin.js +314 -0
- package/src/plugins/WindowFunctionPlugin.js +343 -0
- package/src/utils/config-loader.js +632 -0
- package/src/utils/error-handler.js +724 -0
- package/src/utils/migration-runner.js +1066 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/database/core/PluginManager
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* PluginManager - Manages QueryBuilder plugin system
|
|
12
|
+
* Provides plugin registration, loading, unloading, and event management
|
|
13
|
+
*/
|
|
14
|
+
export class PluginManager extends EventEmitter {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super();
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.plugins = new Map(); // Store registered plugin classes
|
|
19
|
+
this.pluginInstances = new Map(); // Store plugin instances
|
|
20
|
+
this.methods = new Map(); // Store plugin methods
|
|
21
|
+
this.hooks = new Map(); // Store hook functions
|
|
22
|
+
this.middlewares = new Map(); // Store middleware functions
|
|
23
|
+
this.initialized = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize plugin manager
|
|
28
|
+
* @param {Object} config - Plugin manager configuration
|
|
29
|
+
* @returns {PluginManager} PluginManager instance
|
|
30
|
+
*/
|
|
31
|
+
initialize(config = {}) {
|
|
32
|
+
if (this.initialized) return this;
|
|
33
|
+
|
|
34
|
+
this.config = { ...this.config, ...config };
|
|
35
|
+
this.initialized = true;
|
|
36
|
+
|
|
37
|
+
this.emit("initialized", { config: this.config });
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Register a plugin
|
|
45
|
+
* @param {string} name - Plugin name
|
|
46
|
+
* @param {Function} PluginClass - Plugin class constructor
|
|
47
|
+
* @param {Object} options - Plugin options
|
|
48
|
+
* @returns {PluginManager} PluginManager instance
|
|
49
|
+
*/
|
|
50
|
+
register(name, PluginClass, options = {}) {
|
|
51
|
+
if (this.plugins.has(name)) {
|
|
52
|
+
throw new Error(`Plugin "${name}" is already registered`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate plugin class
|
|
56
|
+
if (!PluginClass || typeof PluginClass !== "function") {
|
|
57
|
+
throw new Error(`Plugin "${name}" must be a class constructor`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check if plugin extends BasePlugin
|
|
61
|
+
const pluginProto = PluginClass.prototype;
|
|
62
|
+
if (!pluginProto || typeof pluginProto._registerMethods !== "function") {
|
|
63
|
+
console.warn(`Plugin "${name}" may not extend BasePlugin properly`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Store plugin class and options
|
|
67
|
+
this.plugins.set(name, {
|
|
68
|
+
class: PluginClass,
|
|
69
|
+
options: {
|
|
70
|
+
name,
|
|
71
|
+
enabled: options.enabled !== false,
|
|
72
|
+
priority: options.priority || 0,
|
|
73
|
+
dependencies: options.dependencies || [],
|
|
74
|
+
config: options.config || {},
|
|
75
|
+
registeredAt: new Date(),
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.emit("plugin:registered", { name, PluginClass, options });
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Enable a plugin
|
|
87
|
+
* @param {string} name - Plugin name
|
|
88
|
+
* @param {Object} options - Plugin options
|
|
89
|
+
* @returns {PluginManager} PluginManager instance
|
|
90
|
+
*/
|
|
91
|
+
enable(name, options = {}) {
|
|
92
|
+
const pluginInfo = this.plugins.get(name);
|
|
93
|
+
if (!pluginInfo) {
|
|
94
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (pluginInfo.options.enabled) {
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check dependencies
|
|
102
|
+
this._checkDependencies(name);
|
|
103
|
+
|
|
104
|
+
pluginInfo.options.enabled = true;
|
|
105
|
+
pluginInfo.options.config = { ...pluginInfo.options.config, ...options };
|
|
106
|
+
|
|
107
|
+
this.emit("plugin:enabled", { name, options: pluginInfo.options });
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Disable a plugin
|
|
115
|
+
* @param {string} name - Plugin name
|
|
116
|
+
* @returns {PluginManager} PluginManager instance
|
|
117
|
+
*/
|
|
118
|
+
disable(name) {
|
|
119
|
+
const pluginInfo = this.plugins.get(name);
|
|
120
|
+
if (!pluginInfo) {
|
|
121
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!pluginInfo.options.enabled) {
|
|
125
|
+
return this;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pluginInfo.options.enabled = false;
|
|
129
|
+
|
|
130
|
+
// Cleanup plugin instance if it exists
|
|
131
|
+
if (this.pluginInstances.has(name)) {
|
|
132
|
+
const instance = this.pluginInstances.get(name);
|
|
133
|
+
if (instance && typeof instance.cleanup === "function") {
|
|
134
|
+
instance.cleanup();
|
|
135
|
+
}
|
|
136
|
+
this.pluginInstances.delete(name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.emit("plugin:disabled", { name });
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Unregister a plugin
|
|
147
|
+
* @param {string} name - Plugin name
|
|
148
|
+
* @returns {PluginManager} PluginManager instance
|
|
149
|
+
*/
|
|
150
|
+
unregister(name) {
|
|
151
|
+
const pluginInfo = this.plugins.get(name);
|
|
152
|
+
if (!pluginInfo) {
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if other plugins depend on this plugin
|
|
157
|
+
for (const [otherName, otherPlugin] of this.plugins) {
|
|
158
|
+
if (otherName !== name && otherPlugin.options.dependencies.includes(name)) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Cannot unregister plugin "${name}" because "${otherName}" depends on it`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Disable first
|
|
166
|
+
this.disable(name);
|
|
167
|
+
|
|
168
|
+
// Remove from registry
|
|
169
|
+
this.plugins.delete(name);
|
|
170
|
+
|
|
171
|
+
this.emit("plugin:unregistered", { name });
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
return this;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check plugin dependencies
|
|
179
|
+
* @param {string} pluginName - Plugin name
|
|
180
|
+
* @private
|
|
181
|
+
*/
|
|
182
|
+
_checkDependencies(pluginName) {
|
|
183
|
+
const pluginInfo = this.plugins.get(pluginName);
|
|
184
|
+
if (!pluginInfo) return;
|
|
185
|
+
|
|
186
|
+
const visited = new Set();
|
|
187
|
+
const stack = new Set();
|
|
188
|
+
const missing = [];
|
|
189
|
+
const circular = [];
|
|
190
|
+
|
|
191
|
+
const check = (name, path = []) => {
|
|
192
|
+
if (visited.has(name)) return;
|
|
193
|
+
|
|
194
|
+
if (stack.has(name)) {
|
|
195
|
+
circular.push([...path, name]);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const plugin = this.plugins.get(name);
|
|
200
|
+
if (!plugin) {
|
|
201
|
+
missing.push(name);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
stack.add(name);
|
|
206
|
+
path.push(name);
|
|
207
|
+
|
|
208
|
+
for (const dep of plugin.options.dependencies) {
|
|
209
|
+
check(dep, [...path]);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
stack.delete(name);
|
|
213
|
+
visited.add(name);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
check(pluginName);
|
|
217
|
+
|
|
218
|
+
if (missing.length > 0) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Plugin "${pluginName}" missing dependencies: ${missing.join(", ")}`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (circular.length > 0) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Plugin "${pluginName}" has circular dependencies: ${circular
|
|
227
|
+
.map((path) => path.join(" -> "))
|
|
228
|
+
.join(", ")}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get plugin class
|
|
235
|
+
* @param {string} name - Plugin name
|
|
236
|
+
* @returns {Function|null} Plugin class or null
|
|
237
|
+
*/
|
|
238
|
+
get(name) {
|
|
239
|
+
const pluginInfo = this.plugins.get(name);
|
|
240
|
+
return pluginInfo ? pluginInfo.class : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get or create plugin instance for a specific QueryBuilder
|
|
245
|
+
* @param {string} name - Plugin name
|
|
246
|
+
* @param {QueryBuilder} queryBuilder - QueryBuilder instance
|
|
247
|
+
* @returns {BasePlugin|null} Plugin instance or null
|
|
248
|
+
*/
|
|
249
|
+
getInstance(name, queryBuilder) {
|
|
250
|
+
// Create a unique key for this plugin instance (plugin name + queryBuilder reference)
|
|
251
|
+
const instanceKey = `${name}_${queryBuilder._instanceId || Date.now()}`;
|
|
252
|
+
|
|
253
|
+
if (this.pluginInstances.has(instanceKey)) {
|
|
254
|
+
return this.pluginInstances.get(instanceKey);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const PluginClass = this.get(name);
|
|
258
|
+
if (!PluginClass) return null;
|
|
259
|
+
|
|
260
|
+
const instance = new PluginClass(queryBuilder);
|
|
261
|
+
this.pluginInstances.set(instanceKey, instance);
|
|
262
|
+
return instance;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check if plugin exists
|
|
267
|
+
* @param {string} name - Plugin name
|
|
268
|
+
* @returns {boolean} True if plugin exists
|
|
269
|
+
*/
|
|
270
|
+
has(name) {
|
|
271
|
+
return this.plugins.has(name);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check if plugin is enabled
|
|
276
|
+
* @param {string} name - Plugin name
|
|
277
|
+
* @returns {boolean} True if plugin is enabled
|
|
278
|
+
*/
|
|
279
|
+
isEnabled(name) {
|
|
280
|
+
const pluginInfo = this.plugins.get(name);
|
|
281
|
+
return pluginInfo ? pluginInfo.options.enabled : false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get all registered plugins
|
|
286
|
+
* @returns {Array} All registered plugins
|
|
287
|
+
*/
|
|
288
|
+
getAll() {
|
|
289
|
+
return Array.from(this.plugins.values()).map((info) => ({
|
|
290
|
+
name: info.options.name,
|
|
291
|
+
class: info.class,
|
|
292
|
+
options: info.options,
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get enabled plugins
|
|
298
|
+
* @returns {Array} Enabled plugins
|
|
299
|
+
*/
|
|
300
|
+
getEnabled() {
|
|
301
|
+
return Array.from(this.plugins.values())
|
|
302
|
+
.filter((info) => info.options.enabled)
|
|
303
|
+
.map((info) => info.class);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get enabled plugin names mapping
|
|
308
|
+
* @returns {Object} Enabled plugins name mapping
|
|
309
|
+
*/
|
|
310
|
+
getEnabledPlugins() {
|
|
311
|
+
const enabledPlugins = {};
|
|
312
|
+
for (const [name, info] of this.plugins) {
|
|
313
|
+
if (info.options.enabled) {
|
|
314
|
+
enabledPlugins[name] = info.class;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return enabledPlugins;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get plugin information
|
|
322
|
+
* @param {string} name - Plugin name
|
|
323
|
+
* @returns {Object|null} Plugin information or null
|
|
324
|
+
*/
|
|
325
|
+
getPluginInfo(name) {
|
|
326
|
+
const pluginInfo = this.plugins.get(name);
|
|
327
|
+
if (!pluginInfo) return null;
|
|
328
|
+
|
|
329
|
+
const PluginClass = pluginInfo.class;
|
|
330
|
+
const proto = PluginClass.prototype;
|
|
331
|
+
const methods = [];
|
|
332
|
+
|
|
333
|
+
// Get all methods from prototype
|
|
334
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
335
|
+
if (key !== "constructor" && typeof proto[key] === "function") {
|
|
336
|
+
methods.push(key);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
name: pluginInfo.options.name,
|
|
342
|
+
version: pluginInfo.options.version || "1.0.0",
|
|
343
|
+
enabled: pluginInfo.options.enabled,
|
|
344
|
+
priority: pluginInfo.options.priority,
|
|
345
|
+
dependencies: pluginInfo.options.dependencies,
|
|
346
|
+
config: pluginInfo.options.config,
|
|
347
|
+
registeredAt: pluginInfo.options.registeredAt,
|
|
348
|
+
methods: methods,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Initialize all plugins for a specific QueryBuilder
|
|
354
|
+
* @param {QueryBuilder} queryBuilder - QueryBuilder instance
|
|
355
|
+
* @returns {Promise<void>}
|
|
356
|
+
*/
|
|
357
|
+
async initializePluginsForQueryBuilder(queryBuilder) {
|
|
358
|
+
if (!this.initialized) {
|
|
359
|
+
throw new Error("PluginManager must be initialized first");
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!queryBuilder) {
|
|
363
|
+
throw new Error("QueryBuilder is required for plugin initialization");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const enabledPlugins = this.getEnabled();
|
|
367
|
+
|
|
368
|
+
for (const PluginClass of enabledPlugins) {
|
|
369
|
+
try {
|
|
370
|
+
// Create plugin instance with QueryBuilder
|
|
371
|
+
const pluginInstance = new PluginClass(queryBuilder);
|
|
372
|
+
|
|
373
|
+
// Initialize the plugin
|
|
374
|
+
await pluginInstance.init();
|
|
375
|
+
|
|
376
|
+
// Store plugin instance with unique key
|
|
377
|
+
const pluginName = pluginInstance.pluginName || PluginClass.name;
|
|
378
|
+
const instanceKey = `${pluginName}_${queryBuilder._instanceId || Date.now()}`;
|
|
379
|
+
this.pluginInstances.set(instanceKey, pluginInstance);
|
|
380
|
+
|
|
381
|
+
} catch (error) {
|
|
382
|
+
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Initialize all registered plugins (without QueryBuilder)
|
|
390
|
+
* This method only registers plugins, doesn't create instances
|
|
391
|
+
* @returns {Promise<void>}
|
|
392
|
+
*/
|
|
393
|
+
async initializeAll() {
|
|
394
|
+
if (!this.initialized) {
|
|
395
|
+
throw new Error("PluginManager must be initialized first");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const enabledPlugins = this.getEnabled();
|
|
399
|
+
|
|
400
|
+
for (const PluginClass of enabledPlugins) {
|
|
401
|
+
try {
|
|
402
|
+
// Just log registration, don't create instances
|
|
403
|
+
const pluginName = PluginClass.prototype.pluginName || PluginClass.name;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Get plugin configuration
|
|
411
|
+
* @param {string} pluginName - Plugin name
|
|
412
|
+
* @returns {Object|null} Plugin configuration or null if not found
|
|
413
|
+
*/
|
|
414
|
+
getPluginConfig(pluginName) {
|
|
415
|
+
const plugin = this.plugins.get(pluginName);
|
|
416
|
+
return plugin ? plugin.config : null;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Register method to QueryBuilder
|
|
420
|
+
* @param {string} name - Method name
|
|
421
|
+
* @param {Function} method - Method function
|
|
422
|
+
* @param {Object} options - Method options
|
|
423
|
+
* @returns {PluginManager} PluginManager instance
|
|
424
|
+
*/
|
|
425
|
+
registerMethod(name, method, options = {}) {
|
|
426
|
+
if (typeof method !== "function") {
|
|
427
|
+
throw new Error(`Method "${name}" must be a function`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Check if method already exists
|
|
431
|
+
if (this.methods.has(name)) {
|
|
432
|
+
if (options.override) {
|
|
433
|
+
console.warn(`Method "${name}" will be overridden`);
|
|
434
|
+
} else {
|
|
435
|
+
throw new Error(`Method "${name}" already exists`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Store method
|
|
440
|
+
this.methods.set(name, {
|
|
441
|
+
method,
|
|
442
|
+
plugin: options.plugin,
|
|
443
|
+
description: options.description,
|
|
444
|
+
addedAt: new Date(),
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
this.emit("method:registered", { name, method, options });
|
|
448
|
+
return this;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Remove method
|
|
453
|
+
* @param {string} name - Method name
|
|
454
|
+
* @returns {PluginManager} PluginManager instance
|
|
455
|
+
*/
|
|
456
|
+
unregisterMethod(name) {
|
|
457
|
+
const methodInfo = this.methods.get(name);
|
|
458
|
+
if (!methodInfo) {
|
|
459
|
+
return this;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Remove from storage
|
|
463
|
+
this.methods.delete(name);
|
|
464
|
+
|
|
465
|
+
this.emit("method:unregistered", { name, methodInfo });
|
|
466
|
+
return this;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Register hook
|
|
471
|
+
* @param {string} event - Event name
|
|
472
|
+
* @param {Function} handler - Handler function
|
|
473
|
+
* @param {Object} options - Hook options
|
|
474
|
+
* @returns {PluginManager} PluginManager instance
|
|
475
|
+
*/
|
|
476
|
+
registerHook(event, handler, options = {}) {
|
|
477
|
+
if (typeof handler !== "function") {
|
|
478
|
+
throw new Error(`Hook handler "${event}" must be a function`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!this.hooks.has(event)) {
|
|
482
|
+
this.hooks.set(event, []);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const hook = {
|
|
486
|
+
handler,
|
|
487
|
+
plugin: options.plugin,
|
|
488
|
+
priority: options.priority || 0,
|
|
489
|
+
once: options.once || false,
|
|
490
|
+
addedAt: new Date(),
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Insert sorted by priority
|
|
494
|
+
const hooks = this.hooks.get(event);
|
|
495
|
+
const index = hooks.findIndex((h) => h.priority < hook.priority);
|
|
496
|
+
if (index === -1) {
|
|
497
|
+
hooks.push(hook);
|
|
498
|
+
} else {
|
|
499
|
+
hooks.splice(index, 0, hook);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
this.emit("hook:registered", { event, hook, options });
|
|
503
|
+
return this;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Trigger hook
|
|
508
|
+
* @param {string} event - Event name
|
|
509
|
+
* @param {...any} args - Arguments
|
|
510
|
+
* @returns {Promise<Array>} Results from all hooks
|
|
511
|
+
*/
|
|
512
|
+
async triggerHook(event, ...args) {
|
|
513
|
+
if (!this.hooks.has(event)) {
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const hooks = this.hooks.get(event);
|
|
518
|
+
const results = [];
|
|
519
|
+
|
|
520
|
+
for (const hook of hooks) {
|
|
521
|
+
try {
|
|
522
|
+
const result = await hook.handler(...args);
|
|
523
|
+
results.push(result);
|
|
524
|
+
|
|
525
|
+
// If it's a one-time hook, remove it
|
|
526
|
+
if (hook.once) {
|
|
527
|
+
this.unregisterHook(event, hook.handler);
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
console.error(`Hook "${event}" execution error:`, error);
|
|
531
|
+
this.emit("hook:error", { event, hook, error, args });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
this.emit("hook:triggered", { event, results, args });
|
|
536
|
+
return results;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Remove hook
|
|
541
|
+
* @param {string} event - Event name
|
|
542
|
+
* @param {Function} handler - Handler function
|
|
543
|
+
* @returns {PluginManager} PluginManager instance
|
|
544
|
+
*/
|
|
545
|
+
unregisterHook(event, handler) {
|
|
546
|
+
if (!this.hooks.has(event)) {
|
|
547
|
+
return this;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const hooks = this.hooks.get(event);
|
|
551
|
+
const index = hooks.findIndex((h) => h.handler === handler);
|
|
552
|
+
|
|
553
|
+
if (index !== -1) {
|
|
554
|
+
const removed = hooks.splice(index, 1);
|
|
555
|
+
this.emit("hook:unregistered", { event, hook: removed });
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (hooks.length === 0) {
|
|
559
|
+
this.hooks.delete(event);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return this;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Register middleware
|
|
567
|
+
* @param {string} type - Middleware type
|
|
568
|
+
* @param {Function} middleware - Middleware function
|
|
569
|
+
* @param {Object} options - Middleware options
|
|
570
|
+
* @returns {PluginManager} PluginManager instance
|
|
571
|
+
*/
|
|
572
|
+
registerMiddleware(type, middleware, options = {}) {
|
|
573
|
+
if (typeof middleware !== "function") {
|
|
574
|
+
throw new Error(`Middleware "${type}" must be a function`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!this.middlewares.has(type)) {
|
|
578
|
+
this.middlewares.set(type, []);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const mw = {
|
|
582
|
+
middleware,
|
|
583
|
+
plugin: options.plugin,
|
|
584
|
+
priority: options.priority || 0,
|
|
585
|
+
addedAt: new Date(),
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
// Insert sorted by priority
|
|
589
|
+
const middlewares = this.middlewares.get(type);
|
|
590
|
+
const index = middlewares.findIndex((m) => m.priority < mw.priority);
|
|
591
|
+
if (index === -1) {
|
|
592
|
+
middlewares.push(mw);
|
|
593
|
+
} else {
|
|
594
|
+
middlewares.splice(index, 0, mw);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
this.emit("middleware:registered", { type, middleware: mw, options });
|
|
598
|
+
return this;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Execute middleware
|
|
603
|
+
* @param {string} type - Middleware type
|
|
604
|
+
* @param {*} context - Context object
|
|
605
|
+
* @param {...any} args - Additional arguments
|
|
606
|
+
* @returns {Promise<*>} Result after middleware processing
|
|
607
|
+
*/
|
|
608
|
+
async executeMiddleware(type, context, ...args) {
|
|
609
|
+
if (!this.middlewares.has(type)) {
|
|
610
|
+
return context;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const middlewares = this.middlewares.get(type);
|
|
614
|
+
let currentIndex = 0;
|
|
615
|
+
|
|
616
|
+
const next = async () => {
|
|
617
|
+
if (currentIndex >= middlewares.length) {
|
|
618
|
+
return context;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const mw = middlewares[currentIndex++];
|
|
622
|
+
try {
|
|
623
|
+
return await mw.middleware(context, next, ...args);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.error(`Middleware "${type}" execution error:`, error);
|
|
626
|
+
this.emit("middleware:error", { type, middleware: mw, error, args });
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const result = await next();
|
|
632
|
+
this.emit("middleware:executed", { type, result, args });
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Remove middleware
|
|
638
|
+
* @param {string} type - Middleware type
|
|
639
|
+
* @param {Function} middleware - Middleware function
|
|
640
|
+
* @returns {PluginManager} PluginManager instance
|
|
641
|
+
*/
|
|
642
|
+
unregisterMiddleware(type, middleware) {
|
|
643
|
+
if (!this.middlewares.has(type)) {
|
|
644
|
+
return this;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const middlewares = this.middlewares.get(type);
|
|
648
|
+
const index = middlewares.findIndex((m) => m.middleware === middleware);
|
|
649
|
+
|
|
650
|
+
if (index !== -1) {
|
|
651
|
+
const removed = middlewares.splice(index, 1);
|
|
652
|
+
this.emit("middleware:unregistered", { type, middleware: removed });
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (middlewares.length === 0) {
|
|
656
|
+
this.middlewares.delete(type);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return this;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Load plugin configuration
|
|
664
|
+
* @param {Object} config - Plugin configuration
|
|
665
|
+
* @returns {PluginManager} PluginManager instance
|
|
666
|
+
*/
|
|
667
|
+
loadConfig(config) {
|
|
668
|
+
if (!config || typeof config !== "object") {
|
|
669
|
+
return this;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Load plugin configuration
|
|
673
|
+
if (config.plugins) {
|
|
674
|
+
for (const [name, pluginConfig] of Object.entries(config.plugins)) {
|
|
675
|
+
if (pluginConfig.enabled !== undefined) {
|
|
676
|
+
const plugin = this.plugins.get(name);
|
|
677
|
+
if (plugin) {
|
|
678
|
+
if (pluginConfig.enabled) {
|
|
679
|
+
this.enable(name);
|
|
680
|
+
} else {
|
|
681
|
+
this.disable(name);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Update plugin configuration
|
|
687
|
+
if (pluginConfig.config && this.plugins.has(name)) {
|
|
688
|
+
const plugin = this.plugins.get(name);
|
|
689
|
+
plugin.options.config = {
|
|
690
|
+
...plugin.options.config,
|
|
691
|
+
...pluginConfig.config,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
this.emit("config:loaded", { config });
|
|
698
|
+
return this;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Save plugin configuration
|
|
703
|
+
* @returns {Object} Plugin configuration
|
|
704
|
+
*/
|
|
705
|
+
saveConfig() {
|
|
706
|
+
const config = {
|
|
707
|
+
plugins: {},
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
for (const [name, plugin] of this.plugins) {
|
|
711
|
+
config.plugins[name] = {
|
|
712
|
+
enabled: plugin.options.enabled,
|
|
713
|
+
config: plugin.options.config,
|
|
714
|
+
version: plugin.options.version,
|
|
715
|
+
priority: plugin.options.priority,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return config;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Hot reload plugin
|
|
724
|
+
* @param {string} name - Plugin name
|
|
725
|
+
* @param {Function} newPluginClass - New plugin class
|
|
726
|
+
* @returns {PluginManager} PluginManager instance
|
|
727
|
+
*/
|
|
728
|
+
hotReload(name, newPluginClass) {
|
|
729
|
+
const oldPlugin = this.plugins.get(name);
|
|
730
|
+
if (!oldPlugin) {
|
|
731
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const wasEnabled = oldPlugin.options.enabled;
|
|
735
|
+
|
|
736
|
+
// Disable old plugin
|
|
737
|
+
if (wasEnabled) {
|
|
738
|
+
this.disable(name);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Unregister old plugin
|
|
742
|
+
this.unregister(name);
|
|
743
|
+
|
|
744
|
+
// Register new plugin
|
|
745
|
+
this.register(name, newPluginClass, {
|
|
746
|
+
version: newPluginClass.version || oldPlugin.options.version,
|
|
747
|
+
enabled: wasEnabled,
|
|
748
|
+
priority: oldPlugin.options.priority,
|
|
749
|
+
dependencies: oldPlugin.options.dependencies,
|
|
750
|
+
config: oldPlugin.options.config,
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Enable new plugin
|
|
754
|
+
if (wasEnabled) {
|
|
755
|
+
this.enable(name);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
this.emit("plugin:reloaded", { name, oldPlugin, newPluginClass });
|
|
759
|
+
return this;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Check plugin dependencies
|
|
764
|
+
* @param {string} name - Plugin name
|
|
765
|
+
* @returns {Object} Dependency check result
|
|
766
|
+
*/
|
|
767
|
+
checkDependencies(name) {
|
|
768
|
+
const plugin = this.plugins.get(name);
|
|
769
|
+
if (!plugin) {
|
|
770
|
+
return { valid: false, missing: [name], circular: [] };
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const visited = new Set();
|
|
774
|
+
const stack = new Set();
|
|
775
|
+
const missing = [];
|
|
776
|
+
const circular = [];
|
|
777
|
+
|
|
778
|
+
const check = (pluginName, path = []) => {
|
|
779
|
+
if (visited.has(pluginName)) {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
if (stack.has(pluginName)) {
|
|
784
|
+
circular.push([...path, pluginName]);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const depPlugin = this.plugins.get(pluginName);
|
|
789
|
+
if (!depPlugin) {
|
|
790
|
+
missing.push(pluginName);
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
stack.add(pluginName);
|
|
795
|
+
path.push(pluginName);
|
|
796
|
+
|
|
797
|
+
for (const dep of depPlugin.options.dependencies) {
|
|
798
|
+
check(dep, [...path]);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
stack.delete(pluginName);
|
|
802
|
+
visited.add(pluginName);
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
check(name);
|
|
806
|
+
|
|
807
|
+
return {
|
|
808
|
+
valid: missing.length === 0 && circular.length === 0,
|
|
809
|
+
missing,
|
|
810
|
+
circular,
|
|
811
|
+
dependencies: plugin.options.dependencies,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Get plugin status
|
|
817
|
+
* @returns {Object} Plugin status
|
|
818
|
+
*/
|
|
819
|
+
getStatus() {
|
|
820
|
+
const status = {
|
|
821
|
+
initialized: this.initialized,
|
|
822
|
+
totalPlugins: this.plugins.size,
|
|
823
|
+
enabledPlugins: Array.from(this.plugins.values()).filter(p => p.options.enabled).length,
|
|
824
|
+
registeredMethods: this.methods.size,
|
|
825
|
+
registeredHooks: Array.from(this.hooks.keys()).length,
|
|
826
|
+
registeredMiddlewares: Array.from(this.middlewares.keys()).length,
|
|
827
|
+
plugins: [],
|
|
828
|
+
};
|
|
829
|
+
|
|
830
|
+
for (const [name, plugin] of this.plugins) {
|
|
831
|
+
const PluginClass = plugin.class;
|
|
832
|
+
const proto = PluginClass.prototype;
|
|
833
|
+
const methods = [];
|
|
834
|
+
|
|
835
|
+
for (const key of Object.getOwnPropertyNames(proto)) {
|
|
836
|
+
if (key !== "constructor" && typeof proto[key] === "function") {
|
|
837
|
+
methods.push(key);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
status.plugins.push({
|
|
842
|
+
name,
|
|
843
|
+
enabled: plugin.options.enabled,
|
|
844
|
+
version: plugin.options.version || "1.0.0",
|
|
845
|
+
priority: plugin.options.priority,
|
|
846
|
+
dependencies: plugin.options.dependencies,
|
|
847
|
+
methods: methods.length,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
return status;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Cleanup plugin system
|
|
856
|
+
*/
|
|
857
|
+
cleanup() {
|
|
858
|
+
// Disable all plugins
|
|
859
|
+
for (const [name, plugin] of this.plugins) {
|
|
860
|
+
if (plugin.options.enabled) {
|
|
861
|
+
this.disable(name);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Clear all collections
|
|
866
|
+
this.plugins.clear();
|
|
867
|
+
this.pluginInstances.clear();
|
|
868
|
+
this.methods.clear();
|
|
869
|
+
this.hooks.clear();
|
|
870
|
+
this.middlewares.clear();
|
|
871
|
+
|
|
872
|
+
this.initialized = false;
|
|
873
|
+
this.emit("cleaned");
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Batch register plugins
|
|
878
|
+
* @param {Array} plugins - Array of plugin configurations
|
|
879
|
+
* @returns {PluginManager} PluginManager instance
|
|
880
|
+
*/
|
|
881
|
+
registerAll(plugins) {
|
|
882
|
+
if (!Array.isArray(plugins)) {
|
|
883
|
+
throw new Error("Plugins must be an array");
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
for (const plugin of plugins) {
|
|
887
|
+
if (!plugin.name || !plugin.class) {
|
|
888
|
+
throw new Error("Plugin must contain name and class properties");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
this.register(plugin.name, plugin.class, plugin.options || {});
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return this;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Get plugins sorted by priority
|
|
899
|
+
* @returns {Array} Sorted plugins list
|
|
900
|
+
*/
|
|
901
|
+
getPluginsByPriority() {
|
|
902
|
+
return Array.from(this.plugins.values()).sort(
|
|
903
|
+
(a, b) => b.options.priority - a.options.priority
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Get plugin dependency graph
|
|
909
|
+
* @returns {Object} Dependency graph
|
|
910
|
+
*/
|
|
911
|
+
getDependencyGraph() {
|
|
912
|
+
const graph = {
|
|
913
|
+
nodes: [],
|
|
914
|
+
edges: [],
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
for (const [name, plugin] of this.plugins) {
|
|
918
|
+
graph.nodes.push({
|
|
919
|
+
id: name,
|
|
920
|
+
label: name,
|
|
921
|
+
enabled: plugin.options.enabled,
|
|
922
|
+
version: plugin.options.version || "1.0.0",
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
for (const dep of plugin.options.dependencies) {
|
|
926
|
+
graph.edges.push({
|
|
927
|
+
from: name,
|
|
928
|
+
to: dep,
|
|
929
|
+
type: "depends_on",
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
return graph;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Validate plugin compatibility
|
|
939
|
+
* @param {Function} PluginClass - Plugin class
|
|
940
|
+
* @returns {Object} Compatibility check result
|
|
941
|
+
*/
|
|
942
|
+
validatePlugin(PluginClass) {
|
|
943
|
+
const errors = [];
|
|
944
|
+
const warnings = [];
|
|
945
|
+
|
|
946
|
+
// Check required methods
|
|
947
|
+
const requiredMethods = ["_registerMethods"];
|
|
948
|
+
for (const method of requiredMethods) {
|
|
949
|
+
if (typeof PluginClass.prototype[method] !== "function") {
|
|
950
|
+
errors.push(`Missing required method: ${method}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Check plugin name
|
|
955
|
+
if (!PluginClass.prototype.pluginName || typeof PluginClass.prototype.pluginName !== "string") {
|
|
956
|
+
warnings.push("Plugin should have a unique pluginName property");
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return {
|
|
960
|
+
valid: errors.length === 0,
|
|
961
|
+
errors,
|
|
962
|
+
warnings,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Default export
|
|
968
|
+
export default PluginManager;
|