s3db.js 13.2.2 → 13.3.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.
@@ -1322,7 +1322,7 @@ export class Resource extends AsyncEventEmitter {
1322
1322
  // Execute afterGet hooks
1323
1323
  data = await this.executeHooks('afterGet', data);
1324
1324
 
1325
- this._emitStandardized("get", "fetched", data, data.id);
1325
+ this._emitStandardized("fetched", data, data.id);
1326
1326
  const value = data;
1327
1327
  return value;
1328
1328
  }
@@ -2201,31 +2201,6 @@ export class Resource extends AsyncEventEmitter {
2201
2201
  const key = this.getResourceKey(id);
2202
2202
  const [ok2, err2, response] = await tryFn(() => this.client.deleteObject(key));
2203
2203
 
2204
- // Always emit delete event for audit purposes, even if delete fails
2205
- this._emitStandardized("delete", "deleted", {
2206
- ...objectData,
2207
- $before: { ...objectData },
2208
- $after: null
2209
- }, id);
2210
-
2211
- // If we had an error getting the object, throw it now (after emitting the event)
2212
- if (deleteError) {
2213
- throw mapAwsError(deleteError, {
2214
- bucket: this.client.config.bucket,
2215
- key,
2216
- resourceName: this.name,
2217
- operation: 'delete',
2218
- id
2219
- });
2220
- }
2221
-
2222
- if (!ok2) throw mapAwsError(err2, {
2223
- key,
2224
- resourceName: this.name,
2225
- operation: 'delete',
2226
- id
2227
- });
2228
-
2229
2204
  // Handle partition cleanup based on strictPartitions and asyncPartitions config
2230
2205
  if (this.config.partitions && Object.keys(this.config.partitions).length > 0 && objectData) {
2231
2206
  if (this.config.strictPartitions) {
@@ -2257,19 +2232,44 @@ export class Resource extends AsyncEventEmitter {
2257
2232
  }
2258
2233
 
2259
2234
  // Execute other afterDelete hooks synchronously (excluding partition hook)
2260
- const nonPartitionHooks = this.hooks.afterDelete.filter(hook =>
2235
+ const nonPartitionHooks = this.hooks.afterDelete.filter(hook =>
2261
2236
  !hook.toString().includes('deletePartitionReferences')
2262
2237
  );
2263
2238
  let afterDeleteData = objectData;
2264
2239
  for (const hook of nonPartitionHooks) {
2265
2240
  afterDeleteData = await hook(afterDeleteData);
2266
2241
  }
2267
- return response;
2268
2242
  } else {
2269
2243
  // Sync mode: execute all hooks including partition deletion
2270
2244
  const afterDeleteData = await this.executeHooks('afterDelete', objectData);
2271
- return response;
2272
2245
  }
2246
+
2247
+ // Always emit delete event after hooks execute, for audit purposes (even if delete fails)
2248
+ this._emitStandardized("deleted", {
2249
+ ...objectData,
2250
+ $before: { ...objectData },
2251
+ $after: null
2252
+ }, id);
2253
+
2254
+ // If we had an error getting the object, throw it now (after emitting event and hooks)
2255
+ if (deleteError) {
2256
+ throw mapAwsError(deleteError, {
2257
+ bucket: this.client.config.bucket,
2258
+ key,
2259
+ resourceName: this.name,
2260
+ operation: 'delete',
2261
+ id
2262
+ });
2263
+ }
2264
+
2265
+ if (!ok2) throw mapAwsError(err2, {
2266
+ key,
2267
+ resourceName: this.name,
2268
+ operation: 'delete',
2269
+ id
2270
+ });
2271
+
2272
+ return response;
2273
2273
  }
2274
2274
 
2275
2275
  /**
@@ -2357,7 +2357,7 @@ export class Resource extends AsyncEventEmitter {
2357
2357
  // Execute afterCount hooks
2358
2358
  await this.executeHooks('afterCount', { count, partition, partitionValues });
2359
2359
 
2360
- this._emitStandardized("count", "counted", count);
2360
+ this._emitStandardized("count", count);
2361
2361
  return count;
2362
2362
  }
2363
2363
 
@@ -2385,7 +2385,7 @@ export class Resource extends AsyncEventEmitter {
2385
2385
  return result;
2386
2386
  });
2387
2387
 
2388
- this._emitStandardized("insertMany", "inserted-many", objects.length);
2388
+ this._emitStandardized("inserted-many", objects.length);
2389
2389
  return results;
2390
2390
  }
2391
2391
 
@@ -2435,7 +2435,7 @@ export class Resource extends AsyncEventEmitter {
2435
2435
  // Execute afterDeleteMany hooks
2436
2436
  await this.executeHooks('afterDeleteMany', { ids, results });
2437
2437
 
2438
- this._emitStandardized("deleteMany", "deleted-many", ids.length);
2438
+ this._emitStandardized("deleted-many", ids.length);
2439
2439
  return results;
2440
2440
  }
2441
2441
 
@@ -2449,7 +2449,7 @@ export class Resource extends AsyncEventEmitter {
2449
2449
  const prefix = `resource=${this.name}/data`;
2450
2450
  const deletedCount = await this.client.deleteAll({ prefix });
2451
2451
 
2452
- this._emitStandardized("deleteAll", "deleted-all", {
2452
+ this._emitStandardized("deleted-all", {
2453
2453
  version: this.version,
2454
2454
  prefix,
2455
2455
  deletedCount
@@ -2472,7 +2472,7 @@ export class Resource extends AsyncEventEmitter {
2472
2472
  const prefix = `resource=${this.name}`;
2473
2473
  const deletedCount = await this.client.deleteAll({ prefix });
2474
2474
 
2475
- this._emitStandardized("deleteAllData", "deleted-all-data", {
2475
+ this._emitStandardized("deleted-all-data", {
2476
2476
  resource: this.name,
2477
2477
  prefix,
2478
2478
  deletedCount
@@ -2550,7 +2550,7 @@ export class Resource extends AsyncEventEmitter {
2550
2550
  const idPart = parts.find(part => part.startsWith('id='));
2551
2551
  return idPart ? idPart.replace('id=', '') : null;
2552
2552
  }).filter(Boolean);
2553
- this._emitStandardized("listIds", "listed-ids", ids.length);
2553
+ this._emitStandardized("listed-ids", ids.length);
2554
2554
  return ids;
2555
2555
  }
2556
2556
 
@@ -2598,13 +2598,13 @@ export class Resource extends AsyncEventEmitter {
2598
2598
  const [ok, err, ids] = await tryFn(() => this.listIds({ limit, offset }));
2599
2599
  if (!ok) throw err;
2600
2600
  const results = await this.processListResults(ids, 'main');
2601
- this._emitStandardized("list", "listed", { count: results.length, errors: 0 });
2601
+ this._emitStandardized("list", { count: results.length, errors: 0 });
2602
2602
  return results;
2603
2603
  }
2604
2604
 
2605
2605
  async listPartition({ partition, partitionValues, limit, offset = 0 }) {
2606
2606
  if (!this.config.partitions?.[partition]) {
2607
- this._emitStandardized("list", "listed", { partition, partitionValues, count: 0, errors: 0 });
2607
+ this._emitStandardized("list", { partition, partitionValues, count: 0, errors: 0 });
2608
2608
  return [];
2609
2609
  }
2610
2610
  const partitionDef = this.config.partitions[partition];
@@ -2614,7 +2614,7 @@ export class Resource extends AsyncEventEmitter {
2614
2614
  const ids = this.extractIdsFromKeys(keys).slice(offset);
2615
2615
  const filteredIds = limit ? ids.slice(0, limit) : ids;
2616
2616
  const results = await this.processPartitionResults(filteredIds, partition, partitionDef, keys);
2617
- this._emitStandardized("list", "listed", { partition, partitionValues, count: results.length, errors: 0 });
2617
+ this._emitStandardized("list", { partition, partitionValues, count: results.length, errors: 0 });
2618
2618
  return results;
2619
2619
  }
2620
2620
 
@@ -2670,7 +2670,7 @@ export class Resource extends AsyncEventEmitter {
2670
2670
  }
2671
2671
  return this.handleResourceError(err, id, context);
2672
2672
  });
2673
- this._emitStandardized("list", "listed", { count: results.length, errors: 0 });
2673
+ this._emitStandardized("list", { count: results.length, errors: 0 });
2674
2674
  return results;
2675
2675
  }
2676
2676
 
@@ -2743,11 +2743,11 @@ export class Resource extends AsyncEventEmitter {
2743
2743
  */
2744
2744
  handleListError(error, { partition, partitionValues }) {
2745
2745
  if (error.message.includes("Partition '") && error.message.includes("' not found")) {
2746
- this._emitStandardized("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
2746
+ this._emitStandardized("list", { partition, partitionValues, count: 0, errors: 1 });
2747
2747
  return [];
2748
2748
  }
2749
2749
 
2750
- this._emitStandardized("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
2750
+ this._emitStandardized("list", { partition, partitionValues, count: 0, errors: 1 });
2751
2751
  return [];
2752
2752
  }
2753
2753
 
@@ -2789,7 +2789,7 @@ export class Resource extends AsyncEventEmitter {
2789
2789
  // Execute afterGetMany hooks
2790
2790
  const finalResults = await this.executeHooks('afterGetMany', results);
2791
2791
 
2792
- this._emitStandardized("getMany", "fetched-many", ids.length);
2792
+ this._emitStandardized("fetched-many", ids.length);
2793
2793
  return finalResults;
2794
2794
  }
2795
2795
 
@@ -2880,7 +2880,7 @@ export class Resource extends AsyncEventEmitter {
2880
2880
  hasTotalItems: totalItems !== null
2881
2881
  }
2882
2882
  };
2883
- this._emitStandardized("page", "paginated", result);
2883
+ this._emitStandardized("paginated", result);
2884
2884
  return result;
2885
2885
  });
2886
2886
  if (ok) return result;
@@ -2954,7 +2954,7 @@ export class Resource extends AsyncEventEmitter {
2954
2954
  contentType
2955
2955
  }));
