s3db.js 13.2.1 → 13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "13.2.1",
3
+ "version": "13.2.2",
4
4
  "description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
5
5
  "main": "dist/s3db.cjs.js",
6
6
  "module": "dist/s3db.es.js",
@@ -191,6 +191,9 @@ export class StateMachinePlugin extends Plugin {
191
191
  });
192
192
  }
193
193
 
194
+ // Attach state machines to resources for direct API access
195
+ await this._attachStateMachinesToResources();
196
+
194
197
  // Setup trigger system if enabled
195
198
  await this._setupTriggers();
196
199
 
@@ -1249,6 +1252,71 @@ export class StateMachinePlugin extends Plugin {
1249
1252
  }
1250
1253
  }
1251
1254
 
1255
+ /**
1256
+ * Attach state machine instances to their associated resources
1257
+ * This enables the resource API: resource.state(id, event)
1258
+ * @private
1259
+ */
1260
+ async _attachStateMachinesToResources() {
1261
+ for (const [machineName, machineConfig] of Object.entries(this.config.stateMachines)) {
1262
+ const resourceConfig = machineConfig.config || machineConfig;
1263
+
1264
+ // Skip if no resource is specified
1265
+ if (!resourceConfig.resource) {
1266
+ if (this.config.verbose) {
1267
+ console.log(`[StateMachinePlugin] Machine '${machineName}' has no resource configured, skipping attachment`);
1268
+ }
1269
+ continue;
1270
+ }
1271
+
1272
+ // Get the resource instance
1273
+ let resource;
1274
+ if (typeof resourceConfig.resource === 'string') {
1275
+ // Resource specified as name
1276
+ resource = this.database.resources[resourceConfig.resource];
1277
+ if (!resource) {
1278
+ console.warn(
1279
+ `[StateMachinePlugin] Resource '${resourceConfig.resource}' not found for machine '${machineName}'. ` +
1280
+ `Resource API will not be available.`
1281
+ );
1282
+ continue;
1283
+ }
1284
+ } else {
1285
+ // Resource specified as instance
1286
+ resource = resourceConfig.resource;
1287
+ }
1288
+
1289
+ // Create a machine proxy that delegates to this plugin
1290
+ const machineProxy = {
1291
+ send: async (id, event, eventData) => {
1292
+ return this.send(machineName, id, event, eventData);
1293
+ },
1294
+ getState: async (id) => {
1295
+ return this.getState(machineName, id);
1296
+ },
1297
+ canTransition: async (id, event) => {
1298
+ return this.canTransition(machineName, id, event);
1299
+ },
1300
+ getValidEvents: async (id) => {
1301
+ return this.getValidEvents(machineName, id);
1302
+ },
1303
+ initializeEntity: async (id, context) => {
1304
+ return this.initializeEntity(machineName, id, context);
1305
+ },
1306
+ getTransitionHistory: async (id, options) => {
1307
+ return this.getTransitionHistory(machineName, id, options);
1308
+ }
1309
+ };
1310
+
1311
+ // Attach the proxy to the resource
1312
+ resource._attachStateMachine(machineProxy);
1313
+
1314
+ if (this.config.verbose) {
1315
+ console.log(`[StateMachinePlugin] Attached machine '${machineName}' to resource '${resource.name}'`);
1316
+ }
1317
+ }
1318
+ }
1319
+
1252
1320
  async start() {
1253
1321
  if (this.config.verbose) {
1254
1322
  console.log(`[StateMachinePlugin] Started with ${this.machines.size} state machines`);
@@ -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._emitWithDeprecation("get", "fetched", data, data.id);
1325
+ this._emitStandardized("get", "fetched", data, data.id);
1326
1326
  const value = data;
1327
1327
  return value;
1328
1328
  }
@@ -2202,7 +2202,7 @@ export class Resource extends AsyncEventEmitter {
2202
2202
  const [ok2, err2, response] = await tryFn(() => this.client.deleteObject(key));
2203
2203
 
2204
2204
  // Always emit delete event for audit purposes, even if delete fails
2205
- this._emitWithDeprecation("delete", "deleted", {
2205
+ this._emitStandardized("delete", "deleted", {
2206
2206
  ...objectData,
2207
2207
  $before: { ...objectData },
2208
2208
  $after: null
@@ -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._emitWithDeprecation("count", "counted", count);
2360
+ this._emitStandardized("count", "counted", 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._emitWithDeprecation("insertMany", "inserted-many", objects.length);
2388
+ this._emitStandardized("insertMany", "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._emitWithDeprecation("deleteMany", "deleted-many", ids.length);
2438
+ this._emitStandardized("deleteMany", "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._emitWithDeprecation("deleteAll", "deleted-all", {
2452
+ this._emitStandardized("deleteAll", "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._emitWithDeprecation("deleteAllData", "deleted-all-data", {
2475
+ this._emitStandardized("deleteAllData", "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._emitWithDeprecation("listIds", "listed-ids", ids.length);
2553
+ this._emitStandardized("listIds", "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._emitWithDeprecation("list", "listed", { count: results.length, errors: 0 });
2601
+ this._emitStandardized("list", "listed", { 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._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 0 });
2607
+ this._emitStandardized("list", "listed", { 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._emitWithDeprecation("list", "listed", { partition, partitionValues, count: results.length, errors: 0 });
2617
+ this._emitStandardized("list", "listed", { 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._emitWithDeprecation("list", "listed", { count: results.length, errors: 0 });
2673
+ this._emitStandardized("list", "listed", { 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._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
2746
+ this._emitStandardized("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
2747
2747
  return [];
2748
2748
  }
2749
2749
 
2750
- this._emitWithDeprecation("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
2750
+ this._emitStandardized("list", "listed", { 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._emitWithDeprecation("getMany", "fetched-many", ids.length);
2792
+ this._emitStandardized("getMany", "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._emitWithDeprecation("page", "paginated", result);
2883
+ this._emitStandardized("page", "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._emitWithDeprecation("setContent", "content-set", { id, contentType, contentLength: buffer.length }, id);
2957
+ this._emitStandardized("setContent", "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._emitWithDeprecation("content", "content-fetched", { id, contentLength: buffer.length, contentType }, id);
2987
+ this._emitStandardized("content", "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._emitWithDeprecation("deleteContent", "content-deleted", id, id);
3021
+ this._emitStandardized("deleteContent", "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._emitWithDeprecation("getFromPartition", "partition-fetched", data, data.id);
3431
+ this._emitStandardized("getFromPartition", "partition-fetched", data, data.id);
3432
3432
  return data;
3433
3433
  }
3434
3434
 
@@ -3714,6 +3714,137 @@ export class Resource extends AsyncEventEmitter {
3714
3714
  return out;
3715
3715
  }
3716
3716
 
3717
+ // ============================================================================
3718
+ // STATE MACHINE METHODS
3719
+ // ============================================================================
3720
+
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
3728
+ * @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
+ }
3740
+
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
+ }
3758
+
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
+ }
3777
+
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
+ }
3796
+
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
+ }
3815
+
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);
3836
+ }
3837
+
3838
+ /**
3839
+ * Internal method to attach state machine instance
3840
+ * This is called by StateMachinePlugin during initialization
3841
+ * @private
3842
+ * @param {Object} stateMachine - State machine instance
3843
+ */
3844
+ _attachStateMachine(stateMachine) {
3845
+ this._stateMachine = stateMachine;
3846
+ }
3847
+
3717
3848
  }
3718
3849
 
3719
3850
  /**