s3db.js 12.1.0 → 12.2.1
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/README.md +212 -196
- package/dist/s3db.cjs.js +1041 -1941
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1039 -1941
- package/dist/s3db.es.js.map +1 -1
- package/package.json +6 -1
- package/src/cli/index.js +954 -43
- package/src/cli/migration-manager.js +270 -0
- package/src/concerns/calculator.js +0 -4
- package/src/concerns/metadata-encoding.js +1 -21
- package/src/concerns/plugin-storage.js +17 -4
- package/src/concerns/typescript-generator.d.ts +171 -0
- package/src/concerns/typescript-generator.js +275 -0
- package/src/database.class.js +171 -28
- package/src/index.js +15 -9
- package/src/plugins/api/index.js +5 -2
- package/src/plugins/api/routes/resource-routes.js +86 -1
- package/src/plugins/api/server.js +79 -3
- package/src/plugins/api/utils/openapi-generator.js +195 -5
- package/src/plugins/backup/multi-backup-driver.class.js +0 -1
- package/src/plugins/backup.plugin.js +7 -14
- package/src/plugins/concerns/plugin-dependencies.js +73 -19
- package/src/plugins/eventual-consistency/analytics.js +0 -2
- package/src/plugins/eventual-consistency/consolidation.js +2 -13
- package/src/plugins/eventual-consistency/index.js +0 -1
- package/src/plugins/eventual-consistency/install.js +1 -1
- package/src/plugins/geo.plugin.js +5 -6
- package/src/plugins/importer/index.js +1 -1
- package/src/plugins/index.js +2 -1
- package/src/plugins/relation.plugin.js +11 -11
- package/src/plugins/replicator.plugin.js +12 -21
- package/src/plugins/s3-queue.plugin.js +4 -4
- package/src/plugins/scheduler.plugin.js +10 -12
- package/src/plugins/state-machine.plugin.js +8 -12
- package/src/plugins/tfstate/README.md +1 -1
- package/src/plugins/tfstate/errors.js +3 -3
- package/src/plugins/tfstate/index.js +41 -67
- package/src/plugins/ttl.plugin.js +3 -3
- package/src/resource.class.js +263 -61
- package/src/schema.class.js +0 -2
- package/src/testing/factory.class.js +286 -0
- package/src/testing/index.js +15 -0
- package/src/testing/seeder.class.js +183 -0
package/src/resource.class.js
CHANGED
|
@@ -185,14 +185,55 @@ export class Resource extends AsyncEventEmitter {
|
|
|
185
185
|
createdBy,
|
|
186
186
|
};
|
|
187
187
|
|
|
188
|
-
// Initialize hooks system
|
|
188
|
+
// Initialize hooks system - expanded to cover ALL methods
|
|
189
189
|
this.hooks = {
|
|
190
|
+
// Insert hooks
|
|
190
191
|
beforeInsert: [],
|
|
191
192
|
afterInsert: [],
|
|
193
|
+
|
|
194
|
+
// Update hooks
|
|
192
195
|
beforeUpdate: [],
|
|
193
196
|
afterUpdate: [],
|
|
197
|
+
|
|
198
|
+
// Delete hooks
|
|
194
199
|
beforeDelete: [],
|
|
195
|
-
afterDelete: []
|
|
200
|
+
afterDelete: [],
|
|
201
|
+
|
|
202
|
+
// Get hooks
|
|
203
|
+
beforeGet: [],
|
|
204
|
+
afterGet: [],
|
|
205
|
+
|
|
206
|
+
// List hooks
|
|
207
|
+
beforeList: [],
|
|
208
|
+
afterList: [],
|
|
209
|
+
|
|
210
|
+
// Query hooks
|
|
211
|
+
beforeQuery: [],
|
|
212
|
+
afterQuery: [],
|
|
213
|
+
|
|
214
|
+
// Patch hooks
|
|
215
|
+
beforePatch: [],
|
|
216
|
+
afterPatch: [],
|
|
217
|
+
|
|
218
|
+
// Replace hooks
|
|
219
|
+
beforeReplace: [],
|
|
220
|
+
afterReplace: [],
|
|
221
|
+
|
|
222
|
+
// Exists hooks
|
|
223
|
+
beforeExists: [],
|
|
224
|
+
afterExists: [],
|
|
225
|
+
|
|
226
|
+
// Count hooks
|
|
227
|
+
beforeCount: [],
|
|
228
|
+
afterCount: [],
|
|
229
|
+
|
|
230
|
+
// GetMany hooks
|
|
231
|
+
beforeGetMany: [],
|
|
232
|
+
afterGetMany: [],
|
|
233
|
+
|
|
234
|
+
// DeleteMany hooks
|
|
235
|
+
beforeDeleteMany: [],
|
|
236
|
+
afterDeleteMany: []
|
|
196
237
|
};
|
|
197
238
|
|
|
198
239
|
// Store attributes
|
|
@@ -285,20 +326,6 @@ export class Resource extends AsyncEventEmitter {
|
|
|
285
326
|
return idSize;
|
|
286
327
|
}
|
|
287
328
|
|
|
288
|
-
/**
|
|
289
|
-
* Get resource options (for backward compatibility with tests)
|
|
290
|
-
*/
|
|
291
|
-
get options() {
|
|
292
|
-
return {
|
|
293
|
-
timestamps: this.config.timestamps,
|
|
294
|
-
partitions: this.config.partitions || {},
|
|
295
|
-
cache: this.config.cache,
|
|
296
|
-
autoDecrypt: this.config.autoDecrypt,
|
|
297
|
-
paranoid: this.config.paranoid,
|
|
298
|
-
allNestedObjectsOptional: this.config.allNestedObjectsOptional
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
329
|
export() {
|
|
303
330
|
const exported = this.schema.export();
|
|
304
331
|
// Add all configuration at root level
|
|
@@ -458,23 +485,82 @@ export class Resource extends AsyncEventEmitter {
|
|
|
458
485
|
});
|
|
459
486
|
}
|
|
460
487
|
|
|
461
|
-
|
|
488
|
+
/**
|
|
489
|
+
* Validate data against resource schema without saving
|
|
490
|
+
* @param {Object} data - Data to validate
|
|
491
|
+
* @param {Object} options - Validation options
|
|
492
|
+
* @param {boolean} options.throwOnError - Throw error if validation fails (default: false)
|
|
493
|
+
* @param {boolean} options.includeId - Include ID validation (default: false)
|
|
494
|
+
* @param {boolean} options.mutateOriginal - Allow mutation of original data (default: false)
|
|
495
|
+
* @returns {Promise<{valid: boolean, isValid: boolean, errors: Array, data: Object, original: Object}>} Validation result
|
|
496
|
+
* @example
|
|
497
|
+
* // Validate before insert
|
|
498
|
+
* const result = await resource.validate({
|
|
499
|
+
* name: 'John Doe',
|
|
500
|
+
* email: 'invalid-email' // Will fail email validation
|
|
501
|
+
* });
|
|
502
|
+
*
|
|
503
|
+
* if (!result.valid) {
|
|
504
|
+
* console.log('Validation errors:', result.errors);
|
|
505
|
+
* // [{ field: 'email', message: '...', ... }]
|
|
506
|
+
* }
|
|
507
|
+
*
|
|
508
|
+
* // Throw on error
|
|
509
|
+
* try {
|
|
510
|
+
* await resource.validate({ email: 'bad' }, { throwOnError: true });
|
|
511
|
+
* } catch (err) {
|
|
512
|
+
* console.log('Validation failed:', err.message);
|
|
513
|
+
* }
|
|
514
|
+
*/
|
|
515
|
+
async validate(data, options = {}) {
|
|
516
|
+
const {
|
|
517
|
+
throwOnError = false,
|
|
518
|
+
includeId = false,
|
|
519
|
+
mutateOriginal = false
|
|
520
|
+
} = options;
|
|
521
|
+
|
|
522
|
+
// Clone data to avoid mutation (unless mutateOriginal is true)
|
|
523
|
+
const dataToValidate = mutateOriginal ? data : cloneDeep(data);
|
|
524
|
+
|
|
525
|
+
// If includeId is false, remove id from validation
|
|
526
|
+
if (!includeId && dataToValidate.id) {
|
|
527
|
+
delete dataToValidate.id;
|
|
528
|
+
}
|
|
529
|
+
|
|
462
530
|
const result = {
|
|
463
531
|
original: cloneDeep(data),
|
|
464
532
|
isValid: false,
|
|
465
533
|
errors: [],
|
|
534
|
+
data: dataToValidate
|
|
466
535
|
};
|
|
467
536
|
|
|
468
|
-
|
|
537
|
+
try {
|
|
538
|
+
const check = await this.schema.validate(dataToValidate, { mutateOriginal });
|
|
469
539
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
540
|
+
if (check === true) {
|
|
541
|
+
result.isValid = true;
|
|
542
|
+
} else {
|
|
543
|
+
result.errors = Array.isArray(check) ? check : [check];
|
|
544
|
+
result.isValid = false;
|
|
545
|
+
|
|
546
|
+
if (throwOnError) {
|
|
547
|
+
const error = new Error('Validation failed');
|
|
548
|
+
error.validationErrors = result.errors;
|
|
549
|
+
error.invalidData = data;
|
|
550
|
+
throw error;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} catch (err) {
|
|
554
|
+
// If schema.validate threw, and we're not in throwOnError mode, catch and return result
|
|
555
|
+
if (!throwOnError) {
|
|
556
|
+
result.errors = [{ message: err.message, error: err }];
|
|
557
|
+
result.isValid = false;
|
|
558
|
+
} else {
|
|
559
|
+
throw err;
|
|
560
|
+
}
|
|
474
561
|
}
|
|
475
562
|
|
|
476
|
-
result
|
|
477
|
-
return result
|
|
563
|
+
return result;
|
|
478
564
|
}
|
|
479
565
|
|
|
480
566
|
/**
|
|
@@ -798,7 +884,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
798
884
|
const exists = await this.exists(id);
|
|
799
885
|
if (exists) throw new Error(`Resource with id '${id}' already exists`);
|
|
800
886
|
const keyDebug = this.getResourceKey(id || '(auto)');
|
|
801
|
-
if (this.
|
|
887
|
+
if (this.config.timestamps) {
|
|
802
888
|
attributes.createdAt = new Date().toISOString();
|
|
803
889
|
attributes.updatedAt = new Date().toISOString();
|
|
804
890
|
}
|
|
@@ -806,7 +892,10 @@ export class Resource extends AsyncEventEmitter {
|
|
|
806
892
|
// Aplica defaults antes de tudo
|
|
807
893
|
const attributesWithDefaults = this.applyDefaults(attributes);
|
|
808
894
|
// Reconstruct the complete data for validation
|
|
809
|
-
|
|
895
|
+
// Only include id if it's defined (not undefined)
|
|
896
|
+
const completeData = id !== undefined
|
|
897
|
+
? { id, ...attributesWithDefaults }
|
|
898
|
+
: { ...attributesWithDefaults };
|
|
810
899
|
|
|
811
900
|
// Execute beforeInsert hooks
|
|
812
901
|
const preProcessedData = await this.executeHooks('beforeInsert', completeData);
|
|
@@ -822,7 +911,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
822
911
|
errors,
|
|
823
912
|
isValid,
|
|
824
913
|
data: validated,
|
|
825
|
-
} = await this.validate(preProcessedData);
|
|
914
|
+
} = await this.validate(preProcessedData, { includeId: true });
|
|
826
915
|
|
|
827
916
|
if (!isValid) {
|
|
828
917
|
const errorMsg = (errors && errors.length && errors[0].message) ? errors[0].message : 'Insert failed';
|
|
@@ -958,7 +1047,10 @@ export class Resource extends AsyncEventEmitter {
|
|
|
958
1047
|
async get(id) {
|
|
959
1048
|
if (isObject(id)) throw new Error(`id cannot be an object`);
|
|
960
1049
|
if (isEmpty(id)) throw new Error('id cannot be empty');
|
|
961
|
-
|
|
1050
|
+
|
|
1051
|
+
// Execute beforeGet hooks
|
|
1052
|
+
await this.executeHooks('beforeGet', { id });
|
|
1053
|
+
|
|
962
1054
|
const key = this.getResourceKey(id);
|
|
963
1055
|
// LOG: start of get
|
|
964
1056
|
// eslint-disable-next-line no-console
|
|
@@ -1032,18 +1124,87 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1032
1124
|
data = await this.applyVersionMapping(data, objectVersion, this.version);
|
|
1033
1125
|
}
|
|
1034
1126
|
|
|
1127
|
+
// Execute afterGet hooks
|
|
1128
|
+
data = await this.executeHooks('afterGet', data);
|
|
1129
|
+
|
|
1035
1130
|
this.emit("get", data);
|
|
1036
1131
|
const value = data;
|
|
1037
1132
|
return value;
|
|
1038
1133
|
}
|
|
1039
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* Retrieve a resource object by ID, or return null if not found
|
|
1137
|
+
* @param {string} id - Resource ID
|
|
1138
|
+
* @returns {Promise<Object|null>} The resource object or null if not found
|
|
1139
|
+
* @example
|
|
1140
|
+
* const user = await resource.getOrNull('user-123');
|
|
1141
|
+
* if (user) {
|
|
1142
|
+
* console.log('Found user:', user.name);
|
|
1143
|
+
* } else {
|
|
1144
|
+
* console.log('User not found');
|
|
1145
|
+
* }
|
|
1146
|
+
*/
|
|
1147
|
+
async getOrNull(id) {
|
|
1148
|
+
const [ok, err, data] = await tryFn(() => this.get(id));
|
|
1149
|
+
|
|
1150
|
+
// Check if error is NoSuchKey (resource doesn't exist)
|
|
1151
|
+
if (!ok && err && (err.name === 'NoSuchKey' || err.message?.includes('NoSuchKey'))) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// Re-throw other errors (permission denied, network issues, etc.)
|
|
1156
|
+
if (!ok) {
|
|
1157
|
+
throw err;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
return data;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Retrieve a resource object by ID, or throw ResourceNotFoundError if not found
|
|
1165
|
+
* @param {string} id - Resource ID
|
|
1166
|
+
* @returns {Promise<Object>} The resource object
|
|
1167
|
+
* @throws {ResourceError} If resource does not exist
|
|
1168
|
+
* @example
|
|
1169
|
+
* // Throws error if user doesn't exist (no need for null check)
|
|
1170
|
+
* const user = await resource.getOrThrow('user-123');
|
|
1171
|
+
* console.log('User name:', user.name); // Safe to access
|
|
1172
|
+
*/
|
|
1173
|
+
async getOrThrow(id) {
|
|
1174
|
+
const [ok, err, data] = await tryFn(() => this.get(id));
|
|
1175
|
+
|
|
1176
|
+
// Check if error is NoSuchKey (resource doesn't exist)
|
|
1177
|
+
if (!ok && err && (err.name === 'NoSuchKey' || err.message?.includes('NoSuchKey'))) {
|
|
1178
|
+
throw new ResourceError(`Resource '${this.name}' with id '${id}' not found`, {
|
|
1179
|
+
resourceName: this.name,
|
|
1180
|
+
operation: 'getOrThrow',
|
|
1181
|
+
id,
|
|
1182
|
+
code: 'RESOURCE_NOT_FOUND'
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Re-throw other errors (permission denied, network issues, etc.)
|
|
1187
|
+
if (!ok) {
|
|
1188
|
+
throw err;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
return data;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1040
1194
|
/**
|
|
1041
1195
|
* Check if a resource exists by ID
|
|
1042
1196
|
* @returns {Promise<boolean>} True if resource exists, false otherwise
|
|
1043
1197
|
*/
|
|
1044
1198
|
async exists(id) {
|
|
1199
|
+
// Execute beforeExists hooks
|
|
1200
|
+
await this.executeHooks('beforeExists', { id });
|
|
1201
|
+
|
|
1045
1202
|
const key = this.getResourceKey(id);
|
|
1046
1203
|
const [ok, err] = await tryFn(() => this.client.headObject(key));
|
|
1204
|
+
|
|
1205
|
+
// Execute afterExists hooks
|
|
1206
|
+
await this.executeHooks('afterExists', { id, exists: ok });
|
|
1207
|
+
|
|
1047
1208
|
return ok;
|
|
1048
1209
|
}
|
|
1049
1210
|
|
|
@@ -1102,7 +1263,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1102
1263
|
}
|
|
1103
1264
|
const preProcessedData = await this.executeHooks('beforeUpdate', cloneDeep(mergedData));
|
|
1104
1265
|
const completeData = { ...originalData, ...preProcessedData, id };
|
|
1105
|
-
const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
|
|
1266
|
+
const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
|
|
1106
1267
|
if (!isValid) {
|
|
1107
1268
|
throw new InvalidResourceItem({
|
|
1108
1269
|
bucket: this.client.config.bucket,
|
|
@@ -1288,20 +1449,30 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1288
1449
|
throw new Error('fields must be a non-empty object');
|
|
1289
1450
|
}
|
|
1290
1451
|
|
|
1452
|
+
// Execute beforePatch hooks
|
|
1453
|
+
await this.executeHooks('beforePatch', { id, fields, options });
|
|
1454
|
+
|
|
1291
1455
|
const behavior = this.behavior;
|
|
1292
1456
|
|
|
1293
1457
|
// Check if fields contain dot notation (nested fields)
|
|
1294
1458
|
const hasNestedFields = Object.keys(fields).some(key => key.includes('.'));
|
|
1295
1459
|
|
|
1460
|
+
let result;
|
|
1461
|
+
|
|
1296
1462
|
// ✅ Optimization: HEAD + COPY for metadata-only behaviors WITHOUT nested fields
|
|
1297
1463
|
if ((behavior === 'enforce-limits' || behavior === 'truncate-data') && !hasNestedFields) {
|
|
1298
|
-
|
|
1464
|
+
result = await this._patchViaCopyObject(id, fields, options);
|
|
1465
|
+
} else {
|
|
1466
|
+
// ⚠️ Fallback: GET + merge + PUT for:
|
|
1467
|
+
// - Behaviors with body storage
|
|
1468
|
+
// - Nested field updates (need full object merge)
|
|
1469
|
+
result = await this.update(id, fields, options);
|
|
1299
1470
|
}
|
|
1300
1471
|
|
|
1301
|
-
//
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
return
|
|
1472
|
+
// Execute afterPatch hooks
|
|
1473
|
+
const finalResult = await this.executeHooks('afterPatch', result);
|
|
1474
|
+
|
|
1475
|
+
return finalResult;
|
|
1305
1476
|
}
|
|
1306
1477
|
|
|
1307
1478
|
/**
|
|
@@ -1433,6 +1604,9 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1433
1604
|
throw new Error('fullData must be a non-empty object');
|
|
1434
1605
|
}
|
|
1435
1606
|
|
|
1607
|
+
// Execute beforeReplace hooks
|
|
1608
|
+
await this.executeHooks('beforeReplace', { id, fullData, options });
|
|
1609
|
+
|
|
1436
1610
|
const { partition, partitionValues } = options;
|
|
1437
1611
|
|
|
1438
1612
|
// Clone data to avoid mutations
|
|
@@ -1458,7 +1632,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1458
1632
|
errors,
|
|
1459
1633
|
isValid,
|
|
1460
1634
|
data: validated,
|
|
1461
|
-
} = await this.validate(completeData);
|
|
1635
|
+
} = await this.validate(completeData, { includeId: true });
|
|
1462
1636
|
|
|
1463
1637
|
if (!isValid) {
|
|
1464
1638
|
const errorMsg = (errors && errors.length && errors[0].message) ? errors[0].message : 'Replace failed';
|
|
@@ -1556,7 +1730,10 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1556
1730
|
}
|
|
1557
1731
|
}
|
|
1558
1732
|
|
|
1559
|
-
|
|
1733
|
+
// Execute afterReplace hooks
|
|
1734
|
+
const finalResult = await this.executeHooks('afterReplace', replacedObject);
|
|
1735
|
+
|
|
1736
|
+
return finalResult;
|
|
1560
1737
|
}
|
|
1561
1738
|
|
|
1562
1739
|
/**
|
|
@@ -1628,7 +1805,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1628
1805
|
const completeData = { ...originalData, ...preProcessedData, id };
|
|
1629
1806
|
|
|
1630
1807
|
// Validate
|
|
1631
|
-
const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
|
|
1808
|
+
const { isValid, errors, data } = await this.validate(cloneDeep(completeData), { includeId: true });
|
|
1632
1809
|
if (!isValid) {
|
|
1633
1810
|
return {
|
|
1634
1811
|
success: false,
|
|
@@ -1893,6 +2070,9 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1893
2070
|
* });
|
|
1894
2071
|
*/
|
|
1895
2072
|
async count({ partition = null, partitionValues = {} } = {}) {
|
|
2073
|
+
// Execute beforeCount hooks
|
|
2074
|
+
await this.executeHooks('beforeCount', { partition, partitionValues });
|
|
2075
|
+
|
|
1896
2076
|
let prefix;
|
|
1897
2077
|
|
|
1898
2078
|
if (partition && Object.keys(partitionValues).length > 0) {
|
|
@@ -1924,6 +2104,10 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1924
2104
|
}
|
|
1925
2105
|
|
|
1926
2106
|
const count = await this.client.count({ prefix });
|
|
2107
|
+
|
|
2108
|
+
// Execute afterCount hooks
|
|
2109
|
+
await this.executeHooks('afterCount', { count, partition, partitionValues });
|
|
2110
|
+
|
|
1927
2111
|
this.emit("count", count);
|
|
1928
2112
|
return count;
|
|
1929
2113
|
}
|
|
@@ -1965,6 +2149,9 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1965
2149
|
* const results = await resource.deleteMany(deletedIds);
|
|
1966
2150
|
*/
|
|
1967
2151
|
async deleteMany(ids) {
|
|
2152
|
+
// Execute beforeDeleteMany hooks
|
|
2153
|
+
await this.executeHooks('beforeDeleteMany', { ids });
|
|
2154
|
+
|
|
1968
2155
|
const packages = chunk(
|
|
1969
2156
|
ids.map((id) => this.getResourceKey(id)),
|
|
1970
2157
|
1000
|
|
@@ -1996,6 +2183,9 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1996
2183
|
return response;
|
|
1997
2184
|
});
|
|
1998
2185
|
|
|
2186
|
+
// Execute afterDeleteMany hooks
|
|
2187
|
+
await this.executeHooks('afterDeleteMany', { ids, results });
|
|
2188
|
+
|
|
1999
2189
|
this.emit("deleteMany", ids.length);
|
|
2000
2190
|
return results;
|
|
2001
2191
|
}
|
|
@@ -2137,6 +2327,9 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2137
2327
|
* });
|
|
2138
2328
|
*/
|
|
2139
2329
|
async list({ partition = null, partitionValues = {}, limit, offset = 0 } = {}) {
|
|
2330
|
+
// Execute beforeList hooks
|
|
2331
|
+
await this.executeHooks('beforeList', { partition, partitionValues, limit, offset });
|
|
2332
|
+
|
|
2140
2333
|
const [ok, err, result] = await tryFn(async () => {
|
|
2141
2334
|
if (!partition) {
|
|
2142
2335
|
return await this.listMain({ limit, offset });
|
|
@@ -2146,7 +2339,10 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2146
2339
|
if (!ok) {
|
|
2147
2340
|
return this.handleListError(err, { partition, partitionValues });
|
|
2148
2341
|
}
|
|
2149
|
-
|
|
2342
|
+
|
|
2343
|
+
// Execute afterList hooks
|
|
2344
|
+
const finalResult = await this.executeHooks('afterList', result);
|
|
2345
|
+
return finalResult;
|
|
2150
2346
|
}
|
|
2151
2347
|
|
|
2152
2348
|
async listMain({ limit, offset = 0 }) {
|
|
@@ -2314,6 +2510,9 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2314
2510
|
* const users = await resource.getMany(['user-1', 'user-2', 'user-3']);
|
|
2315
2511
|
*/
|
|
2316
2512
|
async getMany(ids) {
|
|
2513
|
+
// Execute beforeGetMany hooks
|
|
2514
|
+
await this.executeHooks('beforeGetMany', { ids });
|
|
2515
|
+
|
|
2317
2516
|
const { results, errors } = await PromisePool.for(ids)
|
|
2318
2517
|
.withConcurrency(this.client.parallelism)
|
|
2319
2518
|
.handleError(async (error, id) => {
|
|
@@ -2338,8 +2537,11 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2338
2537
|
throw err;
|
|
2339
2538
|
});
|
|
2340
2539
|
|
|
2540
|
+
// Execute afterGetMany hooks
|
|
2541
|
+
const finalResults = await this.executeHooks('afterGetMany', results);
|
|
2542
|
+
|
|
2341
2543
|
this.emit("getMany", ids.length);
|
|
2342
|
-
return
|
|
2544
|
+
return finalResults;
|
|
2343
2545
|
}
|
|
2344
2546
|
|
|
2345
2547
|
/**
|
|
@@ -2604,25 +2806,6 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2604
2806
|
* @returns {Object} Schema object for the version
|
|
2605
2807
|
*/
|
|
2606
2808
|
async getSchemaForVersion(version) {
|
|
2607
|
-
// If version is the same as current, return current schema
|
|
2608
|
-
if (version === this.version) {
|
|
2609
|
-
return this.schema;
|
|
2610
|
-
}
|
|
2611
|
-
// For different versions, try to create a compatible schema
|
|
2612
|
-
// This is especially important for v0 objects that might have different encryption
|
|
2613
|
-
const [ok, err, compatibleSchema] = await tryFn(() => Promise.resolve(new Schema({
|
|
2614
|
-
name: this.name,
|
|
2615
|
-
attributes: this.attributes,
|
|
2616
|
-
passphrase: this.passphrase,
|
|
2617
|
-
version: version,
|
|
2618
|
-
options: {
|
|
2619
|
-
...this.config,
|
|
2620
|
-
autoDecrypt: true,
|
|
2621
|
-
autoEncrypt: true
|
|
2622
|
-
}
|
|
2623
|
-
})));
|
|
2624
|
-
if (ok) return compatibleSchema;
|
|
2625
|
-
// console.warn(`Failed to create compatible schema for version ${version}, using current schema:`, err.message);
|
|
2626
2809
|
return this.schema;
|
|
2627
2810
|
}
|
|
2628
2811
|
|
|
@@ -2732,6 +2915,9 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2732
2915
|
* );
|
|
2733
2916
|
*/
|
|
2734
2917
|
async query(filter = {}, { limit = 100, offset = 0, partition = null, partitionValues = {} } = {}) {
|
|
2918
|
+
// Execute beforeQuery hooks
|
|
2919
|
+
await this.executeHooks('beforeQuery', { filter, limit, offset, partition, partitionValues });
|
|
2920
|
+
|
|
2735
2921
|
if (Object.keys(filter).length === 0) {
|
|
2736
2922
|
// No filter, just return paginated results
|
|
2737
2923
|
return await this.list({ partition, partitionValues, limit, offset });
|
|
@@ -2772,7 +2958,10 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2772
2958
|
}
|
|
2773
2959
|
|
|
2774
2960
|
// Return only up to the requested limit
|
|
2775
|
-
|
|
2961
|
+
const finalResults = results.slice(0, limit);
|
|
2962
|
+
|
|
2963
|
+
// Execute afterQuery hooks
|
|
2964
|
+
return await this.executeHooks('afterQuery', finalResults);
|
|
2776
2965
|
}
|
|
2777
2966
|
|
|
2778
2967
|
/**
|
|
@@ -2891,7 +3080,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
2891
3080
|
}
|
|
2892
3081
|
|
|
2893
3082
|
/**
|
|
2894
|
-
* Update partition objects to keep them in sync
|
|
3083
|
+
* Update partition objects to keep them in sync
|
|
2895
3084
|
* @param {Object} data - Updated object data
|
|
2896
3085
|
*/
|
|
2897
3086
|
async updatePartitionReferences(data) {
|
|
@@ -3347,7 +3536,20 @@ function validateResourceConfig(config) {
|
|
|
3347
3536
|
if (typeof config.hooks !== 'object' || Array.isArray(config.hooks)) {
|
|
3348
3537
|
errors.push("Resource 'hooks' must be an object");
|
|
3349
3538
|
} else {
|
|
3350
|
-
const validHookEvents = [
|
|
3539
|
+
const validHookEvents = [
|
|
3540
|
+
'beforeInsert', 'afterInsert',
|
|
3541
|
+
'beforeUpdate', 'afterUpdate',
|
|
3542
|
+
'beforeDelete', 'afterDelete',
|
|
3543
|
+
'beforeGet', 'afterGet',
|
|
3544
|
+
'beforeList', 'afterList',
|
|
3545
|
+
'beforeQuery', 'afterQuery',
|
|
3546
|
+
'beforeExists', 'afterExists',
|
|
3547
|
+
'beforeCount', 'afterCount',
|
|
3548
|
+
'beforePatch', 'afterPatch',
|
|
3549
|
+
'beforeReplace', 'afterReplace',
|
|
3550
|
+
'beforeGetMany', 'afterGetMany',
|
|
3551
|
+
'beforeDeleteMany', 'afterDeleteMany'
|
|
3552
|
+
];
|
|
3351
3553
|
for (const [event, hooksArr] of Object.entries(config.hooks)) {
|
|
3352
3554
|
if (!validHookEvents.includes(event)) {
|
|
3353
3555
|
errors.push(`Invalid hook event '${event}'. Valid events: ${validHookEvents.join(', ')}`);
|
package/src/schema.class.js
CHANGED
|
@@ -307,8 +307,6 @@ export const SchemaActions = {
|
|
|
307
307
|
return decodeFixedPointBatch(str, precision);
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
// Fallback: Legacy format with individual prefixes (^val,^val,^val)
|
|
311
|
-
// This maintains backwards compatibility with data encoded before batch optimization
|
|
312
310
|
const items = [];
|
|
313
311
|
let current = '';
|
|
314
312
|
let i = 0;
|