2956
2956
  if (!ok2) throw err2;
2957
- this._emitStandardized("setContent", "content-set", { id, contentType, contentLength: buffer.length }, id);
2957
+ this._emitStandardized("content-set", { id, contentType, contentLength: buffer.length }, id);
2958
2958
  return updatedData;
2959
2959
  }
2960
2960
 
@@ -2984,7 +2984,7 @@ export class Resource extends AsyncEventEmitter {
2984
2984
  }
2985
2985
  const buffer = Buffer.from(await response.Body.transformToByteArray());
2986
2986
  const contentType = response.ContentType || null;
2987
- this._emitStandardized("content", "content-fetched", { id, contentLength: buffer.length, contentType }, id);
2987
+ this._emitStandardized("content-fetched", { id, contentLength: buffer.length, contentType }, id);
2988
2988
  return {
2989
2989
  buffer,
2990
2990
  contentType
@@ -3018,7 +3018,7 @@ export class Resource extends AsyncEventEmitter {
3018
3018
  metadata: existingMetadata,
3019
3019
  }));
3020
3020
  if (!ok2) throw err2;
3021
- this._emitStandardized("deleteContent", "content-deleted", id, id);
3021
+ this._emitStandardized("content-deleted", id, id);
3022
3022
  return response;
3023
3023
  }
