@aetherframework/database 1.1.0 → 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.
Files changed (47) hide show
  1. package/examples/mysql-test-pressure.js +1530 -0
  2. package/examples/test-direct.js +116 -0
  3. package/examples/transaction_example.js +127 -0
  4. package/package.json +3 -1
  5. package/src/DatabaseManager.js +565 -0
  6. package/src/core/ConnectionManager.js +351 -0
  7. package/src/core/DatabaseFactory.js +188 -0
  8. package/src/core/MongoQueryBuilder.js +576 -0
  9. package/src/core/PluginManager.js +968 -0
  10. package/src/core/QueryBuilder.js +4394 -0
  11. package/src/core/TransactionManager.js +40 -0
  12. package/src/drivers/clickhouse-driver.js +272 -0
  13. package/src/drivers/index.js +273 -0
  14. package/src/drivers/mongodb-driver.js +87 -0
  15. package/src/drivers/mssql-driver.js +117 -0
  16. package/src/drivers/mysql-driver.js +169 -0
  17. package/src/drivers/oracle-driver.js +101 -0
  18. package/src/drivers/postgres-driver.js +234 -0
  19. package/src/drivers/redis-driver.js +52 -0
  20. package/src/drivers/sqlite-driver.js +67 -0
  21. package/src/middleware/connection-pool.js +455 -0
  22. package/src/middleware/performance-monitor.js +652 -0
  23. package/src/middleware/query-cache.js +500 -0
  24. package/src/middleware/query-logger.js +262 -0
  25. package/src/plugins/AuditPlugin.js +447 -0
  26. package/src/plugins/BasePlugin.js +418 -0
  27. package/src/plugins/BatchOperationPlugin.js +165 -0
  28. package/src/plugins/CachePlugin.js +407 -0
  29. package/src/plugins/CtePlugin.js +523 -0
  30. package/src/plugins/DistributedPlugin.js +543 -0
  31. package/src/plugins/EncryptionPlugin.js +211 -0
  32. package/src/plugins/FullTextSearchPlugin.js +164 -0
  33. package/src/plugins/GeospatialPlugin.js +219 -0
  34. package/src/plugins/GraphQLPlugin.js +162 -0
  35. package/src/plugins/HookPlugin.js +211 -0
  36. package/src/plugins/JsonPlugin.js +366 -0
  37. package/src/plugins/OptimisticLockPlugin.js +374 -0
  38. package/src/plugins/PerformancePlugin.js +175 -0
  39. package/src/plugins/ResiliencePlugin.js +114 -0
  40. package/src/plugins/ShardingPlugin.js +227 -0
  41. package/src/plugins/SoftDeletePlugin.js +258 -0
  42. package/src/plugins/SyncPlugin.js +373 -0
  43. package/src/plugins/VersioningPlugin.js +314 -0
  44. package/src/plugins/WindowFunctionPlugin.js +343 -0
  45. package/src/utils/config-loader.js +632 -0
  46. package/src/utils/error-handler.js +724 -0
  47. package/src/utils/migration-runner.js +1066 -0
