@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.
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,227 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/database/plugin/ShardingPlugin
6
+ */
7
+ import { BasePlugin } from './BasePlugin.js';
8
+ import crypto from 'crypto';
9
+
10
+ /**
11
+ * Sharding Plugin - Provides horizontal and vertical data sharding/partitioning support.
12
+ * Dynamically routes queries to the correct physical table based on shard/partition keys.
13
+ */
14
+ export class ShardingPlugin extends BasePlugin {
15
+ constructor(queryBuilder) {
16
+ super(queryBuilder);
17
+ this.shardKey = null;
18
+ this.partitionKey = null;
19
+ this.shardStrategy = 'hash'; // 'hash', 'range', 'list'
20
+ this.partitionStrategy = 'time'; // 'time', 'hash', 'range'
21
+ this.totalShards = 10;
22
+ this.partitionFormat = 'pYYYYMM'; // Format for time-based partitions
23
+ }
24
+
25
+ _registerMethods() {
26
+ // Register sharding methods to QueryBuilder
27
+ this.queryBuilder.shard = this.shard.bind(this);
28
+ this.queryBuilder.partition = this.partition.bind(this);
29
+ this.queryBuilder.getActualTableName = this.getActualTableName.bind(this);
30
+ this.queryBuilder.calculateShardKey = this.calculateShardKey.bind(this);
31
+ this.queryBuilder.shardRoute = this.shardRoute.bind(this);
32
+ this.queryBuilder.setShardingConfig = this.setShardingConfig.bind(this);
33
+ }
34
+
35
+ /**
36
+ * Set the shard key for horizontal partitioning.
37
+ * @param {string} shardKey - The value used to determine the shard.
38
+ * @returns {QueryBuilder} The QueryBuilder instance for chaining.
39
+ */
40
+ shard(shardKey) {
41
+ this.shardKey = shardKey;
42
+ return this.queryBuilder;
43
+ }
44
+
45
+ /**
46
+ * Set the partition key for vertical partitioning (e.g., time-based).
47
+ * @param {string|Date} partitionKey - The value used to determine the partition.
48
+ * @returns {QueryBuilder} The QueryBuilder instance for chaining.
49
+ */
50
+ partition(partitionKey) {
51
+ this.partitionKey = partitionKey;
52
+ return this.queryBuilder;
53
+ }
54
+
55
+ /**
56
+ * Configures the sharding/partitioning strategy.
57
+ * @param {Object} config - Configuration options.
58
+ * @returns {QueryBuilder} The QueryBuilder instance.
59
+ */
60
+ setShardingConfig(config = {}) {
61
+ Object.assign(this, config);
62
+ return this.queryBuilder;
63
+ }
64
+
65
+ /**
66
+ * Calculates the actual physical table name based on shard/partition keys.
67
+ * @returns {string} The actual table name to use in the query.
68
+ */
69
+ getActualTableName() {
70
+ let tableName = this.queryBuilder.tableName;
71
+
72
+ // Apply sharding (horizontal)
73
+ if (this.shardKey) {
74
+ let shardSuffix;
75
+ switch (this.shardStrategy) {
76
+ case 'hash':
77
+ shardSuffix = this._hashShard(this.shardKey);
78
+ break;
79
+ case 'range':
80
+ shardSuffix = this._rangeShard(this.shardKey);
81
+ break;
82
+ case 'list':
83
+ shardSuffix = this._listShard(this.shardKey);
84
+ break;
85
+ default:
86
+ shardSuffix = this._hashShard(this.shardKey);
87
+ }
88
+ tableName = `${tableName}_${shardSuffix}`;
89
+ }
90
+
91
+ // Apply partitioning (vertical, e.g., by time)
92
+ if (this.partitionKey) {
93
+ let partitionSuffix;
94
+ switch (this.partitionStrategy) {
95
+ case 'time':
96
+ partitionSuffix = this._timePartition(this.partitionKey);
97
+ break;
98
+ case 'hash':
99
+ partitionSuffix = this._hashPartition(this.partitionKey);
100
+ break;
101
+ case 'range':
102
+ partitionSuffix = this._rangePartition(this.partitionKey);
103
+ break;
104
+ default:
105
+ partitionSuffix = this._timePartition(this.partitionKey);
106
+ }
107
+ tableName = `${tableName}_${partitionSuffix}`;
108
+ }
109
+
110
+ return tableName;
111
+ }
112
+
113
+ /**
114
+ * Calculates a shard key using consistent hashing.
115
+ * @param {*} value - The value to hash (e.g., user ID).
116
+ * @param {number} totalShards - Total number of shards.
117
+ * @returns {string} The calculated shard identifier.
118
+ */
119
+ calculateShardKey(value, totalShards = this.totalShards) {
120
+ const hash = crypto.createHash('md5').update(String(value)).digest('hex');
121
+ const shardNum = parseInt(hash.substring(0, 8), 16) % totalShards;
122
+ return `shard${shardNum}`;
123
+ }
124
+
125
+ /**
126
+ * Routes an insert operation to the appropriate shard based on data.
127
+ * @param {Object} data - The data to insert.
128
+ * @param {string} keyField - The field used for shard calculation (default: 'id').
129
+ * @returns {Promise<Object>} The result of the insert operation.
130
+ */
131
+ async shardRoute(data, keyField = 'id') {
132
+ const shardValue = data[keyField];
133
+ if (!shardValue) {
134
+ throw new Error(`Shard key field '${keyField}' not found in data.`);
135
+ }
136
+ const shardKey = this.calculateShardKey(shardValue);
137
+ return this.queryBuilder.clone().shard(shardKey).insert(data).execute();
138
+ }
139
+
140
+ /**
141
+ * Hash-based sharding strategy.
142
+ * @private
143
+ */
144
+ _hashShard(value) {
145
+ return this.calculateShardKey(value);
146
+ }
147
+
148
+ /**
149
+ * Range-based sharding strategy (example: by numeric ranges).
150
+ * @private
151
+ */
152
+ _rangeShard(value) {
153
+ // Example: shard by user_id ranges (0-999 -> shard0, 1000-1999 -> shard1, etc.)
154
+ const num = Number(value);
155
+ if (isNaN(num)) {
156
+ throw new Error('Range sharding requires a numeric value.');
157
+ }
158
+ const shardNum = Math.floor(num / 1000) % this.totalShards;
159
+ return `shard${shardNum}`;
160
+ }
161
+
162
+ /**
163
+ * List-based sharding strategy (example: by region).
164
+ * @private
165
+ */
166
+ _listShard(value) {
167
+ // Example: map specific values to specific shards
168
+ const shardMap = {
169
+ 'us': 'shard0',
170
+ 'eu': 'shard1',
171
+ 'asia': 'shard2'
172
+ // ... extend as needed
173
+ };
174
+ return shardMap[value] || this._hashShard(value);
175
+ }
176
+
177
+ /**
178
+ * Time-based partitioning strategy.
179
+ * @private
180
+ */
181
+ _timePartition(value) {
182
+ const date = new Date(value);
183
+ const year = date.getFullYear();
184
+ const month = String(date.getMonth() + 1).padStart(2, '0');
185
+ // Supports formats like pYYYYMM, pYYYYMMDD, etc.
186
+ return this.partitionFormat
187
+ .replace('YYYY', year)
188
+ .replace('MM', month)
189
+ .replace('DD', String(date.getDate()).padStart(2, '0'));
190
+ }
191
+
192
+ /**
193
+ * Hash-based partitioning strategy.
194
+ * @private
195
+ */
196
+ _hashPartition(value) {
197
+ const hash = crypto.createHash('md5').update(String(value)).digest('hex');
198
+ const partitionNum = parseInt(hash.substring(0, 4), 16) % 100; // Example: 100 partitions
199
+ return `part${String(partitionNum).padStart(3, '0')}`;
200
+ }
201
+
202
+ /**
203
+ * Range-based partitioning strategy (example: by date ranges).
204
+ * @private
205
+ */
206
+ _rangePartition(value) {
207
+ const date = new Date(value);
208
+ const year = date.getFullYear();
209
+ const quarter = Math.floor(date.getMonth() / 3) + 1;
210
+ return `y${year}q${quarter}`;
211
+ }
212
+
213
+ /**
214
+ * Override the internal method that builds the final SQL to use the actual table name.
215
+ * This method should be called by the plugin after the main QueryBuilder registers it.
216
+ */
217
+ applyTableNameOverride() {
218
+ const originalGetActualTableName = this.queryBuilder.getActualTableName;
219
+ this.queryBuilder.getActualTableName = () => {
220
+ // Use the plugin's logic if sharding/partitioning is active, otherwise fall back
221
+ if (this.shardKey || this.partitionKey) {
222
+ return this.getActualTableName();
223
+ }
224
+ return originalGetActualTableName ? originalGetActualTableName.call(this.queryBuilder) : this.queryBuilder.tableName;
225
+ };
226
+ }
227
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/database/plugin/SoftDeletePlugin
6
+ */
7
+ import { BasePlugin } from './BasePlugin.js';
8
+
9
+ /**
10
+ * Soft Delete Plugin - Provides soft delete functionality
11
+ * Automatically adds deleted_at IS NULL condition to queries
12
+ */
13
+ export class SoftDeletePlugin extends BasePlugin {
14
+ constructor(queryBuilder) {
15
+ super(queryBuilder);
16
+ this.softDeleteEnabled = false;
17
+ this.softDeleteColumn = 'deleted_at';
18
+ this.deletedByColumn = 'deleted_by';
19
+ this.deletedReasonColumn = 'deleted_reason';
20
+ this.restoredAtColumn = 'restored_at';
21
+ this.restoredByColumn = 'restored_by';
22
+ }
23
+
24
+ /**
25
+ * Initialize the plugin
26
+ * @returns {Promise<void>}
27
+ */
28
+ async init() {
29
+ if (this.initialized) return;
30
+ this.initialized = true;
31
+ this._registerMethods();
32
+ }
33
+
34
+ /**
35
+ * Register plugin methods to QueryBuilder
36
+ * @protected
37
+ */
38
+ _registerMethods() {
39
+ this.queryBuilder.enableSoftDelete = this.enableSoftDelete.bind(this);
40
+ this.queryBuilder.softDelete = this.softDelete.bind(this);
41
+ this.queryBuilder.restore = this.restore.bind(this);
42
+ this.queryBuilder.withTrashed = this.withTrashed.bind(this);
43
+ this.queryBuilder.onlyTrashed = this.onlyTrashed.bind(this);
44
+ this.queryBuilder.forceDelete = this.forceDelete.bind(this);
45
+
46
+ // Override the execute method to add soft delete filtering
47
+ const originalExecute = this.queryBuilder.execute;
48
+ this.queryBuilder.execute = async function() {
49
+ // Add soft delete filter for SELECT queries
50
+ if (this.query.type === 'select' && this.softDeleteEnabled && !this.query.includeDeleted) {
51
+ this.whereNull(this.softDeleteColumn);
52
+ }
53
+ return originalExecute.call(this);
54
+ }.bind(this.queryBuilder);
55
+
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Cleanup plugin resources
61
+ * @returns {Promise<void>}
62
+ */
63
+ async cleanup() {
64
+ delete this.queryBuilder.enableSoftDelete;
65
+ delete this.queryBuilder.softDelete;
66
+ delete this.queryBuilder.restore;
67
+ delete this.queryBuilder.withTrashed;
68
+ delete this.queryBuilder.onlyTrashed;
69
+ delete this.queryBuilder.forceDelete;
70
+
71
+ // Restore original execute method
72
+ // Note: This requires storing the original method reference
73
+
74
+ this.initialized = false;
75
+ }
76
+
77
+ /**
78
+ * Enable soft delete functionality
79
+ * @param {Object} options - Configuration options
80
+ * @param {string} options.column - Soft delete column name (default: 'deleted_at')
81
+ * @param {string} options.deletedByColumn - Deleted by column name (default: 'deleted_by')
82
+ * @param {string} options.deletedReasonColumn - Deleted reason column name (default: 'deleted_reason')
83
+ * @param {string} options.restoredAtColumn - Restored at column name (default: 'restored_at')
84
+ * @param {string} options.restoredByColumn - Restored by column name (default: 'restored_by')
85
+ * @returns {QueryBuilder} Query builder instance
86
+ */
87
+ enableSoftDelete(options = {}) {
88
+ this.softDeleteEnabled = true;
89
+ this.softDeleteColumn = options.column || 'deleted_at';
90
+ this.deletedByColumn = options.deletedByColumn || 'deleted_by';
91
+ this.deletedReasonColumn = options.deletedReasonColumn || 'deleted_reason';
92
+ this.restoredAtColumn = options.restoredAtColumn || 'restored_at';
93
+ this.restoredByColumn = options.restoredByColumn || 'restored_by';
94
+
95
+ return this.queryBuilder;
96
+ }
97
+
98
+ /**
99
+ * Perform soft delete (mark as deleted instead of physical delete)
100
+ * @param {Object} options - Soft delete options
101
+ * @param {string|number} options.deletedBy - User ID or name who performed deletion
102
+ * @param {string} options.reason - Reason for deletion
103
+ * @returns {Promise<Object>} Delete result
104
+ */
105
+ async softDelete(options = {}) {
106
+ if (!this.softDeleteEnabled) {
107
+ throw new Error('Soft delete is not enabled. Call enableSoftDelete() first.');
108
+ }
109
+
110
+ const updateData = {
111
+ [this.softDeleteColumn]: new Date()
112
+ };
113
+
114
+ // Add deleted_by if provided
115
+ if (options.deletedBy !== undefined) {
116
+ updateData[this.deletedByColumn] = options.deletedBy;
117
+ }
118
+
119
+ // Add deleted_reason if provided
120
+ if (options.reason !== undefined) {
121
+ updateData[this.deletedReasonColumn] = options.reason;
122
+ }
123
+
124
+ // Ensure we only update non-deleted records
125
+ if (!this.queryBuilder.query.where.some(
126
+ w => w.column === this.softDeleteColumn && w.type === 'null'
127
+ )) {
128
+ this.queryBuilder.whereNull(this.softDeleteColumn);
129
+ }
130
+
131
+ return this.queryBuilder
132
+ .update(updateData)
133
+ .execute();
134
+ }
135
+
136
+ /**
137
+ * Restore soft deleted records
138
+ * @param {Object} options - Restore options
139
+ * @param {string|number} options.restoredBy - User ID or name who performed restoration
140
+ * @returns {Promise<Object>} Update result
141
+ */
142
+ async restore(options = {}) {
143
+ if (!this.softDeleteEnabled) {
144
+ throw new Error('Soft delete is not enabled. Call enableSoftDelete() first.');
145
+ }
146
+
147
+ const updateData = {
148
+ [this.softDeleteColumn]: null,
149
+ [this.restoredAtColumn]: new Date()
150
+ };
151
+
152
+ // Add restored_by if provided
153
+ if (options.restoredBy !== undefined) {
154
+ updateData[this.restoredByColumn] = options.restoredBy;
155
+ }
156
+
157
+ // Only restore deleted records
158
+ this.queryBuilder.whereNotNull(this.softDeleteColumn);
159
+
160
+ return this.queryBuilder
161
+ .update(updateData)
162
+ .execute();
163
+ }
164
+
165
+ /**
166
+ * Include soft deleted records in query
167
+ * @returns {QueryBuilder} Query builder instance
168
+ */
169
+ withTrashed() {
170
+ this.queryBuilder.query.includeDeleted = true;
171
+
172
+ // Remove soft delete filter if present
173
+ this.queryBuilder.query.where = this.queryBuilder.query.where.filter(
174
+ w => !(w.column === this.softDeleteColumn && w.type === 'null')
175
+ );
176
+
177
+ return this.queryBuilder;
178
+ }
179
+
180
+ /**
181
+ * Query only soft deleted records
182
+ * @returns {QueryBuilder} Query builder instance
183
+ */
184
+ onlyTrashed() {
185
+ if (!this.softDeleteEnabled) {
186
+ throw new Error('Soft delete is not enabled. Call enableSoftDelete() first.');
187
+ }
188
+
189
+ this.queryBuilder.whereNotNull(this.softDeleteColumn);
190
+ return this.queryBuilder;
191
+ }
192
+
193
+ /**
194
+ * Force delete (permanent delete)
195
+ * @returns {Promise<Object>} Delete result
196
+ */
197
+ async forceDelete() {
198
+ // Remove soft delete filter
199
+ this.queryBuilder.query.where = this.queryBuilder.query.where.filter(
200
+ w => !(w.column === this.softDeleteColumn && w.type === 'null')
201
+ );
202
+
203
+ return this.queryBuilder.delete().execute();
204
+ }
205
+
206
+ /**
207
+ * Get soft delete configuration
208
+ * @returns {Object} Configuration object
209
+ */
210
+ getConfig() {
211
+ return {
212
+ enabled: this.softDeleteEnabled,
213
+ column: this.softDeleteColumn,
214
+ deletedByColumn: this.deletedByColumn,
215
+ deletedReasonColumn: this.deletedReasonColumn,
216
+ restoredAtColumn: this.restoredAtColumn,
217
+ restoredByColumn: this.restoredByColumn
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Check if record is soft deleted
223
+ * @param {Object} record - Database record
224
+ * @returns {boolean} True if record is soft deleted
225
+ */
226
+ isSoftDeleted(record) {
227
+ if (!this.softDeleteEnabled) return false;
228
+ return record[this.softDeleteColumn] !== null && record[this.softDeleteColumn] !== undefined;
229
+ }
230
+
231
+ /**
232
+ * Get soft delete status
233
+ * @param {Object} record - Database record
234
+ * @returns {Object} Soft delete status information
235
+ */
236
+ getSoftDeleteStatus(record) {
237
+ if (!this.softDeleteEnabled) {
238
+ return { enabled: false, deleted: false };
239
+ }
240
+
241
+ const deleted = this.isSoftDeleted(record);
242
+ const status = {
243
+ enabled: true,
244
+ deleted,
245
+ deletedAt: record[this.softDeleteColumn],
246
+ deletedBy: record[this.deletedByColumn],
247
+ deletedReason: record[this.deletedReasonColumn]
248
+ };
249
+
250
+ if (!deleted && record[this.restoredAtColumn]) {
251
+ status.restored = true;
252
+ status.restoredAt = record[this.restoredAtColumn];
253
+ status.restoredBy = record[this.restoredByColumn];
254
+ }
255
+
256
+ return status;
257
+ }
258
+ }