3024
3024
 
@@ -3428,7 +3428,7 @@ export class Resource extends AsyncEventEmitter {
3428
3428
  data._partition = partitionName;
3429
3429
  data._partitionValues = partitionValues;
3430
3430
 
3431
- this._emitStandardized("getFromPartition", "partition-fetched", data, data.id);
3431
+ this._emitStandardized("partition-fetched", data, data.id);
3432
3432
  return data;
3433
3433
  }
3434
3434
 
@@ -3719,120 +3719,114 @@ export class Resource extends AsyncEventEmitter {
3719
3719
  // ============================================================================
3720
3720
 
3721
3721
  /**
3722
- * Send an event to trigger a state transition
3723
- * @param {string} id - Entity ID
3724
- * @param {string} event - Event name
3725
- * @param {Object} [eventData] - Event data
3726
- * @returns {Promise<Object>} Transition result
3727
- * @throws {Error} If no state machine is configured for this resource
3722
+ * State machine accessor object
3723
+ * Provides namespaced access to state machine operations
3724
+ * @type {Object}
3725
+ * @property {Function} send - Trigger state transition
3726
+ * @property {Function} get - Get current state
3727
+ * @property {Function} canTransition - Check if transition is valid
3728
+ * @property {Function} getValidEvents - Get valid events for current state
3729
+ * @property {Function} initialize - Initialize entity with initial state
3730
+ * @property {Function} history - Get transition history
3728
3731
  * @example
3729
- * await orders.state('order-123', 'CONFIRM', { confirmedBy: 'user-456' });
3730
- */
3731
- async state(id, event, eventData) {
3732
- if (!this._stateMachine) {
3733
- throw new Error(
3734
- `No state machine configured for resource '${this.name}'. ` +
3735
- `Ensure StateMachinePlugin is installed and configured for this resource.`
3736
- );
3737
- }
3738
- return this._stateMachine.send(id, event, eventData);
3739
- }
3732
+ * await orders.state.send('order-123', 'CONFIRM');
3733
+ * const state = await orders.state.get('order-123');
3734
+ * const canShip = await orders.state.canTransition('order-123', 'SHIP');
3735
+ */
3736
+ get state() {
3737
+ const resource = this;
3738
+
3739
+ const throwIfNoStateMachine = () => {
3740
+ if (!resource._stateMachine) {
3741
+ throw new Error(
3742
+ `No state machine configured for resource '${resource.name}'. ` +
3743
+ `Ensure StateMachinePlugin is installed and configured for this resource.`
3744
+ );
3745
+ }
3746
+ };
3740
3747
 
3741
- /**
3742
- * Get current state of an entity
3743
- * @param {string} id - Entity ID
3744
- * @returns {Promise<string>} Current state
3745
- * @throws {Error} If no state machine is configured for this resource
3746
- * @example
3747
- * const currentState = await orders.getState('order-123');
3748
- */
3749
- async getState(id) {
3750
- if (!this._stateMachine) {
3751
- throw new Error(
3752
- `No state machine configured for resource '${this.name}'. ` +
3753
- `Ensure StateMachinePlugin is installed and configured for this resource.`
3754
- );
3755
- }
3756
- return this._stateMachine.getState(id);
3757
- }
3748
+ return {
3749
+ /**
3750
+ * Trigger a state transition
3751
+ * @param {string} id - Entity ID
3752
+ * @param {string} event - Event name
3753
+ * @param {Object} [eventData] - Event data
3754
+ * @returns {Promise<Object>} Transition result
3755
+ * @example
3756
+ * await orders.state.send('order-123', 'CONFIRM', { confirmedBy: 'user-456' });
3757
+ */
3758
+ send: async (id, event, eventData) => {
3759
+ throwIfNoStateMachine();
3760
+ return resource._stateMachine.send(id, event, eventData);
3761
+ },
3758
3762
 
3759
- /**
3760
- * Check if a transition is valid
3761
- * @param {string} id - Entity ID
3762
- * @param {string} event - Event name
3763
- * @returns {Promise<boolean>} True if transition is valid
3764
- * @throws {Error} If no state machine is configured for this resource
3765
- * @example
3766
- * const canConfirm = await orders.canTransition('order-123', 'CONFIRM');
3767
- */
3768
- async canTransition(id, event) {
3769
- if (!this._stateMachine) {
3770
- throw new Error(
3771
- `No state machine configured for resource '${this.name}'. ` +
3772
- `Ensure StateMachinePlugin is installed and configured for this resource.`
3773
- );
3774
- }
3775
- return this._stateMachine.canTransition(id, event);
3776
- }
3763
+ /**
3764
+ * Get current state of an entity
3765
+ * @param {string} id - Entity ID
3766
+ * @returns {Promise<string>} Current state
3767
+ * @example
3768
+ * const currentState = await orders.state.get('order-123');
3769
+ */
3770
+ get: async (id) => {
3771
+ throwIfNoStateMachine();
3772
+ return resource._stateMachine.getState(id);
3773
+ },
3777
3774
 
3778
- /**
3779
- * Get all valid events for the current state
3780
- * @param {string} id - Entity ID
3781
- * @returns {Promise<Array<string>>} Array of valid event names
3782
- * @throws {Error} If no state machine is configured for this resource
3783
- * @example
3784
- * const events = await orders.getValidEvents('order-123');
3785
- * // Returns: ['SHIP', 'CANCEL']
3786
- */
3787
- async getValidEvents(id) {
3788
- if (!this._stateMachine) {
3789
- throw new Error(
3790
- `No state machine configured for resource '${this.name}'. ` +
3791
- `Ensure StateMachinePlugin is installed and configured for this resource.`
3792
- );
3793
- }
3794
- return this._stateMachine.getValidEvents(id);
3795
- }
3775
+ /**
3776
+ * Check if a transition is valid
3777
+ * @param {string} id - Entity ID
3778
+ * @param {string} event - Event name
3779
+ * @returns {Promise<boolean>} True if transition is valid
3780
+ * @example
3781
+ * const canConfirm = await orders.state.canTransition('order-123', 'CONFIRM');
3782
+ */
3783
+ canTransition: async (id, event) => {
3784
+ throwIfNoStateMachine();
3785
+ return resource._stateMachine.canTransition(id, event);
3786
+ },
3796
3787
 
3797
- /**
3798
- * Initialize entity with initial state
3799
- * @param {string} id - Entity ID
3800
- * @param {Object} [context] - Initial context data
3801
- * @returns {Promise<void>}
3802
- * @throws {Error} If no state machine is configured for this resource
3803
- * @example
3804
- * await orders.initializeState('order-456', { customerId: 'user-123' });
3805
- */
3806
- async initializeState(id, context) {
3807
- if (!this._stateMachine) {
3808
- throw new Error(
3809
- `No state machine configured for resource '${this.name}'. ` +
3810
- `Ensure StateMachinePlugin is installed and configured for this resource.`
3811
- );
3812
- }
3813
- return this._stateMachine.initializeEntity(id, context);
3814
- }
3788
+ /**
3789
+ * Get all valid events for the current state
3790
+ * @param {string} id - Entity ID
3791
+ * @returns {Promise<Array<string>>} Array of valid event names
3792
+ * @example
3793
+ * const events = await orders.state.getValidEvents('order-123');
3794
+ * // Returns: ['SHIP', 'CANCEL']
3795
+ */
3796
+ getValidEvents: async (id) => {
3797
+ throwIfNoStateMachine();
3798
+ return resource._stateMachine.getValidEvents(id);
3799
+ },
3815
3800
 
3816
- /**
3817
- * Get transition history for an entity
3818
- * @param {string} id - Entity ID
3819
- * @param {Object} [options] - Query options
3820
- * @param {number} [options.limit=100] - Maximum number of transitions
3821
- * @param {Date} [options.fromDate] - Filter from date
3822
- * @param {Date} [options.toDate] - Filter to date
3823
- * @returns {Promise<Array<Object>>} Transition history
3824
- * @throws {Error} If no state machine is configured for this resource
3825
- * @example
3826
- * const history = await orders.getStateHistory('order-123', { limit: 50 });
3827
- */
3828
- async getStateHistory(id, options) {
3829
- if (!this._stateMachine) {
3830
- throw new Error(
3831
- `No state machine configured for resource '${this.name}'. ` +
3832
- `Ensure StateMachinePlugin is installed and configured for this resource.`
3833
- );
3834
- }
3835
- return this._stateMachine.getTransitionHistory(id, options);
3801
+ /**
3802
+ * Initialize entity with initial state
3803
+ * @param {string} id - Entity ID
3804
+ * @param {Object} [context] - Initial context data
3805
+ * @returns {Promise<void>}
3806
+ * @example
3807
+ * await orders.state.initialize('order-456', { customerId: 'user-123' });
3808
+ */
3809
+ initialize: async (id, context) => {
3810
+ throwIfNoStateMachine();
3811
+ return resource._stateMachine.initializeEntity(id, context);
3812
+ },
3813
+
3814
+ /**
3815
+ * Get transition history for an entity
3816
+ * @param {string} id - Entity ID
3817
+ * @param {Object} [options] - Query options
3818
+ * @param {number} [options.limit=100] - Maximum number of transitions
3819
+ * @param {Date} [options.fromDate] - Filter from date
3820
+ * @param {Date} [options.toDate] - Filter to date
3821
+ * @returns {Promise<Array<Object>>} Transition history
3822
+ * @example
3823
+ * const history = await orders.state.history('order-123', { limit: 50 });
3824
+ */
3825
+ history: async (id, options) => {
3826
+ throwIfNoStateMachine();
3827
+ return resource._stateMachine.getTransitionHistory(id, options);
3828
+ }
3829
+ };
3836
3830
  }
3837
3831
 
3838
3832
  /**