@@ -0,0 +1,418 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/database/plugin/BasePlugin
6
+ */
7
+ /**
8
+ * Base Plugin - Provides common functionality for all plugins
9
+ * All plugins should extend this class
10
+ */
11
+ export class BasePlugin {
12
+ /**
13
+ * Constructor for BasePlugin
14
+ * @param {Object} queryBuilder - QueryBuilder instance
15
+ */
16
+ constructor(queryBuilder) {
17
+ this.queryBuilder = queryBuilder;
18
+ this.pluginName = this.constructor.name;
19
+ this.initialized = false;
20
+ this.methods = {}; // Store methods to register
21
+ this.hooks = {}; // Store hook callbacks
22
+ this.middlewares = {}; // Store middleware functions
23
+ }
24
+
25
+ /**
26
+ * Initialize the plugin
27
+ * This method should be called after plugin instantiation
28
+ * @returns {Promise<void>}
29
+ */
30
+ async init() {
31
+ if (this.initialized) return;
32
+
33
+ // Validate queryBuilder
34
+ if (!this.queryBuilder) {
35
+ throw new Error(`${this.pluginName}: queryBuilder is required but was not provided.`);
36
+ }
37
+
38
+ this.initialized = true;
39
+ this._registerMethods();
40
+ this._bindMethods();
41
+ this._registerHooks();
42
+ this._registerMiddlewares();
43
+
44
+ }
45
+
46
+ /**
47
+ * Register plugin methods to QueryBuilder
48
+ * This method must be implemented by child classes
49
+ * @protected
50
+ */
51
+ _registerMethods() {
52
+ throw new Error('_registerMethods() must be implemented by plugin');
53
+ }
54
+
55
+ /**
56
+ * Bind plugin methods to QueryBuilder instance
57
+ * @protected
58
+ */
59
+ _bindMethods() {
60
+ // Bind methods to QueryBuilder
61
+ Object.entries(this.methods).forEach(([methodName, method]) => {
62
+ if (typeof method === 'function') {
63
+ // Check if method already exists
64
+ if (this.queryBuilder[methodName]) {
65
+ console.warn(`Method ${methodName} already exists in QueryBuilder, overriding with plugin method`);
66
+ }
67
+ // Bind to QueryBuilder instance
68
+ this.queryBuilder[methodName] = method.bind(this);
69
+ }
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Register hooks for the plugin
75
+ * Override this method to register hooks
76
+ * @protected
77
+ */
78
+ _registerHooks() {
79
+ // Default implementation - can be overridden by child classes
80
+ }
81
+
82
+ /**
83
+ * Register middlewares for the plugin
84
+ * Override this method to register middlewares
85
+ * @protected
86
+ */
87
+ _registerMiddlewares() {
88
+ // Default implementation - can be overridden by child classes
89
+ }
90
+
91
+ /**
92
+ * Add method to plugin
93
+ * @param {string} name - Method name
94
+ * @param {Function} method - Method function
95
+ * @returns {BasePlugin} Plugin instance for chaining
96
+ */
97
+ addMethod(name, method) {
98
+ this.methods[name] = method;
99
+ return this;
100
+ }
101
+
102
+ /**
103
+ * Remove method from plugin
104
+ * @param {string} name - Method name
105
+ * @returns {BasePlugin} Plugin instance for chaining
106
+ */
107
+ removeMethod(name) {
108
+ delete this.methods[name];
109
+ return this;
110
+ }
111
+
112
+ /**
113
+ * Register a hook
114
+ * @param {string} event - Hook event name
115
+ * @param {Function} handler - Hook handler function
116
+ * @param {Object} options - Hook options
117
+ * @param {number} options.priority - Hook priority (higher number = higher priority)
118
+ * @param {boolean} options.once - Whether the hook should run only once
119
+ * @returns {BasePlugin} Plugin instance for chaining
120
+ */
121
+ registerHook(event, handler, options = {}) {
122
+ if (!this.hooks[event]) {
123
+ this.hooks[event] = [];
124
+ }
125
+
126
+ this.hooks[event].push({
127
+ handler,
128
+ priority: options.priority || 0,
129
+ once: options.once || false,
130
+ });
131
+
132
+ // Sort hooks by priority (higher priority first)
133
+ this.hooks[event].sort((a, b) => b.priority - a.priority);
134
+
135
+ return this;
136
+ }
137
+
138
+ /**
139
+ * Trigger a hook
140
+ * @param {string} event - Hook event name
141
+ * @param {...any} args - Arguments to pass to hook handlers
142
+ * @returns {Promise<Array>} Results from all hook handlers
143
+ */
144
+ async triggerHook(event, ...args) {
145
+ if (!this.hooks[event]) {
146
+ return [];
147
+ }
148
+
149
+ const results = [];
150
+ const hooksToRemove = [];
151
+
152
+ for (let i = 0; i < this.hooks[event].length; i++) {
153
+ const hook = this.hooks[event][i];
154
+ try {
155
+ const result = await hook.handler(...args);
156
+ results.push(result);
157
+
158
+ if (hook.once) {
159
+ hooksToRemove.push(i);
160
+ }
161
+ } catch (error) {
162
+ console.error(`Error in hook "${event}" for plugin "${this.pluginName}":`, error);
163
+ // Continue with other hooks even if one fails
164
+ }
165
+ }
166
+
167
+ // Remove once hooks
168
+ if (hooksToRemove.length > 0) {
169
+ for (let i = hooksToRemove.length - 1; i >= 0; i--) {
170
+ this.hooks[event].splice(hooksToRemove[i], 1);
171
+ }
172
+ }
173
+
174
+ return results;
175
+ }
176
+
177
+ /**
178
+ * Register a middleware
179
+ * @param {string} type - Middleware type
180
+ * @param {Function} middleware - Middleware function
181
+ * @param {Object} options - Middleware options
182
+ * @param {number} options.priority - Middleware priority (higher number = higher priority)
183
+ * @returns {BasePlugin} Plugin instance for chaining
184
+ */
185
+ registerMiddleware(type, middleware, options = {}) {
186
+ if (!this.middlewares[type]) {
187
+ this.middlewares[type] = [];
188
+ }
189
+
190
+ this.middlewares[type].push({
191
+ middleware,
192
+ priority: options.priority || 0,
193
+ });
194
+
195
+ // Sort middlewares by priority (higher priority first)
196
+ this.middlewares[type].sort((a, b) => b.priority - a.priority);
197
+
198
+ return this;
199
+ }
200
+
201
+ /**
202
+ * Execute middlewares
203
+ * @param {string} type - Middleware type
204
+ * @param {*} context - Context object
205
+ * @param {...any} args - Additional arguments
206
+ * @returns {Promise<*>} Result after middleware processing
207
+ */
208
+ async executeMiddlewares(type, context, ...args) {
209
+ if (!this.middlewares[type]) {
210
+ return context;
211
+ }
212
+
213
+ let index = 0;
214
+ const middlewares = this.middlewares[type];
215
+
216
+ const next = async () => {
217
+ if (index >= middlewares.length) {
218
+ return context;
219
+ }
220
+
221
+ const mw = middlewares[index++];
222
+ try {
223
+ return await mw.middleware(context, next, ...args);
224
+ } catch (error) {
225
+ console.error(`Error in middleware "${type}" for plugin "${this.pluginName}":`, error);
226
+ throw error;
227
+ }
228
+ };
229
+
230
+ return await next();
231
+ }
232
+
233
+ /**
234
+ * Cleanup plugin resources
235
+ * This method should be called when plugin is being removed
236
+ * @returns {Promise<void>}
237
+ */
238
+ async cleanup() {
239
+ // Remove bound methods from QueryBuilder
240
+ Object.keys(this.methods).forEach(methodName => {
241
+ if (this.queryBuilder[methodName]) {
242
+ delete this.queryBuilder[methodName];
243
+ }
244
+ });
245
+
246
+ // Clear hooks and middlewares
247
+ this.hooks = {};
248
+ this.middlewares = {};
249
+ this.methods = {};
250
+ this.initialized = false;
251
+ }
252
+
253
+ /**
254
+ * Get plugin metadata
255
+ * @returns {Object} Plugin metadata
256
+ */
257
+ getMetadata() {
258
+ return {
259
+ name: this.pluginName,
260
+ version: '1.0.0',
261
+ description: 'Base plugin class',
262
+ dependencies: [],
263
+ methods: Object.keys(this.methods),
264
+ hooks: Object.keys(this.hooks),
265
+ middlewares: Object.keys(this.middlewares),
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Validate plugin configuration
271
+ * @param {Object} config - Plugin configuration
272
+ * @returns {Object} Validation result
273
+ */
274
+ validateConfig(config) {
275
+ const errors = [];
276
+ const warnings = [];
277
+
278
+ // Check required methods
279
+ if (typeof this._registerMethods !== 'function') {
280
+ errors.push('Plugin must implement _registerMethods() method');
281
+ }
282
+
283
+ // Check plugin name
284
+ if (!this.pluginName || this.pluginName === 'BasePlugin') {
285
+ warnings.push('Plugin should have a unique name');
286
+ }
287
+
288
+ return {
289
+ valid: errors.length === 0,
290
+ errors,
291
+ warnings,
292
+ };
293
+ }
294
+
295
+ /**
296
+ * Get plugin configuration
297
+ * @returns {Object} Plugin configuration
298
+ */
299
+ getConfig() {
300
+ return {
301
+ name: this.pluginName,
302
+ enabled: this.initialized,
303
+ methods: Object.keys(this.methods),
304
+ hooks: Object.keys(this.hooks),
305
+ middlewares: Object.keys(this.middlewares),
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Check if plugin is initialized
311
+ * @returns {boolean} True if plugin is initialized
312
+ */
313
+ isInitialized() {
314
+ return this.initialized;
315
+ }
316
+
317
+ /**
318
+ * Get plugin name
319
+ * @returns {string} Plugin name
320
+ */
321
+ getName() {
322
+ return this.pluginName;
323
+ }
324
+
325
+ /**
326
+ * Get all registered methods
327
+ * @returns {Array<string>} Array of method names
328
+ */
329
+ getMethods() {
330
+ return Object.keys(this.methods);
331
+ }
332
+
333
+ /**
334
+ * Get all registered hooks
335
+ * @returns {Object} Object containing hook events and handlers
336
+ */
337
+ getHooks() {
338
+ return this.hooks;
339
+ }
340
+
341
+ /**
342
+ * Get all registered middlewares
343
+ * @returns {Object} Object containing middleware types and handlers
344
+ */
345
+ getMiddlewares() {
346
+ return this.middlewares;
347
+ }
348
+
349
+ /**
350
+ * Check if a method exists
351
+ * @param {string} methodName - Method name to check
352
+ * @returns {boolean} True if method exists
353
+ */
354
+ hasMethod(methodName) {
355
+ return this.methods.hasOwnProperty(methodName);
356
+ }
357
+
358
+ /**
359
+ * Check if a hook exists for an event
360
+ * @param {string} event - Hook event name
361
+ * @returns {boolean} True if hook exists for the event
362
+ */
363
+ hasHook(event) {
364
+ return this.hooks.hasOwnProperty(event) && this.hooks[event].length > 0;
365
+ }
366
+
367
+ /**
368
+ * Check if a middleware exists for a type
369
+ * @param {string} type - Middleware type
370
+ * @returns {boolean} True if middleware exists for the type
371
+ */
372
+ hasMiddleware(type) {
373
+ return this.middlewares.hasOwnProperty(type) && this.middlewares[type].length > 0;
374
+ }
375
+
376
+ /**
377
+ * Get QueryBuilder instance
378
+ * @returns {Object} QueryBuilder instance
379
+ */
380
+ getQueryBuilder() {
381
+ return this.queryBuilder;
382
+ }
383
+
384
+ /**
385
+ * Set QueryBuilder instance
386
+ * @param {Object} queryBuilder - QueryBuilder instance
387
+ * @returns {BasePlugin} Plugin instance for chaining
388
+ */
389
+ setQueryBuilder(queryBuilder) {
390
+ this.queryBuilder = queryBuilder;
391
+ return this;
392
+ }
393
+
394
+ /**
395
+ * Reload plugin
396
+ * This method can be used to reload plugin configuration
397
+ * @returns {Promise<void>}
398
+ */
399
+ async reload() {
400
+ await this.cleanup();
401
+ await this.init();
402
+ }
403
+
404
+ /**
405
+ * Get plugin status
406
+ * @returns {Object} Plugin status information
407
+ */
408
+ getStatus() {
409
+ return {
410
+ name: this.pluginName,
411
+ initialized: this.initialized,
412
+ methodsCount: Object.keys(this.methods).length,
413
+ hooksCount: Object.keys(this.hooks).length,
414
+ middlewaresCount: Object.keys(this.middlewares).length,
415
+ queryBuilder: this.queryBuilder ? 'Connected' : 'Not connected'
416
+ };
417
+ }
418
+ }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/database/plugin/BatchOperationPlugin
6
+ */
7
+ import { BasePlugin } from "./BasePlugin.js";
8
+
9
+ /**
10
+ * Batch Operation Plugin - Optimizes performance and reliability for bulk data operations
11
+ */
12
+ export class BatchOperationPlugin extends BasePlugin {
13
+ constructor(queryBuilder) {
14
+ super(queryBuilder);
15
+ this.pluginName = "BatchOperationPlugin";
16
+ }
17
+
18
+ _registerMethods() {
19
+ // Register batch operation methods to QueryBuilder
20
+ this.queryBuilder.batchUpdate = this.batchUpdate.bind(this);
21
+ this.queryBuilder.batchDelete = this.batchDelete.bind(this);
22
+ this.queryBuilder.batchUpsert = this.batchUpsert.bind(this);
23
+ this.queryBuilder.batchIncrementDecrement = this.batchIncrementDecrement.bind(this);
24
+ this.queryBuilder.insertBatch = this.insertBatch.bind(this);
25
+ this.queryBuilder._optimizeChunkSize = this._optimizeChunkSize.bind(this);
26
+ }
27
+
28
+ /**
29
+ * Batch update multiple records
30
+ * @param {Array} data - Array of data objects for update
31
+ * @param {string} keyField - Key field name (default: 'id')
32
+ * @returns {Promise<Array>} Update results
33
+ */
34
+ async batchUpdate(data, keyField = "id") {
35
+ if (!Array.isArray(data) || data.length === 0) {
36
+ throw new Error("Data must be a non-empty array for batch update");
37
+ }
38
+
39
+ // Execute updates in parallel
40
+ const promises = data.map((item) => {
41
+ const keyValue = item[keyField];
42
+ const updateData = { ...item };
43
+ delete updateData[keyField];
44
+
45
+ return this.queryBuilder.clone()
46
+ .where(keyField, "=", keyValue)
47
+ .update(updateData)
48
+ .execute();
49
+ });
50
+
51
+ return Promise.all(promises);
52
+ }
53
+
54
+ /**
55
+ * Batch delete multiple records by key values
56
+ * @param {Array} values - Array of key values to delete
57
+ * @param {string} keyField - Key field name (default: 'id')
58
+ * @returns {Promise<Object>} Delete result
59
+ */
60
+ async batchDelete(values, keyField = "id") {
61
+ return this.queryBuilder.whereIn(keyField, values).delete().execute();
62
+ }
63
+
64
+ /**
65
+ * Batch upsert (insert or update)
66
+ * @param {Array} data - Array of data objects
67
+ * @param {Array} uniqueColumns - Unique constraint columns
68
+ * @param {number} chunkSize - Chunk size for batch processing
69
+ * @returns {Promise<Array>} Upsert results
70
+ */
71
+ async batchUpsert(data, uniqueColumns = ["id"], chunkSize = 100) {
72
+ if (!Array.isArray(data) || data.length === 0) {
73
+ throw new Error("Data must be a non-empty array");
74
+ }
75
+
76
+ const results = [];
77
+
78
+ // Process in chunks using Promise.all for parallel execution
79
+ for (let i = 0; i < data.length; i += chunkSize) {
80
+ const chunk = data.slice(i, i + chunkSize);
81
+ const chunkPromises = chunk.map((item) =>
82
+ this.queryBuilder.clone().upsert(item, uniqueColumns),
83
+ );
84
+
85
+ const chunkResults = await Promise.all(chunkPromises);
86
+ results.push(...chunkResults);
87
+ }
88
+
89
+ return results;
90
+ }
91
+
92
+ /**
93
+ * Batch increment/decrement
94
+ * @param {string} field - Field to increment/decrement
95
+ * @param {Object} values - Object with key: amount pairs
96
+ * @param {string} keyField - Key field name (default: 'id')
97
+ * @param {boolean} increment - True for increment, false for decrement
98
+ * @returns {Promise<Array>} Update results
99
+ */
100
+ async batchIncrementDecrement(
101
+ field,
102
+ values,
103
+ keyField = "id",
104
+ increment = true,
105
+ ) {
106
+ const results = [];
107
+ for (const [keyValue, amount] of Object.entries(values)) {
108
+ const query = this.queryBuilder.clone().where(keyField, "=", keyValue);
109
+ const result = increment
110
+ ? await query.increment(field, amount)
111
+ : await query.decrement(field, amount);
112
+ results.push(result);
113
+ }
114
+ return results;
115
+ }
116
+
117
+ /**
118
+ * Batch insert with optimized chunk size
119
+ * @param {Array} data - Array of data objects
120
+ * @param {number} chunkSize - Chunk size for batch processing
121
+ * @returns {Promise<Object>} Insert results
122
+ */
123
+ async insertBatch(data, chunkSize = 1000) {
124
+ if (!Array.isArray(data) || data.length === 0) {
125
+ throw new Error("insertBatch: data must be a non-empty array");
126
+ }
127
+
128
+ // Optimize chunk size based on data structure
129
+ const optimizedChunkSize = this._optimizeChunkSize(chunkSize, data[0]);
130
+
131
+ const results = [];
132
+ for (let i = 0; i < data.length; i += optimizedChunkSize) {
133
+ const chunk = data.slice(i, i + optimizedChunkSize);
134
+ const result = await this.queryBuilder.clone().insert(chunk).execute();
135
+ results.push(result);
136
+ }
137
+
138
+ return {
139
+ success: true,
140
+ totalInserted: data.length,
141
+ affectedRows: results.reduce((sum, r) => sum + (r.affectedRows || 0), 0),
142
+ chunks: results.length,
143
+ details: results,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Optimize chunk size based on data structure
149
+ * @param {number} defaultSize - Default chunk size
150
+ * @param {Object} sampleRow - Sample data row
151
+ * @returns {number} Optimized chunk size
152
+ */
153
+ _optimizeChunkSize(defaultSize, sampleRow) {
154
+ if (!sampleRow) return Math.min(defaultSize, 1000);
155
+
156
+ const columnCount = Object.keys(sampleRow).length;
157
+
158
+ // MySQL recommendation: each SQL should not exceed 4~16MB
159
+ if (columnCount > 35) return Math.min(defaultSize, 350); // Very wide table
160
+ if (columnCount > 20) return Math.min(defaultSize, 650);
161
+ if (columnCount > 12) return Math.min(defaultSize, 1100);
162
+ if (columnCount > 6) return Math.min(defaultSize, 1800);
163
+ return Math.min(defaultSize, 2500); // Narrow table can be larger
164
+ }
165
+ }