s3db.js 13.3.1 → 13.5.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 +34 -10
- package/dist/s3db.cjs.js +12208 -11139
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +12209 -11140
- package/dist/s3db.es.js.map +1 -1
- package/package.json +16 -3
- package/src/database.class.js +2 -2
- package/src/plugins/api/auth/basic-auth.js +17 -9
- package/src/plugins/api/index.js +23 -19
- package/src/plugins/api/routes/auth-routes.js +100 -79
- package/src/plugins/api/routes/resource-routes.js +3 -2
- package/src/plugins/api/server.js +176 -5
- package/src/plugins/api/utils/custom-routes.js +102 -0
- package/src/plugins/api/utils/openapi-generator.js +52 -6
- package/src/plugins/audit.plugin.js +427 -0
- package/src/plugins/concerns/plugin-dependencies.js +1 -1
- package/src/plugins/costs.plugin.js +524 -0
- package/src/plugins/eventual-consistency/consolidation.js +2 -2
- package/src/plugins/eventual-consistency/garbage-collection.js +2 -2
- package/src/plugins/eventual-consistency/install.js +2 -2
- package/src/plugins/fulltext.plugin.js +484 -0
- package/src/plugins/metrics.plugin.js +575 -0
- package/src/plugins/ml/base-model.class.js +33 -9
- package/src/plugins/ml.plugin.js +474 -13
- package/src/plugins/queue-consumer.plugin.js +607 -19
- package/src/plugins/state-machine.plugin.js +187 -26
|
@@ -133,10 +133,46 @@ export class StateMachinePlugin extends Plugin {
|
|
|
133
133
|
this.machines = new Map();
|
|
134
134
|
this.triggerIntervals = [];
|
|
135
135
|
this.schedulerPlugin = null;
|
|
136
|
+
this._pendingEventHandlers = new Set();
|
|
136
137
|
|
|
137
138
|
this._validateConfiguration();
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Wait for all pending event handlers to complete
|
|
143
|
+
* Useful when working with async events (asyncEvents: true)
|
|
144
|
+
* @param {number} timeout - Maximum time to wait in milliseconds (default: 5000)
|
|
145
|
+
* @returns {Promise<void>}
|
|
146
|
+
*/
|
|
147
|
+
async waitForPendingEvents(timeout = 5000) {
|
|
148
|
+
if (this._pendingEventHandlers.size === 0) {
|
|
149
|
+
return; // No pending events
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const startTime = Date.now();
|
|
153
|
+
|
|
154
|
+
while (this._pendingEventHandlers.size > 0) {
|
|
155
|
+
if (Date.now() - startTime > timeout) {
|
|
156
|
+
throw new StateMachineError(
|
|
157
|
+
`Timeout waiting for ${this._pendingEventHandlers.size} pending event handlers`,
|
|
158
|
+
{
|
|
159
|
+
operation: 'waitForPendingEvents',
|
|
160
|
+
pendingCount: this._pendingEventHandlers.size,
|
|
161
|
+
timeout
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Wait for at least one handler to complete
|
|
167
|
+
if (this._pendingEventHandlers.size > 0) {
|
|
168
|
+
await Promise.race(Array.from(this._pendingEventHandlers));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Small delay before checking again
|
|
172
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
140
176
|
_validateConfiguration() {
|
|
141
177
|
if (!this.config.stateMachines || Object.keys(this.config.stateMachines).length === 0) {
|
|
142
178
|
throw new StateMachineError('At least one state machine must be defined', {
|
|
@@ -1170,10 +1206,22 @@ export class StateMachinePlugin extends Plugin {
|
|
|
1170
1206
|
|
|
1171
1207
|
/**
|
|
1172
1208
|
* Setup an event-based trigger
|
|
1209
|
+
* Supports both old API (trigger.event) and new API (trigger.eventName + eventSource)
|
|
1173
1210
|
* @private
|
|
1174
1211
|
*/
|
|
1175
1212
|
async _setupEventTrigger(machineId, stateName, trigger, triggerName) {
|
|
1176
|
-
|
|
1213
|
+
// Support both old API (event) and new API (eventName)
|
|
1214
|
+
const baseEventName = trigger.eventName || trigger.event;
|
|
1215
|
+
const eventSource = trigger.eventSource;
|
|
1216
|
+
|
|
1217
|
+
if (!baseEventName) {
|
|
1218
|
+
throw new StateMachineError(`Event trigger '${triggerName}' must have either 'event' or 'eventName' property`, {
|
|
1219
|
+
operation: '_setupEventTrigger',
|
|
1220
|
+
machineId,
|
|
1221
|
+
stateName,
|
|
1222
|
+
triggerName
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1177
1225
|
|
|
1178
1226
|
// Create event listener
|
|
1179
1227
|
const eventHandler = async (eventData) => {
|
|
@@ -1181,6 +1229,26 @@ export class StateMachinePlugin extends Plugin {
|
|
|
1181
1229
|
|
|
1182
1230
|
for (const entity of entities) {
|
|
1183
1231
|
try {
|
|
1232
|
+
// Resolve dynamic event name if it's a function
|
|
1233
|
+
let resolvedEventName;
|
|
1234
|
+
if (typeof baseEventName === 'function') {
|
|
1235
|
+
resolvedEventName = baseEventName(entity.context);
|
|
1236
|
+
} else {
|
|
1237
|
+
resolvedEventName = baseEventName;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// Skip if event name doesn't match (for dynamic event names)
|
|
1241
|
+
// This allows filtering events by entity context
|
|
1242
|
+
if (eventSource && typeof baseEventName === 'function') {
|
|
1243
|
+
// For resource-specific events with dynamic names, we need to check
|
|
1244
|
+
// if this specific event matches this entity
|
|
1245
|
+
// The eventData will contain the ID that was part of the event name
|
|
1246
|
+
const eventIdMatch = eventData?.id || eventData?.entityId;
|
|
1247
|
+
if (eventIdMatch && entity.entityId !== eventIdMatch) {
|
|
1248
|
+
continue; // Not for this entity
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1184
1252
|
// Check condition if provided
|
|
1185
1253
|
if (trigger.condition) {
|
|
1186
1254
|
const shouldTrigger = await trigger.condition(entity.context, entity.entityId, eventData);
|
|
@@ -1198,33 +1266,92 @@ export class StateMachinePlugin extends Plugin {
|
|
|
1198
1266
|
}
|
|
1199
1267
|
}
|
|
1200
1268
|
|
|
1201
|
-
//
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1269
|
+
// NEW: Support targetState for automatic transitions
|
|
1270
|
+
if (trigger.targetState) {
|
|
1271
|
+
// Automatic transition to target state
|
|
1272
|
+
await this._transition(
|
|
1273
|
+
machineId,
|
|
1274
|
+
entity.entityId,
|
|
1275
|
+
stateName,
|
|
1276
|
+
trigger.targetState,
|
|
1277
|
+
'TRIGGER',
|
|
1278
|
+
{ ...entity.context, eventData, triggerName }
|
|
1279
|
+
);
|
|
1209
1280
|
|
|
1210
|
-
|
|
1281
|
+
// Update resource's stateField if configured
|
|
1282
|
+
const machine = this.machines.get(machineId);
|
|
1283
|
+
const resourceConfig = machine.config;
|
|
1284
|
+
if (resourceConfig.resource && resourceConfig.stateField) {
|
|
1285
|
+
// Get the resource instance
|
|
1286
|
+
let resource;
|
|
1287
|
+
if (typeof resourceConfig.resource === 'string') {
|
|
1288
|
+
resource = await this.database.getResource(resourceConfig.resource);
|
|
1289
|
+
} else {
|
|
1290
|
+
resource = resourceConfig.resource;
|
|
1291
|
+
}
|
|
1211
1292
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1293
|
+
// Update the state field in the resource
|
|
1294
|
+
if (resource) {
|
|
1295
|
+
const [ok] = await tryFn(() =>
|
|
1296
|
+
resource.patch(entity.entityId, { [resourceConfig.stateField]: trigger.targetState })
|
|
1297
|
+
);
|
|
1298
|
+
if (!ok && this.config.verbose) {
|
|
1299
|
+
console.warn(`[StateMachinePlugin] Failed to update resource stateField for entity ${entity.entityId}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// Execute entry action of target state if exists
|
|
1305
|
+
const targetStateConfig = machine.config.states[trigger.targetState];
|
|
1306
|
+
if (targetStateConfig?.entry) {
|
|
1307
|
+
await this._executeAction(
|
|
1308
|
+
targetStateConfig.entry,
|
|
1309
|
+
{ ...entity.context, eventData },
|
|
1310
|
+
'TRIGGER',
|
|
1311
|
+
machineId,
|
|
1312
|
+
entity.entityId
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// Emit transition event
|
|
1317
|
+
this.emit('plg:state-machine:transition', {
|
|
1318
|
+
machineId,
|
|
1319
|
+
entityId: entity.entityId,
|
|
1320
|
+
from: stateName,
|
|
1321
|
+
to: trigger.targetState,
|
|
1322
|
+
event: 'TRIGGER',
|
|
1323
|
+
context: { ...entity.context, eventData, triggerName }
|
|
1218
1324
|
});
|
|
1325
|
+
} else if (trigger.action) {
|
|
1326
|
+
// Execute trigger action with event data in context
|
|
1327
|
+
const result = await this._executeAction(
|
|
1328
|
+
trigger.action,
|
|
1329
|
+
{ ...entity.context, eventData },
|
|
1330
|
+
'TRIGGER',
|
|
1331
|
+
machineId,
|
|
1332
|
+
entity.entityId
|
|
1333
|
+
);
|
|
1334
|
+
|
|
1335
|
+
// Send success event if configured
|
|
1336
|
+
if (trigger.sendEvent) {
|
|
1337
|
+
await this.send(machineId, entity.entityId, trigger.sendEvent, {
|
|
1338
|
+
...entity.context,
|
|
1339
|
+
triggerResult: result,
|
|
1340
|
+
eventData
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1219
1343
|
}
|
|
1220
1344
|
|
|
1345
|
+
await this._incrementTriggerCount(machineId, entity.entityId, triggerName);
|
|
1346
|
+
|
|
1221
1347
|
this.emit('plg:state-machine:trigger-executed', {
|
|
1222
1348
|
machineId,
|
|
1223
1349
|
entityId: entity.entityId,
|
|
1224
1350
|
state: stateName,
|
|
1225
1351
|
trigger: triggerName,
|
|
1226
1352
|
type: 'event',
|
|
1227
|
-
eventName
|
|
1353
|
+
eventName: resolvedEventName,
|
|
1354
|
+
targetState: trigger.targetState
|
|
1228
1355
|
});
|
|
1229
1356
|
} catch (error) {
|
|
1230
1357
|
if (this.config.verbose) {
|
|
@@ -1234,20 +1361,54 @@ export class StateMachinePlugin extends Plugin {
|
|
|
1234
1361
|
}
|
|
1235
1362
|
};
|
|
1236
1363
|
|
|
1237
|
-
//
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
|
|
1364
|
+
// NEW: Support eventSource for resource-specific events
|
|
1365
|
+
if (eventSource) {
|
|
1366
|
+
// Listen to events from a specific resource
|
|
1367
|
+
// Resource events are typically: inserted, updated, deleted
|
|
1368
|
+
const baseEvent = typeof baseEventName === 'function' ? 'updated' : baseEventName;
|
|
1369
|
+
|
|
1370
|
+
// IMPORTANT: For resources with async events, we need to ensure the event handler
|
|
1371
|
+
// completes before returning control. We wrap the handler to track pending operations.
|
|
1372
|
+
const wrappedHandler = async (...args) => {
|
|
1373
|
+
// Track this as a pending operation
|
|
1374
|
+
const handlerPromise = eventHandler(...args);
|
|
1375
|
+
|
|
1376
|
+
// Store promise if state machine has event tracking
|
|
1377
|
+
if (!this._pendingEventHandlers) {
|
|
1378
|
+
this._pendingEventHandlers = new Set();
|
|
1379
|
+
}
|
|
1380
|
+
this._pendingEventHandlers.add(handlerPromise);
|
|
1381
|
+
|
|
1382
|
+
try {
|
|
1383
|
+
await handlerPromise;
|
|
1384
|
+
} finally {
|
|
1385
|
+
this._pendingEventHandlers.delete(handlerPromise);
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
|
|
1389
|
+
eventSource.on(baseEvent, wrappedHandler);
|
|
1241
1390
|
|
|
1242
1391
|
if (this.config.verbose) {
|
|
1243
|
-
console.log(`[StateMachinePlugin] Listening to
|
|
1392
|
+
console.log(`[StateMachinePlugin] Listening to resource event '${baseEvent}' from '${eventSource.name}' for trigger '${triggerName}' (async-safe)`);
|
|
1244
1393
|
}
|
|
1245
1394
|
} else {
|
|
1246
|
-
//
|
|
1247
|
-
|
|
1395
|
+
// Original behavior: listen to database or plugin events
|
|
1396
|
+
const staticEventName = typeof baseEventName === 'function' ? 'updated' : baseEventName;
|
|
1248
1397
|
|
|
1249
|
-
if (
|
|
1250
|
-
|
|
1398
|
+
if (staticEventName.startsWith('db:')) {
|
|
1399
|
+
const dbEventName = staticEventName.substring(3); // Remove 'db:' prefix
|
|
1400
|
+
this.database.on(dbEventName, eventHandler);
|
|
1401
|
+
|
|
1402
|
+
if (this.config.verbose) {
|
|
1403
|
+
console.log(`[StateMachinePlugin] Listening to database event '${dbEventName}' for trigger '${triggerName}'`);
|
|
1404
|
+
}
|
|
1405
|
+
} else {
|
|
1406
|
+
// Listen to plugin events
|
|
1407
|
+
this.on(staticEventName, eventHandler);
|
|
1408
|
+
|
|
1409
|
+
if (this.config.verbose) {
|
|
1410
|
+
console.log(`[StateMachinePlugin] Listening to plugin event '${staticEventName}' for trigger '${triggerName}'`);
|
|
1411
|
+
}
|
|
1251
1412
|
}
|
|
1252
1413
|
}
|
|
1253
1414
|
}
|