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/dist/s3db.cjs.js +191 -21
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +191 -21
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/plugins/state-machine.plugin.js +68 -0
- package/src/resource.class.js +151 -20
package/package.json
CHANGED
|
@@ -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`);
|
package/src/resource.class.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2746
|
+
this._emitStandardized("list", "listed", { partition, partitionValues, count: 0, errors: 1 });
|
|
2747
2747
|
return [];
|
|
2748
2748
|
}
|
|
2749
2749
|
|
|
2750
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
/**
|