@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,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
|
+
}
|