s3db.js 9.2.0 → 9.2.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/PLUGINS.md +453 -102
- package/README.md +31 -2
- package/dist/s3db.cjs.js +1240 -565
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1240 -565
- package/dist/s3db.es.js.map +1 -1
- package/package.json +5 -5
- package/src/concerns/async-event-emitter.js +46 -0
- package/src/database.class.js +23 -0
- package/src/plugins/backup/base-backup-driver.class.js +119 -0
- package/src/plugins/backup/filesystem-backup-driver.class.js +254 -0
- package/src/plugins/backup/index.js +85 -0
- package/src/plugins/backup/multi-backup-driver.class.js +304 -0
- package/src/plugins/backup/s3-backup-driver.class.js +313 -0
- package/src/plugins/backup.plugin.js +375 -729
- package/src/plugins/backup.plugin.js.backup +1026 -0
- package/src/plugins/scheduler.plugin.js +0 -1
- package/src/resource.class.js +156 -41
package/src/resource.class.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { join } from "path";
|
|
2
|
-
import EventEmitter from "events";
|
|
3
2
|
import { createHash } from "crypto";
|
|
3
|
+
import AsyncEventEmitter from "./concerns/async-event-emitter.js";
|
|
4
4
|
import { customAlphabet, urlAlphabet } from 'nanoid';
|
|
5
5
|
import jsonStableStringify from "json-stable-stringify";
|
|
6
6
|
import { PromisePool } from "@supercharge/promise-pool";
|
|
@@ -16,7 +16,7 @@ import { calculateTotalSize, calculateEffectiveLimit } from "./concerns/calculat
|
|
|
16
16
|
import { mapAwsError, InvalidResourceItem, ResourceError, PartitionError } from "./errors.js";
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
export class Resource extends
|
|
19
|
+
export class Resource extends AsyncEventEmitter {
|
|
20
20
|
/**
|
|
21
21
|
* Create a new Resource instance
|
|
22
22
|
* @param {Object} config - Resource configuration
|
|
@@ -40,6 +40,7 @@ export class Resource extends EventEmitter {
|
|
|
40
40
|
* @param {number} [config.idSize=22] - Size for auto-generated IDs
|
|
41
41
|
* @param {boolean} [config.versioningEnabled=false] - Enable versioning for this resource
|
|
42
42
|
* @param {Object} [config.events={}] - Event listeners to automatically add
|
|
43
|
+
* @param {boolean} [config.asyncEvents=true] - Whether events should be emitted asynchronously
|
|
43
44
|
* @example
|
|
44
45
|
* const users = new Resource({
|
|
45
46
|
* name: 'users',
|
|
@@ -133,7 +134,9 @@ export class Resource extends EventEmitter {
|
|
|
133
134
|
idGenerator: customIdGenerator,
|
|
134
135
|
idSize = 22,
|
|
135
136
|
versioningEnabled = false,
|
|
136
|
-
events = {}
|
|
137
|
+
events = {},
|
|
138
|
+
asyncEvents = true,
|
|
139
|
+
asyncPartitions = true
|
|
137
140
|
} = config;
|
|
138
141
|
|
|
139
142
|
// Set instance properties
|
|
@@ -145,6 +148,9 @@ export class Resource extends EventEmitter {
|
|
|
145
148
|
this.parallelism = parallelism;
|
|
146
149
|
this.passphrase = passphrase ?? 'secret';
|
|
147
150
|
this.versioningEnabled = versioningEnabled;
|
|
151
|
+
|
|
152
|
+
// Configure async events mode
|
|
153
|
+
this.setAsyncMode(asyncEvents);
|
|
148
154
|
|
|
149
155
|
// Configure ID generator
|
|
150
156
|
this.idGenerator = this.configureIdGenerator(customIdGenerator, idSize);
|
|
@@ -171,6 +177,8 @@ export class Resource extends EventEmitter {
|
|
|
171
177
|
partitions,
|
|
172
178
|
autoDecrypt,
|
|
173
179
|
allNestedObjectsOptional,
|
|
180
|
+
asyncEvents,
|
|
181
|
+
asyncPartitions,
|
|
174
182
|
};
|
|
175
183
|
|
|
176
184
|
// Initialize hooks system
|
|
@@ -811,14 +819,42 @@ export class Resource extends EventEmitter {
|
|
|
811
819
|
// Get the inserted object
|
|
812
820
|
const insertedObject = await this.get(finalId);
|
|
813
821
|
|
|
814
|
-
//
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
+
// Handle partition indexing based on asyncPartitions config
|
|
823
|
+
if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
|
|
824
|
+
// Async mode: create partition indexes in background
|
|
825
|
+
setImmediate(() => {
|
|
826
|
+
this.createPartitionReferences(insertedObject).catch(err => {
|
|
827
|
+
this.emit('partitionIndexError', {
|
|
828
|
+
operation: 'insert',
|
|
829
|
+
id: finalId,
|
|
830
|
+
error: err,
|
|
831
|
+
message: err.message
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Execute other afterInsert hooks synchronously (excluding partition hook)
|
|
837
|
+
const nonPartitionHooks = this.hooks.afterInsert.filter(hook =>
|
|
838
|
+
!hook.toString().includes('createPartitionReferences')
|
|
839
|
+
);
|
|
840
|
+
let finalResult = insertedObject;
|
|
841
|
+
for (const hook of nonPartitionHooks) {
|
|
842
|
+
finalResult = await hook(finalResult);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Emit insert event
|
|
846
|
+
this.emit('insert', finalResult);
|
|
847
|
+
return finalResult;
|
|
848
|
+
} else {
|
|
849
|
+
// Sync mode: execute all hooks including partition creation
|
|
850
|
+
const finalResult = await this.executeHooks('afterInsert', insertedObject);
|
|
851
|
+
|
|
852
|
+
// Emit insert event
|
|
853
|
+
this.emit('insert', finalResult);
|
|
854
|
+
|
|
855
|
+
// Return the final object
|
|
856
|
+
return finalResult;
|
|
857
|
+
}
|
|
822
858
|
}
|
|
823
859
|
|
|
824
860
|
/**
|
|
@@ -1081,13 +1117,46 @@ export class Resource extends EventEmitter {
|
|
|
1081
1117
|
body: finalBody,
|
|
1082
1118
|
behavior: this.behavior
|
|
1083
1119
|
});
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1120
|
+
|
|
1121
|
+
// Handle partition updates based on asyncPartitions config
|
|
1122
|
+
if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
|
|
1123
|
+
// Async mode: update partition indexes in background
|
|
1124
|
+
setImmediate(() => {
|
|
1125
|
+
this.handlePartitionReferenceUpdates(originalData, updatedData).catch(err => {
|
|
1126
|
+
this.emit('partitionIndexError', {
|
|
1127
|
+
operation: 'update',
|
|
1128
|
+
id,
|
|
1129
|
+
error: err,
|
|
1130
|
+
message: err.message
|
|
1131
|
+
});
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// Execute other afterUpdate hooks synchronously (excluding partition hook)
|
|
1136
|
+
const nonPartitionHooks = this.hooks.afterUpdate.filter(hook =>
|
|
1137
|
+
!hook.toString().includes('handlePartitionReferenceUpdates')
|
|
1138
|
+
);
|
|
1139
|
+
let finalResult = updatedData;
|
|
1140
|
+
for (const hook of nonPartitionHooks) {
|
|
1141
|
+
finalResult = await hook(finalResult);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
this.emit('update', {
|
|
1145
|
+
...updatedData,
|
|
1146
|
+
$before: { ...originalData },
|
|
1147
|
+
$after: { ...finalResult }
|
|
1148
|
+
});
|
|
1149
|
+
return finalResult;
|
|
1150
|
+
} else {
|
|
1151
|
+
// Sync mode: execute all hooks including partition updates
|
|
1152
|
+
const finalResult = await this.executeHooks('afterUpdate', updatedData);
|
|
1153
|
+
this.emit('update', {
|
|
1154
|
+
...updatedData,
|
|
1155
|
+
$before: { ...originalData },
|
|
1156
|
+
$after: { ...finalResult }
|
|
1157
|
+
});
|
|
1158
|
+
return finalResult;
|
|
1159
|
+
}
|
|
1091
1160
|
}
|
|
1092
1161
|
|
|
1093
1162
|
/**
|
|
@@ -1143,8 +1212,34 @@ export class Resource extends EventEmitter {
|
|
|
1143
1212
|
id
|
|
1144
1213
|
});
|
|
1145
1214
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1215
|
+
// Handle partition cleanup based on asyncPartitions config
|
|
1216
|
+
if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
|
|
1217
|
+
// Async mode: delete partition indexes in background
|
|
1218
|
+
setImmediate(() => {
|
|
1219
|
+
this.deletePartitionReferences(objectData).catch(err => {
|
|
1220
|
+
this.emit('partitionIndexError', {
|
|
1221
|
+
operation: 'delete',
|
|
1222
|
+
id,
|
|
1223
|
+
error: err,
|
|
1224
|
+
message: err.message
|
|
1225
|
+
});
|
|
1226
|
+
});
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// Execute other afterDelete hooks synchronously (excluding partition hook)
|
|
1230
|
+
const nonPartitionHooks = this.hooks.afterDelete.filter(hook =>
|
|
1231
|
+
!hook.toString().includes('deletePartitionReferences')
|
|
1232
|
+
);
|
|
1233
|
+
let afterDeleteData = objectData;
|
|
1234
|
+
for (const hook of nonPartitionHooks) {
|
|
1235
|
+
afterDeleteData = await hook(afterDeleteData);
|
|
1236
|
+
}
|
|
1237
|
+
return response;
|
|
1238
|
+
} else {
|
|
1239
|
+
// Sync mode: execute all hooks including partition deletion
|
|
1240
|
+
const afterDeleteData = await this.executeHooks('afterDelete', objectData);
|
|
1241
|
+
return response;
|
|
1242
|
+
}
|
|
1148
1243
|
}
|
|
1149
1244
|
|
|
1150
1245
|
/**
|
|
@@ -1937,21 +2032,36 @@ export class Resource extends EventEmitter {
|
|
|
1937
2032
|
return;
|
|
1938
2033
|
}
|
|
1939
2034
|
|
|
1940
|
-
// Create
|
|
1941
|
-
|
|
2035
|
+
// Create all partition references in parallel
|
|
2036
|
+
const promises = Object.entries(partitions).map(async ([partitionName, partition]) => {
|
|
1942
2037
|
const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
|
|
1943
2038
|
if (partitionKey) {
|
|
1944
2039
|
// Save only version as metadata, never object attributes
|
|
1945
2040
|
const partitionMetadata = {
|
|
1946
2041
|
_v: String(this.version)
|
|
1947
2042
|
};
|
|
1948
|
-
|
|
2043
|
+
return this.client.putObject({
|
|
1949
2044
|
key: partitionKey,
|
|
1950
2045
|
metadata: partitionMetadata,
|
|
1951
2046
|
body: '',
|
|
1952
2047
|
contentType: undefined,
|
|
1953
2048
|
});
|
|
1954
2049
|
}
|
|
2050
|
+
return null;
|
|
2051
|
+
});
|
|
2052
|
+
|
|
2053
|
+
// Wait for all partition references to be created
|
|
2054
|
+
const results = await Promise.allSettled(promises);
|
|
2055
|
+
|
|
2056
|
+
// Check for any failures
|
|
2057
|
+
const failures = results.filter(r => r.status === 'rejected');
|
|
2058
|
+
if (failures.length > 0) {
|
|
2059
|
+
// Emit warning but don't throw - partitions are secondary indexes
|
|
2060
|
+
this.emit('partitionIndexWarning', {
|
|
2061
|
+
operation: 'create',
|
|
2062
|
+
id: data.id,
|
|
2063
|
+
failures: failures.map(f => f.reason)
|
|
2064
|
+
});
|
|
1955
2065
|
}
|
|
1956
2066
|
}
|
|
1957
2067
|
|
|
@@ -2071,33 +2181,41 @@ export class Resource extends EventEmitter {
|
|
|
2071
2181
|
if (!partitions || Object.keys(partitions).length === 0) {
|
|
2072
2182
|
return;
|
|
2073
2183
|
}
|
|
2074
|
-
|
|
2184
|
+
|
|
2185
|
+
// Update all partitions in parallel
|
|
2186
|
+
const updatePromises = Object.entries(partitions).map(async ([partitionName, partition]) => {
|
|
2075
2187
|
const [ok, err] = await tryFn(() => this.handlePartitionReferenceUpdate(partitionName, partition, oldData, newData));
|
|
2076
2188
|
if (!ok) {
|
|
2077
2189
|
// console.warn(`Failed to update partition references for ${partitionName}:`, err.message);
|
|
2190
|
+
return { partitionName, error: err };
|
|
2078
2191
|
}
|
|
2079
|
-
|
|
2192
|
+
return { partitionName, success: true };
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
await Promise.allSettled(updatePromises);
|
|
2196
|
+
|
|
2197
|
+
// Aggressive cleanup: remove stale partition keys in parallel
|
|
2080
2198
|
const id = newData.id || oldData.id;
|
|
2081
|
-
|
|
2199
|
+
const cleanupPromises = Object.entries(partitions).map(async ([partitionName, partition]) => {
|
|
2082
2200
|
const prefix = `resource=${this.name}/partition=${partitionName}`;
|
|
2083
|
-
let allKeys = [];
|
|
2084
2201
|
const [okKeys, errKeys, keys] = await tryFn(() => this.client.getAllKeys({ prefix }));
|
|
2085
|
-
if (okKeys) {
|
|
2086
|
-
allKeys = keys;
|
|
2087
|
-
} else {
|
|
2202
|
+
if (!okKeys) {
|
|
2088
2203
|
// console.warn(`Aggressive cleanup: could not list keys for partition ${partitionName}:`, errKeys.message);
|
|
2089
|
-
|
|
2204
|
+
return;
|
|
2090
2205
|
}
|
|
2206
|
+
|
|
2091
2207
|
const validKey = this.getPartitionKey({ partitionName, id, data: newData });
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2208
|
+
const staleKeys = keys.filter(key => key.endsWith(`/id=${id}`) && key !== validKey);
|
|
2209
|
+
|
|
2210
|
+
if (staleKeys.length > 0) {
|
|
2211
|
+
const [okDel, errDel] = await tryFn(() => this.client.deleteObjects(staleKeys));
|
|
2212
|
+
if (!okDel) {
|
|
2213
|
+
// console.warn(`Aggressive cleanup: could not delete stale partition keys:`, errDel.message);
|
|
2098
2214
|
}
|
|
2099
2215
|
}
|
|
2100
|
-
}
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
await Promise.allSettled(cleanupPromises);
|
|
2101
2219
|
}
|
|
2102
2220
|
|
|
2103
2221
|
/**
|
|
@@ -2452,9 +2570,6 @@ export class Resource extends EventEmitter {
|
|
|
2452
2570
|
return filtered;
|
|
2453
2571
|
}
|
|
2454
2572
|
|
|
2455
|
-
emit(event, ...args) {
|
|
2456
|
-
return super.emit(event, ...args);
|
|
2457
|
-
}
|
|
2458
2573
|
|
|
2459
2574
|
async replace(id, attributes) {
|
|
2460
2575
|
await this.delete(id);
|