s3db.js 11.3.2 → 12.0.0
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 +102 -8
- package/dist/s3db.cjs.js +36664 -15480
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +57 -0
- package/dist/s3db.es.js +36661 -15531
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +27 -6
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +41 -46
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +39 -19
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +539 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +350 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +14 -10
- package/src/s3db.d.ts +57 -0
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- package/src/partition-drivers/sync-partition-driver.js +0 -38
|
@@ -100,7 +100,7 @@ import { StateMachineError } from "./state-machine.errors.js";
|
|
|
100
100
|
export class StateMachinePlugin extends Plugin {
|
|
101
101
|
constructor(options = {}) {
|
|
102
102
|
super();
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
this.config = {
|
|
105
105
|
stateMachines: options.stateMachines || {},
|
|
106
106
|
actions: options.actions || {},
|
|
@@ -110,12 +110,16 @@ export class StateMachinePlugin extends Plugin {
|
|
|
110
110
|
stateResource: options.stateResource || 'plg_entity_states',
|
|
111
111
|
retryAttempts: options.retryAttempts || 3,
|
|
112
112
|
retryDelay: options.retryDelay || 100,
|
|
113
|
-
verbose: options.verbose || false
|
|
113
|
+
verbose: options.verbose || false,
|
|
114
|
+
// Distributed lock configuration (prevents concurrent transitions)
|
|
115
|
+
workerId: options.workerId || 'default',
|
|
116
|
+
lockTimeout: options.lockTimeout || 1000, // Wait up to 1s for lock
|
|
117
|
+
lockTTL: options.lockTTL || 5 // Lock expires after 5s (prevent deadlock)
|
|
114
118
|
};
|
|
115
119
|
|
|
116
120
|
this.database = null;
|
|
117
121
|
this.machines = new Map();
|
|
118
|
-
|
|
122
|
+
|
|
119
123
|
this._validateConfiguration();
|
|
120
124
|
}
|
|
121
125
|
|
|
@@ -227,78 +231,86 @@ export class StateMachinePlugin extends Plugin {
|
|
|
227
231
|
suggestion: 'Check machine ID or use getMachines() to list available machines'
|
|
228
232
|
});
|
|
229
233
|
}
|
|
230
|
-
|
|
231
|
-
const currentState = await this.getState(machineId, entityId);
|
|
232
|
-
const stateConfig = machine.config.states[currentState];
|
|
233
234
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
// Acquire distributed lock to prevent concurrent transitions
|
|
236
|
+
const lockName = await this._acquireTransitionLock(machineId, entityId);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const currentState = await this.getState(machineId, entityId);
|
|
240
|
+
const stateConfig = machine.config.states[currentState];
|
|
241
|
+
|
|
242
|
+
if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
|
|
243
|
+
throw new StateMachineError(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`, {
|
|
244
|
+
operation: 'send',
|
|
245
|
+
machineId,
|
|
246
|
+
entityId,
|
|
247
|
+
event,
|
|
248
|
+
currentState,
|
|
249
|
+
validEvents: stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [],
|
|
250
|
+
suggestion: 'Use getValidEvents() to check which events are valid for the current state'
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const targetState = stateConfig.on[event];
|
|
255
|
+
|
|
256
|
+
// Check guards
|
|
257
|
+
if (stateConfig.guards && stateConfig.guards[event]) {
|
|
258
|
+
const guardName = stateConfig.guards[event];
|
|
259
|
+
const guard = this.config.guards[guardName];
|
|
260
|
+
|
|
261
|
+
if (guard) {
|
|
262
|
+
const [guardOk, guardErr, guardResult] = await tryFn(() =>
|
|
263
|
+
guard(context, event, { database: this.database, machineId, entityId })
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
if (!guardOk || !guardResult) {
|
|
267
|
+
throw new StateMachineError(`Transition blocked by guard '${guardName}'`, {
|
|
268
|
+
operation: 'send',
|
|
269
|
+
machineId,
|
|
270
|
+
entityId,
|
|
271
|
+
event,
|
|
272
|
+
currentState,
|
|
273
|
+
guardName,
|
|
274
|
+
guardError: guardErr?.message || 'Guard returned false',
|
|
275
|
+
suggestion: 'Check guard conditions or modify the context to satisfy guard requirements'
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Execute exit action for current state
|
|
282
|
+
if (stateConfig.exit) {
|
|
283
|
+
await this._executeAction(stateConfig.exit, context, event, machineId, entityId);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Execute the transition
|
|
287
|
+
await this._transition(machineId, entityId, currentState, targetState, event, context);
|
|
288
|
+
|
|
289
|
+
// Execute entry action for target state
|
|
290
|
+
const targetStateConfig = machine.config.states[targetState];
|
|
291
|
+
if (targetStateConfig && targetStateConfig.entry) {
|
|
292
|
+
await this._executeAction(targetStateConfig.entry, context, event, machineId, entityId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
this.emit('transition', {
|
|
237
296
|
machineId,
|
|
238
297
|
entityId,
|
|
298
|
+
from: currentState,
|
|
299
|
+
to: targetState,
|
|
239
300
|
event,
|
|
240
|
-
|
|
241
|
-
validEvents: stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [],
|
|
242
|
-
suggestion: 'Use getValidEvents() to check which events are valid for the current state'
|
|
301
|
+
context
|
|
243
302
|
});
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const targetState = stateConfig.on[event];
|
|
247
|
-
|
|
248
|
-
// Check guards
|
|
249
|
-
if (stateConfig.guards && stateConfig.guards[event]) {
|
|
250
|
-
const guardName = stateConfig.guards[event];
|
|
251
|
-
const guard = this.config.guards[guardName];
|
|
252
|
-
|
|
253
|
-
if (guard) {
|
|
254
|
-
const [guardOk, guardErr, guardResult] = await tryFn(() =>
|
|
255
|
-
guard(context, event, { database: this.database, machineId, entityId })
|
|
256
|
-
);
|
|
257
303
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
suggestion: 'Check guard conditions or modify the context to satisfy guard requirements'
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Execute exit action for current state
|
|
274
|
-
if (stateConfig.exit) {
|
|
275
|
-
await this._executeAction(stateConfig.exit, context, event, machineId, entityId);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Execute the transition
|
|
279
|
-
await this._transition(machineId, entityId, currentState, targetState, event, context);
|
|
280
|
-
|
|
281
|
-
// Execute entry action for target state
|
|
282
|
-
const targetStateConfig = machine.config.states[targetState];
|
|
283
|
-
if (targetStateConfig && targetStateConfig.entry) {
|
|
284
|
-
await this._executeAction(targetStateConfig.entry, context, event, machineId, entityId);
|
|
304
|
+
return {
|
|
305
|
+
from: currentState,
|
|
306
|
+
to: targetState,
|
|
307
|
+
event,
|
|
308
|
+
timestamp: new Date().toISOString()
|
|
309
|
+
};
|
|
310
|
+
} finally {
|
|
311
|
+
// Always release lock, even if transition fails
|
|
312
|
+
await this._releaseTransitionLock(lockName);
|
|
285
313
|
}
|
|
286
|
-
|
|
287
|
-
this.emit('transition', {
|
|
288
|
-
machineId,
|
|
289
|
-
entityId,
|
|
290
|
-
from: currentState,
|
|
291
|
-
to: targetState,
|
|
292
|
-
event,
|
|
293
|
-
context
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
return {
|
|
297
|
-
from: currentState,
|
|
298
|
-
to: targetState,
|
|
299
|
-
event,
|
|
300
|
-
timestamp: new Date().toISOString()
|
|
301
|
-
};
|
|
302
314
|
}
|
|
303
315
|
|
|
304
316
|
async _executeAction(actionName, context, event, machineId, entityId) {
|
|
@@ -399,6 +411,48 @@ export class StateMachinePlugin extends Plugin {
|
|
|
399
411
|
}
|
|
400
412
|
}
|
|
401
413
|
|
|
414
|
+
/**
|
|
415
|
+
* Acquire distributed lock for transition
|
|
416
|
+
* Prevents concurrent transitions for the same entity
|
|
417
|
+
* @private
|
|
418
|
+
*/
|
|
419
|
+
async _acquireTransitionLock(machineId, entityId) {
|
|
420
|
+
const storage = this.getStorage();
|
|
421
|
+
const lockName = `transition-${machineId}-${entityId}`;
|
|
422
|
+
|
|
423
|
+
const lock = await storage.acquireLock(lockName, {
|
|
424
|
+
ttl: this.config.lockTTL,
|
|
425
|
+
timeout: this.config.lockTimeout,
|
|
426
|
+
workerId: this.config.workerId
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (!lock) {
|
|
430
|
+
throw new StateMachineError('Could not acquire transition lock - concurrent transition in progress', {
|
|
431
|
+
operation: 'send',
|
|
432
|
+
machineId,
|
|
433
|
+
entityId,
|
|
434
|
+
lockTimeout: this.config.lockTimeout,
|
|
435
|
+
workerId: this.config.workerId,
|
|
436
|
+
suggestion: 'Wait for current transition to complete or increase lockTimeout'
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return lockName;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Release distributed lock for transition
|
|
445
|
+
* @private
|
|
446
|
+
*/
|
|
447
|
+
async _releaseTransitionLock(lockName) {
|
|
448
|
+
const storage = this.getStorage();
|
|
449
|
+
const [ok, err] = await tryFn(() => storage.releaseLock(lockName));
|
|
450
|
+
|
|
451
|
+
if (!ok && this.config.verbose) {
|
|
452
|
+
console.warn(`[StateMachinePlugin] Failed to release lock '${lockName}':`, err.message);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
402
456
|
/**
|
|
403
457
|
* Get current state for an entity
|
|
404
458
|
*/
|