s3db.js 9.3.0 → 10.0.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 +72 -13
- package/dist/s3db.cjs.js +2342 -540
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +2341 -541
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/client.class.js +8 -7
- package/src/concerns/high-performance-inserter.js +285 -0
- package/src/concerns/partition-queue.js +171 -0
- package/src/errors.js +10 -2
- package/src/partition-drivers/base-partition-driver.js +96 -0
- package/src/partition-drivers/index.js +60 -0
- package/src/partition-drivers/memory-partition-driver.js +274 -0
- package/src/partition-drivers/sqs-partition-driver.js +332 -0
- package/src/partition-drivers/sync-partition-driver.js +38 -0
- package/src/plugins/audit.plugin.js +4 -4
- package/src/plugins/backup.plugin.js +380 -105
- package/src/plugins/backup.plugin.js.backup +1 -1
- package/src/plugins/cache.plugin.js +203 -150
- package/src/plugins/eventual-consistency.plugin.js +1012 -0
- package/src/plugins/fulltext.plugin.js +6 -6
- package/src/plugins/index.js +2 -0
- package/src/plugins/metrics.plugin.js +13 -13
- package/src/plugins/replicator.plugin.js +108 -70
- package/src/plugins/replicators/s3db-replicator.class.js +7 -3
- package/src/plugins/replicators/sqs-replicator.class.js +11 -3
- package/src/plugins/s3-queue.plugin.js +776 -0
- package/src/plugins/scheduler.plugin.js +226 -164
- package/src/plugins/state-machine.plugin.js +109 -81
- package/src/resource.class.js +205 -0
- package/PLUGINS.md +0 -5036
|
@@ -79,7 +79,7 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
79
79
|
* },
|
|
80
80
|
*
|
|
81
81
|
* persistTransitions: true,
|
|
82
|
-
* transitionLogResource: '
|
|
82
|
+
* transitionLogResource: 'plg_state_transitions'
|
|
83
83
|
* });
|
|
84
84
|
*
|
|
85
85
|
* === Usage ===
|
|
@@ -91,7 +91,7 @@ import tryFn from "../concerns/try-fn.js";
|
|
|
91
91
|
* const state = await stateMachine.getState('order_processing', orderId);
|
|
92
92
|
*
|
|
93
93
|
* // Get valid events for current state
|
|
94
|
-
* const validEvents = stateMachine.getValidEvents('order_processing', 'pending');
|
|
94
|
+
* const validEvents = await stateMachine.getValidEvents('order_processing', 'pending');
|
|
95
95
|
*
|
|
96
96
|
* // Get transition history
|
|
97
97
|
* const history = await stateMachine.getTransitionHistory('order_processing', orderId);
|
|
@@ -105,15 +105,15 @@ export class StateMachinePlugin extends Plugin {
|
|
|
105
105
|
actions: options.actions || {},
|
|
106
106
|
guards: options.guards || {},
|
|
107
107
|
persistTransitions: options.persistTransitions !== false,
|
|
108
|
-
transitionLogResource: options.transitionLogResource || '
|
|
109
|
-
stateResource: options.stateResource || '
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
transitionLogResource: options.transitionLogResource || 'plg_state_transitions',
|
|
109
|
+
stateResource: options.stateResource || 'plg_entity_states',
|
|
110
|
+
retryAttempts: options.retryAttempts || 3,
|
|
111
|
+
retryDelay: options.retryDelay || 100,
|
|
112
|
+
verbose: options.verbose || false
|
|
112
113
|
};
|
|
113
|
-
|
|
114
|
+
|
|
114
115
|
this.database = null;
|
|
115
116
|
this.machines = new Map();
|
|
116
|
-
this.stateStorage = new Map(); // In-memory cache for states
|
|
117
117
|
|
|
118
118
|
this._validateConfiguration();
|
|
119
119
|
}
|
|
@@ -292,49 +292,68 @@ export class StateMachinePlugin extends Plugin {
|
|
|
292
292
|
// Persist transition log
|
|
293
293
|
if (this.config.persistTransitions) {
|
|
294
294
|
const transitionId = `${machineId}_${entityId}_${timestamp}`;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
295
|
+
|
|
296
|
+
// Retry transition logging (critical for audit trail)
|
|
297
|
+
let logOk = false;
|
|
298
|
+
let lastLogErr;
|
|
299
|
+
|
|
300
|
+
for (let attempt = 0; attempt < this.config.retryAttempts; attempt++) {
|
|
301
|
+
const [ok, err] = await tryFn(() =>
|
|
302
|
+
this.database.resource(this.config.transitionLogResource).insert({
|
|
303
|
+
id: transitionId,
|
|
304
|
+
machineId,
|
|
305
|
+
entityId,
|
|
306
|
+
fromState,
|
|
307
|
+
toState,
|
|
308
|
+
event,
|
|
309
|
+
context,
|
|
310
|
+
timestamp,
|
|
311
|
+
createdAt: now.slice(0, 10) // YYYY-MM-DD for partitioning
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (ok) {
|
|
316
|
+
logOk = true;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
lastLogErr = err;
|
|
321
|
+
|
|
322
|
+
if (attempt < this.config.retryAttempts - 1) {
|
|
323
|
+
const delay = this.config.retryDelay * Math.pow(2, attempt);
|
|
324
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
310
328
|
if (!logOk && this.config.verbose) {
|
|
311
|
-
console.warn(`[StateMachinePlugin] Failed to log transition:`,
|
|
329
|
+
console.warn(`[StateMachinePlugin] Failed to log transition after ${this.config.retryAttempts} attempts:`, lastLogErr.message);
|
|
312
330
|
}
|
|
313
|
-
|
|
314
|
-
// Update current state
|
|
331
|
+
|
|
332
|
+
// Update current state with upsert pattern
|
|
315
333
|
const stateId = `${machineId}_${entityId}`;
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
334
|
+
const stateData = {
|
|
335
|
+
machineId,
|
|
336
|
+
entityId,
|
|
337
|
+
currentState: toState,
|
|
338
|
+
context,
|
|
339
|
+
lastTransition: transitionId,
|
|
340
|
+
updatedAt: now
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Try update first (most common case), fallback to insert if doesn't exist
|
|
344
|
+
const [updateOk] = await tryFn(() =>
|
|
345
|
+
this.database.resource(this.config.stateResource).update(stateId, stateData)
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
if (!updateOk) {
|
|
349
|
+
// Record doesn't exist, insert it
|
|
350
|
+
const [insertOk, insertErr] = await tryFn(() =>
|
|
351
|
+
this.database.resource(this.config.stateResource).insert({ id: stateId, ...stateData })
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (!insertOk && this.config.verbose) {
|
|
355
|
+
console.warn(`[StateMachinePlugin] Failed to upsert state:`, insertErr.message);
|
|
333
356
|
}
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
if (!stateOk && this.config.verbose) {
|
|
337
|
-
console.warn(`[StateMachinePlugin] Failed to update state:`, stateErr.message);
|
|
338
357
|
}
|
|
339
358
|
}
|
|
340
359
|
}
|
|
@@ -374,22 +393,23 @@ export class StateMachinePlugin extends Plugin {
|
|
|
374
393
|
|
|
375
394
|
/**
|
|
376
395
|
* Get valid events for current state
|
|
396
|
+
* Can accept either a state name (sync) or entityId (async to fetch latest state)
|
|
377
397
|
*/
|
|
378
|
-
getValidEvents(machineId, stateOrEntityId) {
|
|
398
|
+
async getValidEvents(machineId, stateOrEntityId) {
|
|
379
399
|
const machine = this.machines.get(machineId);
|
|
380
400
|
if (!machine) {
|
|
381
401
|
throw new Error(`State machine '${machineId}' not found`);
|
|
382
402
|
}
|
|
383
|
-
|
|
403
|
+
|
|
384
404
|
let state;
|
|
385
405
|
if (machine.config.states[stateOrEntityId]) {
|
|
386
|
-
// stateOrEntityId is a state name
|
|
406
|
+
// stateOrEntityId is a state name - direct lookup
|
|
387
407
|
state = stateOrEntityId;
|
|
388
408
|
} else {
|
|
389
|
-
// stateOrEntityId is an entityId
|
|
390
|
-
state =
|
|
409
|
+
// stateOrEntityId is an entityId - fetch latest state from storage
|
|
410
|
+
state = await this.getState(machineId, stateOrEntityId);
|
|
391
411
|
}
|
|
392
|
-
|
|
412
|
+
|
|
393
413
|
const stateConfig = machine.config.states[state];
|
|
394
414
|
return stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [];
|
|
395
415
|
}
|
|
@@ -401,29 +421,30 @@ export class StateMachinePlugin extends Plugin {
|
|
|
401
421
|
if (!this.config.persistTransitions) {
|
|
402
422
|
return [];
|
|
403
423
|
}
|
|
404
|
-
|
|
424
|
+
|
|
405
425
|
const { limit = 50, offset = 0 } = options;
|
|
406
|
-
|
|
407
|
-
const [ok, err, transitions] = await tryFn(() =>
|
|
408
|
-
this.database.resource(this.config.transitionLogResource).
|
|
409
|
-
|
|
410
|
-
|
|
426
|
+
|
|
427
|
+
const [ok, err, transitions] = await tryFn(() =>
|
|
428
|
+
this.database.resource(this.config.transitionLogResource).query({
|
|
429
|
+
machineId,
|
|
430
|
+
entityId
|
|
431
|
+
}, {
|
|
411
432
|
limit,
|
|
412
433
|
offset
|
|
413
434
|
})
|
|
414
435
|
);
|
|
415
|
-
|
|
436
|
+
|
|
416
437
|
if (!ok) {
|
|
417
438
|
if (this.config.verbose) {
|
|
418
439
|
console.warn(`[StateMachinePlugin] Failed to get transition history:`, err.message);
|
|
419
440
|
}
|
|
420
441
|
return [];
|
|
421
442
|
}
|
|
422
|
-
|
|
423
|
-
// Sort by timestamp descending
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
return
|
|
443
|
+
|
|
444
|
+
// Sort by timestamp descending (newest first)
|
|
445
|
+
const sorted = (transitions || []).sort((a, b) => b.timestamp - a.timestamp);
|
|
446
|
+
|
|
447
|
+
return sorted.map(t => ({
|
|
427
448
|
from: t.fromState,
|
|
428
449
|
to: t.toState,
|
|
429
450
|
event: t.event,
|
|
@@ -440,33 +461,41 @@ export class StateMachinePlugin extends Plugin {
|
|
|
440
461
|
if (!machine) {
|
|
441
462
|
throw new Error(`State machine '${machineId}' not found`);
|
|
442
463
|
}
|
|
443
|
-
|
|
464
|
+
|
|
444
465
|
const initialState = machine.config.initialState;
|
|
445
466
|
machine.currentStates.set(entityId, initialState);
|
|
446
|
-
|
|
467
|
+
|
|
447
468
|
if (this.config.persistTransitions) {
|
|
448
469
|
const now = new Date().toISOString();
|
|
449
470
|
const stateId = `${machineId}_${entityId}`;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
471
|
+
|
|
472
|
+
// Try to insert, ignore if already exists (idempotent)
|
|
473
|
+
const [ok, err] = await tryFn(() =>
|
|
474
|
+
this.database.resource(this.config.stateResource).insert({
|
|
475
|
+
id: stateId,
|
|
476
|
+
machineId,
|
|
477
|
+
entityId,
|
|
478
|
+
currentState: initialState,
|
|
479
|
+
context,
|
|
480
|
+
lastTransition: null,
|
|
481
|
+
updatedAt: now
|
|
482
|
+
})
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
// Only throw if error is NOT "already exists"
|
|
486
|
+
if (!ok && err && !err.message?.includes('already exists')) {
|
|
487
|
+
throw new Error(`Failed to initialize entity state: ${err.message}`);
|
|
488
|
+
}
|
|
460
489
|
}
|
|
461
|
-
|
|
490
|
+
|
|
462
491
|
// Execute entry action for initial state
|
|
463
492
|
const initialStateConfig = machine.config.states[initialState];
|
|
464
493
|
if (initialStateConfig && initialStateConfig.entry) {
|
|
465
494
|
await this._executeAction(initialStateConfig.entry, context, 'INIT', machineId, entityId);
|
|
466
495
|
}
|
|
467
|
-
|
|
496
|
+
|
|
468
497
|
this.emit('entity_initialized', { machineId, entityId, initialState });
|
|
469
|
-
|
|
498
|
+
|
|
470
499
|
return initialState;
|
|
471
500
|
}
|
|
472
501
|
|
|
@@ -531,7 +560,6 @@ export class StateMachinePlugin extends Plugin {
|
|
|
531
560
|
|
|
532
561
|
async stop() {
|
|
533
562
|
this.machines.clear();
|
|
534
|
-
this.stateStorage.clear();
|
|
535
563
|
}
|
|
536
564
|
|
|
537
565
|
async cleanup() {
|
package/src/resource.class.js
CHANGED
|
@@ -926,6 +926,7 @@ export class Resource extends AsyncEventEmitter {
|
|
|
926
926
|
data._lastModified = request.LastModified;
|
|
927
927
|
data._hasContent = request.ContentLength > 0;
|
|
928
928
|
data._mimeType = request.ContentType || null;
|
|
929
|
+
data._etag = request.ETag;
|
|
929
930
|
data._v = objectVersion;
|
|
930
931
|
|
|
931
932
|
// Add version info to returned data
|
|
@@ -1159,6 +1160,210 @@ export class Resource extends AsyncEventEmitter {
|
|
|
1159
1160
|
}
|
|
1160
1161
|
}
|
|
1161
1162
|
|
|
1163
|
+
/**
|
|
1164
|
+
* Update with conditional check (If-Match ETag)
|
|
1165
|
+
* @param {string} id - Resource ID
|
|
1166
|
+
* @param {Object} attributes - Attributes to update
|
|
1167
|
+
* @param {Object} options - Options including ifMatch (ETag)
|
|
1168
|
+
* @returns {Promise<Object>} { success: boolean, data?: Object, etag?: string, error?: string }
|
|
1169
|
+
* @example
|
|
1170
|
+
* const msg = await resource.get('msg-123');
|
|
1171
|
+
* const result = await resource.updateConditional('msg-123', { status: 'processing' }, { ifMatch: msg._etag });
|
|
1172
|
+
* if (!result.success) {
|
|
1173
|
+
* console.log('Update failed - object was modified by another process');
|
|
1174
|
+
* }
|
|
1175
|
+
*/
|
|
1176
|
+
async updateConditional(id, attributes, options = {}) {
|
|
1177
|
+
if (isEmpty(id)) {
|
|
1178
|
+
throw new Error('id cannot be empty');
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const { ifMatch } = options;
|
|
1182
|
+
if (!ifMatch) {
|
|
1183
|
+
throw new Error('updateConditional requires ifMatch option with ETag value');
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Check if resource exists
|
|
1187
|
+
const exists = await this.exists(id);
|
|
1188
|
+
if (!exists) {
|
|
1189
|
+
return {
|
|
1190
|
+
success: false,
|
|
1191
|
+
error: `Resource with id '${id}' does not exist`
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
// Get original data
|
|
1196
|
+
const originalData = await this.get(id);
|
|
1197
|
+
const attributesClone = cloneDeep(attributes);
|
|
1198
|
+
let mergedData = cloneDeep(originalData);
|
|
1199
|
+
|
|
1200
|
+
// Merge attributes (same logic as update)
|
|
1201
|
+
for (const [key, value] of Object.entries(attributesClone)) {
|
|
1202
|
+
if (key.includes('.')) {
|
|
1203
|
+
let ref = mergedData;
|
|
1204
|
+
const parts = key.split('.');
|
|
1205
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
1206
|
+
if (typeof ref[parts[i]] !== 'object' || ref[parts[i]] === null) {
|
|
1207
|
+
ref[parts[i]] = {};
|
|
1208
|
+
}
|
|
1209
|
+
ref = ref[parts[i]];
|
|
1210
|
+
}
|
|
1211
|
+
ref[parts[parts.length - 1]] = cloneDeep(value);
|
|
1212
|
+
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1213
|
+
mergedData[key] = merge({}, mergedData[key], value);
|
|
1214
|
+
} else {
|
|
1215
|
+
mergedData[key] = cloneDeep(value);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Update timestamps if enabled
|
|
1220
|
+
if (this.config.timestamps) {
|
|
1221
|
+
const now = new Date().toISOString();
|
|
1222
|
+
mergedData.updatedAt = now;
|
|
1223
|
+
if (!mergedData.metadata) mergedData.metadata = {};
|
|
1224
|
+
mergedData.metadata.updatedAt = now;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Execute beforeUpdate hooks
|
|
1228
|
+
const preProcessedData = await this.executeHooks('beforeUpdate', cloneDeep(mergedData));
|
|
1229
|
+
const completeData = { ...originalData, ...preProcessedData, id };
|
|
1230
|
+
|
|
1231
|
+
// Validate
|
|
1232
|
+
const { isValid, errors, data } = await this.validate(cloneDeep(completeData));
|
|
1233
|
+
if (!isValid) {
|
|
1234
|
+
return {
|
|
1235
|
+
success: false,
|
|
1236
|
+
error: 'Validation failed: ' + ((errors && errors.length) ? JSON.stringify(errors) : 'unknown'),
|
|
1237
|
+
validationErrors: errors
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// Prepare data for storage
|
|
1242
|
+
const { id: validatedId, ...validatedAttributes } = data;
|
|
1243
|
+
const mappedData = await this.schema.mapper(validatedAttributes);
|
|
1244
|
+
mappedData._v = String(this.version);
|
|
1245
|
+
|
|
1246
|
+
const behaviorImpl = getBehavior(this.behavior);
|
|
1247
|
+
const { mappedData: processedMetadata, body } = await behaviorImpl.handleUpdate({
|
|
1248
|
+
resource: this,
|
|
1249
|
+
id,
|
|
1250
|
+
data: validatedAttributes,
|
|
1251
|
+
mappedData,
|
|
1252
|
+
originalData: { ...attributesClone, id }
|
|
1253
|
+
});
|
|
1254
|
+
|
|
1255
|
+
const key = this.getResourceKey(id);
|
|
1256
|
+
let existingContentType = undefined;
|
|
1257
|
+
let finalBody = body;
|
|
1258
|
+
|
|
1259
|
+
if (body === "" && this.behavior !== 'body-overflow') {
|
|
1260
|
+
const [ok, err, existingObject] = await tryFn(() => this.client.getObject(key));
|
|
1261
|
+
if (ok && existingObject.ContentLength > 0) {
|
|
1262
|
+
const existingBodyBuffer = Buffer.from(await existingObject.Body.transformToByteArray());
|
|
1263
|
+
const existingBodyString = existingBodyBuffer.toString();
|
|
1264
|
+
const [okParse, errParse] = await tryFn(() => Promise.resolve(JSON.parse(existingBodyString)));
|
|
1265
|
+
if (!okParse) {
|
|
1266
|
+
finalBody = existingBodyBuffer;
|
|
1267
|
+
existingContentType = existingObject.ContentType;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
let finalContentType = existingContentType;
|
|
1273
|
+
if (finalBody && finalBody !== "" && !finalContentType) {
|
|
1274
|
+
const [okParse, errParse] = await tryFn(() => Promise.resolve(JSON.parse(finalBody)));
|
|
1275
|
+
if (okParse) finalContentType = 'application/json';
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Attempt conditional write with IfMatch
|
|
1279
|
+
const [ok, err, response] = await tryFn(() => this.client.putObject({
|
|
1280
|
+
key,
|
|
1281
|
+
body: finalBody,
|
|
1282
|
+
contentType: finalContentType,
|
|
1283
|
+
metadata: processedMetadata,
|
|
1284
|
+
ifMatch // ← Conditional write with ETag
|
|
1285
|
+
}));
|
|
1286
|
+
|
|
1287
|
+
if (!ok) {
|
|
1288
|
+
// Check if it's a PreconditionFailed error (412)
|
|
1289
|
+
if (err.name === 'PreconditionFailed' || err.$metadata?.httpStatusCode === 412) {
|
|
1290
|
+
return {
|
|
1291
|
+
success: false,
|
|
1292
|
+
error: 'ETag mismatch - object was modified by another process'
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Other errors
|
|
1297
|
+
return {
|
|
1298
|
+
success: false,
|
|
1299
|
+
error: err.message || 'Update failed'
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// Success - compose updated data
|
|
1304
|
+
const updatedData = await this.composeFullObjectFromWrite({
|
|
1305
|
+
id,
|
|
1306
|
+
metadata: processedMetadata,
|
|
1307
|
+
body: finalBody,
|
|
1308
|
+
behavior: this.behavior
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1311
|
+
// Handle partition updates (async if configured)
|
|
1312
|
+
const oldData = { ...originalData, id };
|
|
1313
|
+
const newData = { ...validatedAttributes, id };
|
|
1314
|
+
|
|
1315
|
+
if (this.config.asyncPartitions && this.config.partitions && Object.keys(this.config.partitions).length > 0) {
|
|
1316
|
+
// Async mode
|
|
1317
|
+
setImmediate(() => {
|
|
1318
|
+
this.handlePartitionReferenceUpdates(oldData, newData).catch(err => {
|
|
1319
|
+
this.emit('partitionIndexError', {
|
|
1320
|
+
operation: 'updateConditional',
|
|
1321
|
+
id,
|
|
1322
|
+
error: err,
|
|
1323
|
+
message: err.message
|
|
1324
|
+
});
|
|
1325
|
+
});
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
// Execute non-partition hooks
|
|
1329
|
+
const nonPartitionHooks = this.hooks.afterUpdate.filter(hook =>
|
|
1330
|
+
!hook.toString().includes('handlePartitionReferenceUpdates')
|
|
1331
|
+
);
|
|
1332
|
+
let finalResult = updatedData;
|
|
1333
|
+
for (const hook of nonPartitionHooks) {
|
|
1334
|
+
finalResult = await hook(finalResult);
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
this.emit('update', {
|
|
1338
|
+
...updatedData,
|
|
1339
|
+
$before: { ...originalData },
|
|
1340
|
+
$after: { ...finalResult }
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
return {
|
|
1344
|
+
success: true,
|
|
1345
|
+
data: finalResult,
|
|
1346
|
+
etag: response.ETag
|
|
1347
|
+
};
|
|
1348
|
+
} else {
|
|
1349
|
+
// Sync mode
|
|
1350
|
+
await this.handlePartitionReferenceUpdates(oldData, newData);
|
|
1351
|
+
const finalResult = await this.executeHooks('afterUpdate', updatedData);
|
|
1352
|
+
|
|
1353
|
+
this.emit('update', {
|
|
1354
|
+
...updatedData,
|
|
1355
|
+
$before: { ...originalData },
|
|
1356
|
+
$after: { ...finalResult }
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
return {
|
|
1360
|
+
success: true,
|
|
1361
|
+
data: finalResult,
|
|
1362
|
+
etag: response.ETag
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1162
1367
|
/**
|
|
1163
1368
|
* Delete a resource object by ID
|
|
1164
1369
|
* @param {string} id - Resource ID
|