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.
Files changed (82) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36664 -15480
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +57 -0
  5. package/dist/s3db.es.js +36661 -15531
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +27 -6
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +41 -46
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +39 -19
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +539 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +350 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  55. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  56. package/src/plugins/replicators/index.js +28 -3
  57. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  58. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  59. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  60. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  61. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  62. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  63. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  64. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  65. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  66. package/src/plugins/state-machine.plugin.js +122 -68
  67. package/src/plugins/tfstate/README.md +745 -0
  68. package/src/plugins/tfstate/base-driver.js +80 -0
  69. package/src/plugins/tfstate/errors.js +112 -0
  70. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  71. package/src/plugins/tfstate/index.js +2660 -0
  72. package/src/plugins/tfstate/s3-driver.js +192 -0
  73. package/src/plugins/ttl.plugin.js +536 -0
  74. package/src/resource.class.js +14 -10
  75. package/src/s3db.d.ts +57 -0
  76. package/src/schema.class.js +366 -32
  77. package/SECURITY.md +0 -76
  78. package/src/partition-drivers/base-partition-driver.js +0 -106
  79. package/src/partition-drivers/index.js +0 -66
  80. package/src/partition-drivers/memory-partition-driver.js +0 -289
  81. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  82. 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
- if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
235
- throw new StateMachineError(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`, {
236
- operation: 'send',
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
- currentState,
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
- if (!guardOk || !guardResult) {
259
- throw new StateMachineError(`Transition blocked by guard '${guardName}'`, {
260
- operation: 'send',
261
- machineId,
262
- entityId,
263
- event,
264
- currentState,
265
- guardName,
266
- guardError: guardErr?.message || 'Guard returned false',
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
  */