s3db.js 11.2.2 → 11.2.4

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 (46) hide show
  1. package/dist/s3db.cjs.js +1650 -136
  2. package/dist/s3db.cjs.js.map +1 -1
  3. package/dist/s3db.es.js +1644 -137
  4. package/dist/s3db.es.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/behaviors/enforce-limits.js +28 -4
  7. package/src/behaviors/index.js +6 -1
  8. package/src/client.class.js +11 -1
  9. package/src/concerns/partition-queue.js +7 -1
  10. package/src/concerns/plugin-storage.js +75 -13
  11. package/src/database.class.js +22 -4
  12. package/src/errors.js +414 -24
  13. package/src/partition-drivers/base-partition-driver.js +12 -2
  14. package/src/partition-drivers/index.js +7 -1
  15. package/src/partition-drivers/memory-partition-driver.js +20 -5
  16. package/src/partition-drivers/sqs-partition-driver.js +6 -1
  17. package/src/plugins/audit.errors.js +46 -0
  18. package/src/plugins/backup/base-backup-driver.class.js +36 -6
  19. package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
  20. package/src/plugins/backup/index.js +40 -9
  21. package/src/plugins/backup/multi-backup-driver.class.js +69 -9
  22. package/src/plugins/backup/s3-backup-driver.class.js +48 -6
  23. package/src/plugins/backup.errors.js +45 -0
  24. package/src/plugins/cache/cache.class.js +8 -1
  25. package/src/plugins/cache/memory-cache.class.js +216 -33
  26. package/src/plugins/cache.errors.js +47 -0
  27. package/src/plugins/cache.plugin.js +94 -3
  28. package/src/plugins/eventual-consistency/analytics.js +145 -0
  29. package/src/plugins/eventual-consistency/index.js +203 -1
  30. package/src/plugins/fulltext.errors.js +46 -0
  31. package/src/plugins/fulltext.plugin.js +15 -3
  32. package/src/plugins/metrics.errors.js +46 -0
  33. package/src/plugins/queue-consumer.plugin.js +31 -4
  34. package/src/plugins/queue.errors.js +46 -0
  35. package/src/plugins/replicator.errors.js +46 -0
  36. package/src/plugins/replicator.plugin.js +40 -5
  37. package/src/plugins/replicators/base-replicator.class.js +19 -3
  38. package/src/plugins/replicators/index.js +9 -3
  39. package/src/plugins/replicators/s3db-replicator.class.js +38 -8
  40. package/src/plugins/scheduler.errors.js +46 -0
  41. package/src/plugins/scheduler.plugin.js +79 -19
  42. package/src/plugins/state-machine.errors.js +47 -0
  43. package/src/plugins/state-machine.plugin.js +86 -17
  44. package/src/resource.class.js +8 -1
  45. package/src/stream/index.js +6 -1
  46. package/src/stream/resource-reader.class.js +6 -1
package/dist/s3db.cjs.js CHANGED
@@ -15,6 +15,7 @@ var jsonStableStringify = require('json-stable-stringify');
15
15
  var stream = require('stream');
16
16
  var promisePool = require('@supercharge/promise-pool');
17
17
  var web = require('node:stream/web');
18
+ var os$1 = require('node:os');
18
19
  var lodashEs = require('lodash-es');
19
20
  var http = require('http');
20
21
  var https = require('https');
@@ -221,7 +222,7 @@ function calculateEffectiveLimit(config = {}) {
221
222
  }
222
223
 
223
224
  class BaseError extends Error {
224
- constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, suggestion, ...rest }) {
225
+ constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, description, ...rest }) {
225
226
  if (verbose) message = message + `
226
227
 
227
228
  Verbose:
@@ -246,7 +247,7 @@ ${JSON.stringify(rest, null, 2)}`;
246
247
  this.commandName = commandName;
247
248
  this.commandInput = commandInput;
248
249
  this.metadata = metadata;
249
- this.suggestion = suggestion;
250
+ this.description = description;
250
251
  this.data = { bucket, key, ...rest, verbose, message };
251
252
  }
252
253
  toJson() {
@@ -263,7 +264,7 @@ ${JSON.stringify(rest, null, 2)}`;
263
264
  commandName: this.commandName,
264
265
  commandInput: this.commandInput,
265
266
  metadata: this.metadata,
266
- suggestion: this.suggestion,
267
+ description: this.description,
267
268
  data: this.data,
268
269
  original: this.original,
269
270
  stack: this.stack
@@ -403,26 +404,26 @@ function mapAwsError(err, context = {}) {
403
404
  const metadata = err.$metadata ? { ...err.$metadata } : void 0;
404
405
  const commandName = context.commandName;
405
406
  const commandInput = context.commandInput;
406
- let suggestion;
407
+ let description;
407
408
  if (code === "NoSuchKey" || code === "NotFound") {
408
- suggestion = "Check if the key exists in the specified bucket and if your credentials have permission.";
409
- return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, suggestion });
409
+ description = "The specified key does not exist in the bucket. Check if the key exists and if your credentials have permission to access it.";
410
+ return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, description });
410
411
  }
411
412
  if (code === "NoSuchBucket") {
412
- suggestion = "Check if the bucket exists and if your credentials have permission.";
413
- return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, suggestion });
413
+ description = "The specified bucket does not exist. Check if the bucket name is correct and if your credentials have permission to access it.";
414
+ return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, description });
414
415
  }
415
416
  if (code === "AccessDenied" || err.statusCode === 403 || code === "Forbidden") {
416
- suggestion = "Check your credentials and bucket policy.";
417
- return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput, suggestion });
417
+ description = "Access denied. Check your AWS credentials, IAM permissions, and bucket policy.";
418
+ return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput, description });
418
419
  }
419
420
  if (code === "ValidationError" || err.statusCode === 400) {
420
- suggestion = "Check the request parameters and payload.";
421
- return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput, suggestion });
421
+ description = "Validation error. Check the request parameters and payload format.";
422
+ return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput, description });
422
423
  }
423
424
  if (code === "MissingMetadata") {
424
- suggestion = "Check if the object metadata is present and valid.";
425
- return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, suggestion });
425
+ description = "Object metadata is missing or invalid. Check if the object was uploaded correctly.";
426
+ return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, description });
426
427
  }
427
428
  const errorDetails = [
428
429
  `Unknown error: ${err.message || err.toString()}`,
@@ -430,33 +431,388 @@ function mapAwsError(err, context = {}) {
430
431
  err.statusCode && `Status: ${err.statusCode}`,
431
432
  err.stack && `Stack: ${err.stack.split("\n")[0]}`
432
433
  ].filter(Boolean).join(" | ");
433
- suggestion = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
434
- return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, suggestion });
434
+ description = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
435
+ return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, description });
435
436
  }
436
437
  class ConnectionStringError extends S3dbError {
437
438
  constructor(message, details = {}) {
438
- super(message, { ...details, suggestion: "Check the connection string format and credentials." });
439
+ const description = details.description || "Invalid connection string format. Check the connection string syntax and credentials.";
440
+ super(message, { ...details, description });
439
441
  }
440
442
  }
441
443
  class CryptoError extends S3dbError {
442
444
  constructor(message, details = {}) {
443
- super(message, { ...details, suggestion: "Check if the crypto library is available and input is valid." });
445
+ const description = details.description || "Cryptography operation failed. Check if the crypto library is available and input is valid.";
446
+ super(message, { ...details, description });
444
447
  }
445
448
  }
446
449
  class SchemaError extends S3dbError {
447
450
  constructor(message, details = {}) {
448
- super(message, { ...details, suggestion: "Check schema definition and input data." });
451
+ const description = details.description || "Schema validation failed. Check schema definition and input data format.";
452
+ super(message, { ...details, description });
449
453
  }
450
454
  }
451
455
  class ResourceError extends S3dbError {
452
456
  constructor(message, details = {}) {
453
- super(message, { ...details, suggestion: details.suggestion || "Check resource configuration, attributes, and operation context." });
457
+ const description = details.description || "Resource operation failed. Check resource configuration, attributes, and operation context.";
458
+ super(message, { ...details, description });
454
459
  Object.assign(this, details);
455
460
  }
456
461
  }
457
462
  class PartitionError extends S3dbError {
458
463
  constructor(message, details = {}) {
459
- super(message, { ...details, suggestion: details.suggestion || "Check partition definition, fields, and input values." });
464
+ let description = details.description;
465
+ if (!description && details.resourceName && details.partitionName && details.fieldName) {
466
+ const { resourceName, partitionName, fieldName, availableFields = [] } = details;
467
+ description = `
468
+ Partition Field Validation Error
469
+
470
+ Resource: ${resourceName}
471
+ Partition: ${partitionName}
472
+ Missing Field: ${fieldName}
473
+
474
+ Available fields in schema:
475
+ ${availableFields.map((f) => ` \u2022 ${f}`).join("\n") || " (no fields defined)"}
476
+
477
+ Possible causes:
478
+ 1. Field was removed from schema but partition still references it
479
+ 2. Typo in partition field name
480
+ 3. Nested field path is incorrect (use dot notation like 'utm.source')
481
+
482
+ Solution:
483
+ ${details.strictValidation === false ? " \u2022 Update partition definition to use existing fields" : ` \u2022 Add missing field to schema, OR
484
+ \u2022 Update partition definition to use existing fields, OR
485
+ \u2022 Use strictValidation: false to skip this check during testing`}
486
+
487
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partitions
488
+ `.trim();
489
+ }
490
+ super(message, {
491
+ ...details,
492
+ description
493
+ });
494
+ }
495
+ }
496
+ class AnalyticsNotEnabledError extends S3dbError {
497
+ constructor(details = {}) {
498
+ const {
499
+ pluginName = "EventualConsistency",
500
+ resourceName = "unknown",
501
+ field = "unknown",
502
+ configuredResources = [],
503
+ registeredResources = [],
504
+ pluginInitialized = false,
505
+ ...rest
506
+ } = details;
507
+ const message = `Analytics not enabled for ${resourceName}.${field}`;
508
+ const description = `
509
+ Analytics Not Enabled
510
+
511
+ Plugin: ${pluginName}
512
+ Resource: ${resourceName}
513
+ Field: ${field}
514
+
515
+ Diagnostics:
516
+ \u2022 Plugin initialized: ${pluginInitialized ? "\u2713 Yes" : "\u2717 No"}
517
+ \u2022 Analytics resources created: ${registeredResources.length}/${configuredResources.length}
518
+ ${configuredResources.map((r) => {
519
+ const exists = registeredResources.includes(r);
520
+ return ` ${exists ? "\u2713" : "\u2717"} ${r}${!exists ? " (missing)" : ""}`;
521
+ }).join("\n")}
522
+
523
+ Possible causes:
524
+ 1. Resource not created yet - Analytics resources are created when db.createResource() is called
525
+ 2. Resource created before plugin initialization - Plugin must be initialized before resources
526
+ 3. Field not configured in analytics.resources config
527
+
528
+ Correct initialization order:
529
+ 1. Create database: const db = new Database({ ... })
530
+ 2. Install plugins: await db.connect() (triggers plugin.install())
531
+ 3. Create resources: await db.createResource({ name: '${resourceName}', ... })
532
+ 4. Analytics resources are auto-created by plugin
533
+
534
+ Example fix:
535
+ const db = new Database({
536
+ bucket: 'my-bucket',
537
+ plugins: [new EventualConsistencyPlugin({
538
+ resources: {
539
+ '${resourceName}': {
540
+ fields: {
541
+ '${field}': { type: 'counter', analytics: true }
542
+ }
543
+ }
544
+ }
545
+ })]
546
+ });
547
+
548
+ await db.connect(); // Plugin initialized here
549
+ await db.createResource({ name: '${resourceName}', ... }); // Analytics resource created here
550
+
551
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/eventual-consistency.md
552
+ `.trim();
553
+ super(message, {
554
+ ...rest,
555
+ pluginName,
556
+ resourceName,
557
+ field,
558
+ configuredResources,
559
+ registeredResources,
560
+ pluginInitialized,
561
+ description
562
+ });
563
+ }
564
+ }
565
+ class PluginError extends S3dbError {
566
+ constructor(message, details = {}) {
567
+ const {
568
+ pluginName = "Unknown",
569
+ operation = "unknown",
570
+ ...rest
571
+ } = details;
572
+ let description = details.description;
573
+ if (!description) {
574
+ description = `
575
+ Plugin Error
576
+
577
+ Plugin: ${pluginName}
578
+ Operation: ${operation}
579
+
580
+ Possible causes:
581
+ 1. Plugin not properly initialized
582
+ 2. Plugin configuration is invalid
583
+ 3. Plugin dependencies not met
584
+ 4. Plugin method called before installation
585
+
586
+ Solution:
587
+ Ensure plugin is added to database and connect() is called before usage.
588
+
589
+ Example:
590
+ const db = new Database({
591
+ bucket: 'my-bucket',
592
+ plugins: [new ${pluginName}({ /* config */ })]
593
+ });
594
+
595
+ await db.connect(); // Plugin installed here
596
+ // Now plugin methods are available
597
+
598
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md
599
+ `.trim();
600
+ }
601
+ super(message, {
602
+ ...rest,
603
+ pluginName,
604
+ operation,
605
+ description
606
+ });
607
+ }
608
+ }
609
+ class PluginStorageError extends S3dbError {
610
+ constructor(message, details = {}) {
611
+ const {
612
+ pluginSlug = "unknown",
613
+ key = "",
614
+ operation = "unknown",
615
+ ...rest
616
+ } = details;
617
+ let description = details.description;
618
+ if (!description) {
619
+ description = `
620
+ Plugin Storage Error
621
+
622
+ Plugin: ${pluginSlug}
623
+ Key: ${key}
624
+ Operation: ${operation}
625
+
626
+ Possible causes:
627
+ 1. Storage not initialized (plugin not installed)
628
+ 2. Invalid key format
629
+ 3. S3 operation failed
630
+ 4. Permissions issue
631
+
632
+ Solution:
633
+ Ensure plugin has access to storage and key is valid.
634
+
635
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md#plugin-storage
636
+ `.trim();
637
+ }
638
+ super(message, {
639
+ ...rest,
640
+ pluginSlug,
641
+ key,
642
+ operation,
643
+ description
644
+ });
645
+ }
646
+ }
647
+ class PartitionDriverError extends S3dbError {
648
+ constructor(message, details = {}) {
649
+ const {
650
+ driver = "unknown",
651
+ operation = "unknown",
652
+ queueSize,
653
+ maxQueueSize,
654
+ ...rest
655
+ } = details;
656
+ let description = details.description;
657
+ if (!description && queueSize !== void 0 && maxQueueSize !== void 0) {
658
+ description = `
659
+ Partition Driver Error
660
+
661
+ Driver: ${driver}
662
+ Operation: ${operation}
663
+ Queue Status: ${queueSize}/${maxQueueSize}
664
+
665
+ Possible causes:
666
+ 1. Queue is full (backpressure)
667
+ 2. Driver not properly configured
668
+ 3. SQS permissions issue (if using SQS driver)
669
+
670
+ Solution:
671
+ ${queueSize >= maxQueueSize ? "Wait for queue to drain or increase maxQueueSize" : "Check driver configuration and permissions"}
672
+
673
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
674
+ `.trim();
675
+ } else if (!description) {
676
+ description = `
677
+ Partition Driver Error
678
+
679
+ Driver: ${driver}
680
+ Operation: ${operation}
681
+
682
+ Check driver configuration and permissions.
683
+
684
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
685
+ `.trim();
686
+ }
687
+ super(message, {
688
+ ...rest,
689
+ driver,
690
+ operation,
691
+ queueSize,
692
+ maxQueueSize,
693
+ description
694
+ });
695
+ }
696
+ }
697
+ class BehaviorError extends S3dbError {
698
+ constructor(message, details = {}) {
699
+ const {
700
+ behavior = "unknown",
701
+ availableBehaviors = [],
702
+ ...rest
703
+ } = details;
704
+ let description = details.description;
705
+ if (!description) {
706
+ description = `
707
+ Behavior Error
708
+
709
+ Requested: ${behavior}
710
+ Available: ${availableBehaviors.join(", ") || "body-overflow, body-only, truncate-data, enforce-limits, user-managed"}
711
+
712
+ Possible causes:
713
+ 1. Behavior name misspelled
714
+ 2. Custom behavior not registered
715
+
716
+ Solution:
717
+ Use one of the available behaviors or register custom behavior.
718
+
719
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#behaviors
720
+ `.trim();
721
+ }
722
+ super(message, {
723
+ ...rest,
724
+ behavior,
725
+ availableBehaviors,
726
+ description
727
+ });
728
+ }
729
+ }
730
+ class StreamError extends S3dbError {
731
+ constructor(message, details = {}) {
732
+ const {
733
+ operation = "unknown",
734
+ resource,
735
+ ...rest
736
+ } = details;
737
+ let description = details.description;
738
+ if (!description) {
739
+ description = `
740
+ Stream Error
741
+
742
+ Operation: ${operation}
743
+ ${resource ? `Resource: ${resource}` : ""}
744
+
745
+ Possible causes:
746
+ 1. Stream not properly initialized
747
+ 2. Resource not available
748
+ 3. Network error during streaming
749
+
750
+ Solution:
751
+ Check stream configuration and resource availability.
752
+
753
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#streaming
754
+ `.trim();
755
+ }
756
+ super(message, {
757
+ ...rest,
758
+ operation,
759
+ resource,
760
+ description
761
+ });
762
+ }
763
+ }
764
+ class MetadataLimitError extends S3dbError {
765
+ constructor(message, details = {}) {
766
+ const {
767
+ totalSize,
768
+ effectiveLimit,
769
+ absoluteLimit = 2047,
770
+ excess,
771
+ resourceName,
772
+ operation,
773
+ ...rest
774
+ } = details;
775
+ let description = details.description;
776
+ if (!description && totalSize && effectiveLimit) {
777
+ description = `
778
+ S3 Metadata Size Limit Exceeded
779
+
780
+ Current Size: ${totalSize} bytes
781
+ Effective Limit: ${effectiveLimit} bytes
782
+ Absolute Limit: ${absoluteLimit} bytes
783
+ ${excess ? `Excess: ${excess} bytes` : ""}
784
+ ${resourceName ? `Resource: ${resourceName}` : ""}
785
+ ${operation ? `Operation: ${operation}` : ""}
786
+
787
+ S3 has a hard limit of 2KB (2047 bytes) for object metadata.
788
+
789
+ Solutions:
790
+ 1. Use 'body-overflow' behavior to store excess in body
791
+ 2. Use 'body-only' behavior to store everything in body
792
+ 3. Reduce number of fields
793
+ 4. Use shorter field values
794
+ 5. Enable advanced metadata encoding
795
+
796
+ Example:
797
+ await db.createResource({
798
+ name: '${resourceName || "myResource"}',
799
+ behavior: 'body-overflow', // Automatically handles overflow
800
+ attributes: { ... }
801
+ });
802
+
803
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#metadata-size-limits
804
+ `.trim();
805
+ }
806
+ super(message, {
807
+ ...rest,
808
+ totalSize,
809
+ effectiveLimit,
810
+ absoluteLimit,
811
+ excess,
812
+ resourceName,
813
+ operation,
814
+ description
815
+ });
460
816
  }
461
817
  }
462
818
 
@@ -799,10 +1155,17 @@ class PluginStorage {
799
1155
  */
800
1156
  constructor(client, pluginSlug) {
801
1157
  if (!client) {
802
- throw new Error("PluginStorage requires a client instance");
1158
+ throw new PluginStorageError("PluginStorage requires a client instance", {
1159
+ operation: "constructor",
1160
+ pluginSlug,
1161
+ suggestion: "Pass a valid S3db Client instance when creating PluginStorage"
1162
+ });
803
1163
  }
804
1164
  if (!pluginSlug) {
805
- throw new Error("PluginStorage requires a pluginSlug");
1165
+ throw new PluginStorageError("PluginStorage requires a pluginSlug", {
1166
+ operation: "constructor",
1167
+ suggestion: 'Provide a plugin slug (e.g., "eventual-consistency", "cache", "audit")'
1168
+ });
806
1169
  }
807
1170
  this.client = client;
808
1171
  this.pluginSlug = pluginSlug;
@@ -855,7 +1218,15 @@ class PluginStorage {
855
1218
  }
856
1219
  const [ok, err] = await tryFn(() => this.client.putObject(putParams));
857
1220
  if (!ok) {
858
- throw new Error(`PluginStorage.set failed for key ${key}: ${err.message}`);
1221
+ throw new PluginStorageError(`Failed to save plugin data`, {
1222
+ pluginSlug: this.pluginSlug,
1223
+ key,
1224
+ operation: "set",
1225
+ behavior,
1226
+ ttl,
1227
+ original: err,
1228
+ suggestion: "Check S3 permissions and key format"
1229
+ });
859
1230
  }
860
1231
  }
861
1232
  /**
@@ -877,7 +1248,13 @@ class PluginStorage {
877
1248
  if (err.name === "NoSuchKey" || err.Code === "NoSuchKey") {
878
1249
  return null;
879
1250
  }
880
- throw new Error(`PluginStorage.get failed for key ${key}: ${err.message}`);
1251
+ throw new PluginStorageError(`Failed to retrieve plugin data`, {
1252
+ pluginSlug: this.pluginSlug,
1253
+ key,
1254
+ operation: "get",
1255
+ original: err,
1256
+ suggestion: "Check if the key exists and S3 permissions are correct"
1257
+ });
881
1258
  }
882
1259
  const metadata = response.Metadata || {};
883
1260
  const parsedMetadata = this._parseMetadataValues(metadata);
@@ -890,7 +1267,13 @@ class PluginStorage {
890
1267
  data = { ...parsedMetadata, ...body };
891
1268
  }
892
1269
  } catch (parseErr) {
893
- throw new Error(`PluginStorage.get failed to parse body for key ${key}: ${parseErr.message}`);
1270
+ throw new PluginStorageError(`Failed to parse JSON body`, {
1271
+ pluginSlug: this.pluginSlug,
1272
+ key,
1273
+ operation: "get",
1274
+ original: parseErr,
1275
+ suggestion: "Body content may be corrupted. Check S3 object integrity"
1276
+ });
894
1277
  }
895
1278
  }
896
1279
  const expiresAt = data._expiresat || data._expiresAt;
@@ -951,7 +1334,15 @@ class PluginStorage {
951
1334
  () => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
952
1335
  );
953
1336
  if (!ok) {
954
- throw new Error(`PluginStorage.list failed: ${err.message}`);
1337
+ throw new PluginStorageError(`Failed to list plugin data`, {
1338
+ pluginSlug: this.pluginSlug,
1339
+ operation: "list",
1340
+ prefix,
1341
+ fullPrefix,
1342
+ limit,
1343
+ original: err,
1344
+ suggestion: "Check S3 permissions and bucket configuration"
1345
+ });
955
1346
  }
956
1347
  const keys = result.Contents?.map((item) => item.Key) || [];
957
1348
  return this._removeKeyPrefix(keys);
@@ -971,7 +1362,16 @@ class PluginStorage {
971
1362
  () => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
972
1363
  );
973
1364
  if (!ok) {
974
- throw new Error(`PluginStorage.listForResource failed: ${err.message}`);
1365
+ throw new PluginStorageError(`Failed to list resource data`, {
1366
+ pluginSlug: this.pluginSlug,
1367
+ operation: "listForResource",
1368
+ resourceName,
1369
+ subPrefix,
1370
+ fullPrefix,
1371
+ limit,
1372
+ original: err,
1373
+ suggestion: "Check resource name and S3 permissions"
1374
+ });
975
1375
  }
976
1376
  const keys = result.Contents?.map((item) => item.Key) || [];
977
1377
  return this._removeKeyPrefix(keys);
@@ -1111,7 +1511,13 @@ class PluginStorage {
1111
1511
  async delete(key) {
1112
1512
  const [ok, err] = await tryFn(() => this.client.deleteObject(key));
1113
1513
  if (!ok) {
1114
- throw new Error(`PluginStorage.delete failed for key ${key}: ${err.message}`);
1514
+ throw new PluginStorageError(`Failed to delete plugin data`, {
1515
+ pluginSlug: this.pluginSlug,
1516
+ key,
1517
+ operation: "delete",
1518
+ original: err,
1519
+ suggestion: "Check S3 delete permissions"
1520
+ });
1115
1521
  }
1116
1522
  }
1117
1523
  /**
@@ -1298,16 +1704,28 @@ class PluginStorage {
1298
1704
  const valueSize = calculateUTF8Bytes(encoded);
1299
1705
  currentSize += keySize + valueSize;
1300
1706
  if (currentSize > effectiveLimit) {
1301
- throw new Error(
1302
- `Data exceeds metadata limit (${currentSize} > ${effectiveLimit} bytes). Use 'body-overflow' or 'body-only' behavior.`
1303
- );
1707
+ throw new MetadataLimitError(`Data exceeds metadata limit with enforce-limits behavior`, {
1708
+ totalSize: currentSize,
1709
+ effectiveLimit,
1710
+ absoluteLimit: S3_METADATA_LIMIT,
1711
+ excess: currentSize - effectiveLimit,
1712
+ operation: "PluginStorage.set",
1713
+ pluginSlug: this.pluginSlug,
1714
+ suggestion: "Use 'body-overflow' or 'body-only' behavior to handle large data"
1715
+ });
1304
1716
  }
1305
1717
  metadata[key] = jsonValue;
1306
1718
  }
1307
1719
  break;
1308
1720
  }
1309
1721
  default:
1310
- throw new Error(`Unknown behavior: ${behavior}. Use 'body-overflow', 'body-only', or 'enforce-limits'.`);
1722
+ throw new BehaviorError(`Unknown behavior: ${behavior}`, {
1723
+ behavior,
1724
+ availableBehaviors: ["body-overflow", "body-only", "enforce-limits"],
1725
+ operation: "PluginStorage._applyBehavior",
1726
+ pluginSlug: this.pluginSlug,
1727
+ suggestion: "Use 'body-overflow', 'body-only', or 'enforce-limits'"
1728
+ });
1311
1729
  }
1312
1730
  return { metadata, body };
1313
1731
  }
@@ -1872,6 +2290,35 @@ class AuditPlugin extends Plugin {
1872
2290
  }
1873
2291
  }
1874
2292
 
2293
+ class BackupError extends S3dbError {
2294
+ constructor(message, details = {}) {
2295
+ const { driver = "unknown", operation = "unknown", backupId, ...rest } = details;
2296
+ let description = details.description;
2297
+ if (!description) {
2298
+ description = `
2299
+ Backup Operation Error
2300
+
2301
+ Driver: ${driver}
2302
+ Operation: ${operation}
2303
+ ${backupId ? `Backup ID: ${backupId}` : ""}
2304
+
2305
+ Common causes:
2306
+ 1. Invalid backup driver configuration
2307
+ 2. Destination storage not accessible
2308
+ 3. Insufficient permissions
2309
+ 4. Network connectivity issues
2310
+ 5. Invalid backup file format
2311
+
2312
+ Solution:
2313
+ Check driver configuration and ensure destination storage is accessible.
2314
+
2315
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/backup.md
2316
+ `.trim();
2317
+ }
2318
+ super(message, { ...rest, driver, operation, backupId, description });
2319
+ }
2320
+ }
2321
+
1875
2322
  class BaseBackupDriver {
1876
2323
  constructor(config = {}) {
1877
2324
  this.config = {
@@ -1902,7 +2349,12 @@ class BaseBackupDriver {
1902
2349
  * @returns {Object} Upload result with destination info
1903
2350
  */
1904
2351
  async upload(filePath, backupId, manifest) {
1905
- throw new Error("upload() method must be implemented by subclass");
2352
+ throw new BackupError("upload() method must be implemented by subclass", {
2353
+ operation: "upload",
2354
+ driver: this.constructor.name,
2355
+ backupId,
2356
+ suggestion: "Extend BaseBackupDriver and implement the upload() method"
2357
+ });
1906
2358
  }
1907
2359
  /**
1908
2360
  * Download a backup file from the destination
@@ -1912,7 +2364,12 @@ class BaseBackupDriver {
1912
2364
  * @returns {string} Path to downloaded file
1913
2365
  */
1914
2366
  async download(backupId, targetPath, metadata) {
1915
- throw new Error("download() method must be implemented by subclass");
2367
+ throw new BackupError("download() method must be implemented by subclass", {
2368
+ operation: "download",
2369
+ driver: this.constructor.name,
2370
+ backupId,
2371
+ suggestion: "Extend BaseBackupDriver and implement the download() method"
2372
+ });
1916
2373
  }
1917
2374
  /**
1918
2375
  * Delete a backup from the destination
@@ -1920,7 +2377,12 @@ class BaseBackupDriver {
1920
2377
  * @param {Object} metadata - Backup metadata
1921
2378
  */
1922
2379
  async delete(backupId, metadata) {
1923
- throw new Error("delete() method must be implemented by subclass");
2380
+ throw new BackupError("delete() method must be implemented by subclass", {
2381
+ operation: "delete",
2382
+ driver: this.constructor.name,
2383
+ backupId,
2384
+ suggestion: "Extend BaseBackupDriver and implement the delete() method"
2385
+ });
1924
2386
  }
1925
2387
  /**
1926
2388
  * List backups available in the destination
@@ -1928,7 +2390,11 @@ class BaseBackupDriver {
1928
2390
  * @returns {Array} List of backup metadata
1929
2391
  */
1930
2392
  async list(options = {}) {
1931
- throw new Error("list() method must be implemented by subclass");
2393
+ throw new BackupError("list() method must be implemented by subclass", {
2394
+ operation: "list",
2395
+ driver: this.constructor.name,
2396
+ suggestion: "Extend BaseBackupDriver and implement the list() method"
2397
+ });
1932
2398
  }
1933
2399
  /**
1934
2400
  * Verify backup integrity
@@ -1938,14 +2404,23 @@ class BaseBackupDriver {
1938
2404
  * @returns {boolean} True if backup is valid
1939
2405
  */
1940
2406
  async verify(backupId, expectedChecksum, metadata) {
1941
- throw new Error("verify() method must be implemented by subclass");
2407
+ throw new BackupError("verify() method must be implemented by subclass", {
2408
+ operation: "verify",
2409
+ driver: this.constructor.name,
2410
+ backupId,
2411
+ suggestion: "Extend BaseBackupDriver and implement the verify() method"
2412
+ });
1942
2413
  }
1943
2414
  /**
1944
2415
  * Get driver type identifier
1945
2416
  * @returns {string} Driver type
1946
2417
  */
1947
2418
  getType() {
1948
- throw new Error("getType() method must be implemented by subclass");
2419
+ throw new BackupError("getType() method must be implemented by subclass", {
2420
+ operation: "getType",
2421
+ driver: this.constructor.name,
2422
+ suggestion: "Extend BaseBackupDriver and implement the getType() method"
2423
+ });
1949
2424
  }
1950
2425
  /**
1951
2426
  * Get driver-specific storage info
@@ -1987,7 +2462,11 @@ class FilesystemBackupDriver extends BaseBackupDriver {
1987
2462
  }
1988
2463
  async onSetup() {
1989
2464
  if (!this.config.path) {
1990
- throw new Error("FilesystemBackupDriver: path configuration is required");
2465
+ throw new BackupError("FilesystemBackupDriver: path configuration is required", {
2466
+ operation: "onSetup",
2467
+ driver: "filesystem",
2468
+ suggestion: 'Provide a path in config: new FilesystemBackupDriver({ path: "/path/to/backups" })'
2469
+ });
1991
2470
  }
1992
2471
  this.log(`Initialized with path: ${this.config.path}`);
1993
2472
  }
@@ -2011,11 +2490,26 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2011
2490
  () => promises.mkdir(targetDir, { recursive: true, mode: this.config.directoryPermissions })
2012
2491
  );
2013
2492
  if (!createDirOk) {
2014
- throw new Error(`Failed to create backup directory: ${createDirErr.message}`);
2493
+ throw new BackupError("Failed to create backup directory", {
2494
+ operation: "upload",
2495
+ driver: "filesystem",
2496
+ backupId,
2497
+ targetDir,
2498
+ original: createDirErr,
2499
+ suggestion: "Check directory permissions and disk space"
2500
+ });
2015
2501
  }
2016
2502
  const [copyOk, copyErr] = await tryFn(() => promises.copyFile(filePath, targetPath));
2017
2503
  if (!copyOk) {
2018
- throw new Error(`Failed to copy backup file: ${copyErr.message}`);
2504
+ throw new BackupError("Failed to copy backup file", {
2505
+ operation: "upload",
2506
+ driver: "filesystem",
2507
+ backupId,
2508
+ filePath,
2509
+ targetPath,
2510
+ original: copyErr,
2511
+ suggestion: "Check file permissions and disk space"
2512
+ });
2019
2513
  }
2020
2514
  const [manifestOk, manifestErr] = await tryFn(
2021
2515
  () => import('fs/promises').then((fs) => fs.writeFile(
@@ -2026,7 +2520,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2026
2520
  );
2027
2521
  if (!manifestOk) {
2028
2522
  await tryFn(() => promises.unlink(targetPath));
2029
- throw new Error(`Failed to write manifest: ${manifestErr.message}`);
2523
+ throw new BackupError("Failed to write manifest file", {
2524
+ operation: "upload",
2525
+ driver: "filesystem",
2526
+ backupId,
2527
+ manifestPath,
2528
+ original: manifestErr,
2529
+ suggestion: "Check directory permissions and disk space"
2530
+ });
2030
2531
  }
2031
2532
  const [statOk, , stats] = await tryFn(() => promises.stat(targetPath));
2032
2533
  const size = statOk ? stats.size : 0;
@@ -2045,13 +2546,27 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2045
2546
  );
2046
2547
  const [existsOk] = await tryFn(() => promises.access(sourcePath));
2047
2548
  if (!existsOk) {
2048
- throw new Error(`Backup file not found: ${sourcePath}`);
2549
+ throw new BackupError("Backup file not found", {
2550
+ operation: "download",
2551
+ driver: "filesystem",
2552
+ backupId,
2553
+ sourcePath,
2554
+ suggestion: "Check if backup exists using list() method"
2555
+ });
2049
2556
  }
2050
2557
  const targetDir = path.dirname(targetPath);
2051
2558
  await tryFn(() => promises.mkdir(targetDir, { recursive: true }));
2052
2559
  const [copyOk, copyErr] = await tryFn(() => promises.copyFile(sourcePath, targetPath));
2053
2560
  if (!copyOk) {
2054
- throw new Error(`Failed to download backup: ${copyErr.message}`);
2561
+ throw new BackupError("Failed to download backup", {
2562
+ operation: "download",
2563
+ driver: "filesystem",
2564
+ backupId,
2565
+ sourcePath,
2566
+ targetPath,
2567
+ original: copyErr,
2568
+ suggestion: "Check file permissions and disk space"
2569
+ });
2055
2570
  }
2056
2571
  this.log(`Downloaded backup ${backupId} from ${sourcePath} to ${targetPath}`);
2057
2572
  return targetPath;
@@ -2068,7 +2583,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2068
2583
  const [deleteBackupOk] = await tryFn(() => promises.unlink(backupPath));
2069
2584
  const [deleteManifestOk] = await tryFn(() => promises.unlink(manifestPath));
2070
2585
  if (!deleteBackupOk && !deleteManifestOk) {
2071
- throw new Error(`Failed to delete backup files for ${backupId}`);
2586
+ throw new BackupError("Failed to delete backup files", {
2587
+ operation: "delete",
2588
+ driver: "filesystem",
2589
+ backupId,
2590
+ backupPath,
2591
+ manifestPath,
2592
+ suggestion: "Check file permissions"
2593
+ });
2072
2594
  }
2073
2595
  this.log(`Deleted backup ${backupId}`);
2074
2596
  }
@@ -2173,10 +2695,18 @@ class S3BackupDriver extends BaseBackupDriver {
2173
2695
  this.config.bucket = this.database.bucket;
2174
2696
  }
2175
2697
  if (!this.config.client) {
2176
- throw new Error("S3BackupDriver: client is required (either via config or database)");
2698
+ throw new BackupError("S3BackupDriver: client is required", {
2699
+ operation: "onSetup",
2700
+ driver: "s3",
2701
+ suggestion: "Provide a client in config or ensure database has a client configured"
2702
+ });
2177
2703
  }
2178
2704
  if (!this.config.bucket) {
2179
- throw new Error("S3BackupDriver: bucket is required (either via config or database)");
2705
+ throw new BackupError("S3BackupDriver: bucket is required", {
2706
+ operation: "onSetup",
2707
+ driver: "s3",
2708
+ suggestion: "Provide a bucket in config or ensure database has a bucket configured"
2709
+ });
2180
2710
  }
2181
2711
  this.log(`Initialized with bucket: ${this.config.bucket}, path: ${this.config.path}`);
2182
2712
  }
@@ -2218,7 +2748,15 @@ class S3BackupDriver extends BaseBackupDriver {
2218
2748
  });
2219
2749
  });
2220
2750
  if (!uploadOk) {
2221
- throw new Error(`Failed to upload backup file: ${uploadErr.message}`);
2751
+ throw new BackupError("Failed to upload backup file to S3", {
2752
+ operation: "upload",
2753
+ driver: "s3",
2754
+ backupId,
2755
+ bucket: this.config.bucket,
2756
+ key: backupKey,
2757
+ original: uploadErr,
2758
+ suggestion: "Check S3 permissions and bucket configuration"
2759
+ });
2222
2760
  }
2223
2761
  const [manifestOk, manifestErr] = await tryFn(
2224
2762
  () => this.config.client.uploadObject({
@@ -2239,7 +2777,15 @@ class S3BackupDriver extends BaseBackupDriver {
2239
2777
  bucket: this.config.bucket,
2240
2778
  key: backupKey
2241
2779
  }));
2242
- throw new Error(`Failed to upload manifest: ${manifestErr.message}`);
2780
+ throw new BackupError("Failed to upload manifest to S3", {
2781
+ operation: "upload",
2782
+ driver: "s3",
2783
+ backupId,
2784
+ bucket: this.config.bucket,
2785
+ manifestKey,
2786
+ original: manifestErr,
2787
+ suggestion: "Check S3 permissions and bucket configuration"
2788
+ });
2243
2789
  }
2244
2790
  this.log(`Uploaded backup ${backupId} to s3://${this.config.bucket}/${backupKey} (${fileSize} bytes)`);
2245
2791
  return {
@@ -2262,7 +2808,16 @@ class S3BackupDriver extends BaseBackupDriver {
2262
2808
  })
2263
2809
  );
2264
2810
  if (!downloadOk) {
2265
- throw new Error(`Failed to download backup: ${downloadErr.message}`);
2811
+ throw new BackupError("Failed to download backup from S3", {
2812
+ operation: "download",
2813
+ driver: "s3",
2814
+ backupId,
2815
+ bucket: this.config.bucket,
2816
+ key: backupKey,
2817
+ targetPath,
2818
+ original: downloadErr,
2819
+ suggestion: "Check if backup exists and S3 permissions are correct"
2820
+ });
2266
2821
  }
2267
2822
  this.log(`Downloaded backup ${backupId} from s3://${this.config.bucket}/${backupKey} to ${targetPath}`);
2268
2823
  return targetPath;
@@ -2283,7 +2838,15 @@ class S3BackupDriver extends BaseBackupDriver {
2283
2838
  })
2284
2839
  );
2285
2840
  if (!deleteBackupOk && !deleteManifestOk) {
2286
- throw new Error(`Failed to delete backup objects for ${backupId}`);
2841
+ throw new BackupError("Failed to delete backup from S3", {
2842
+ operation: "delete",
2843
+ driver: "s3",
2844
+ backupId,
2845
+ bucket: this.config.bucket,
2846
+ backupKey,
2847
+ manifestKey,
2848
+ suggestion: "Check S3 delete permissions"
2849
+ });
2287
2850
  }
2288
2851
  this.log(`Deleted backup ${backupId} from S3`);
2289
2852
  }
@@ -2396,11 +2959,22 @@ class MultiBackupDriver extends BaseBackupDriver {
2396
2959
  }
2397
2960
  async onSetup() {
2398
2961
  if (!Array.isArray(this.config.destinations) || this.config.destinations.length === 0) {
2399
- throw new Error("MultiBackupDriver: destinations array is required and must not be empty");
2962
+ throw new BackupError("MultiBackupDriver requires non-empty destinations array", {
2963
+ operation: "onSetup",
2964
+ driver: "multi",
2965
+ destinationsProvided: this.config.destinations,
2966
+ suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }, { driver: "filesystem", config: {...} }] }'
2967
+ });
2400
2968
  }
2401
2969
  for (const [index, destConfig] of this.config.destinations.entries()) {
2402
2970
  if (!destConfig.driver) {
2403
- throw new Error(`MultiBackupDriver: destination[${index}] must have a driver type`);
2971
+ throw new BackupError(`Destination ${index} missing driver type`, {
2972
+ operation: "onSetup",
2973
+ driver: "multi",
2974
+ destinationIndex: index,
2975
+ destination: destConfig,
2976
+ suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} } or { driver: "filesystem", config: {...} }'
2977
+ });
2404
2978
  }
2405
2979
  try {
2406
2980
  const driver = createBackupDriver(destConfig.driver, destConfig.config || {});
@@ -2412,7 +2986,15 @@ class MultiBackupDriver extends BaseBackupDriver {
2412
2986
  });
2413
2987
  this.log(`Setup destination ${index}: ${destConfig.driver}`);
2414
2988
  } catch (error) {
2415
- throw new Error(`Failed to setup destination ${index} (${destConfig.driver}): ${error.message}`);
2989
+ throw new BackupError(`Failed to setup destination ${index}`, {
2990
+ operation: "onSetup",
2991
+ driver: "multi",
2992
+ destinationIndex: index,
2993
+ destinationDriver: destConfig.driver,
2994
+ destinationConfig: destConfig.config,
2995
+ original: error,
2996
+ suggestion: "Check destination driver configuration and ensure dependencies are available"
2997
+ });
2416
2998
  }
2417
2999
  }
2418
3000
  if (this.config.requireAll === false) {
@@ -2441,7 +3023,15 @@ class MultiBackupDriver extends BaseBackupDriver {
2441
3023
  this.log(`Priority upload failed to destination ${index}: ${err.message}`);
2442
3024
  }
2443
3025
  }
2444
- throw new Error(`All priority destinations failed: ${errors.map((e) => `${e.destination}: ${e.error}`).join("; ")}`);
3026
+ throw new BackupError("All priority destinations failed", {
3027
+ operation: "upload",
3028
+ driver: "multi",
3029
+ strategy: "priority",
3030
+ backupId,
3031
+ totalDestinations: this.drivers.length,
3032
+ failures: errors,
3033
+ suggestion: "Check destination configurations and ensure at least one destination is accessible"
3034
+ });
2445
3035
  }
2446
3036
  const uploadPromises = this.drivers.map(async ({ driver, config, index }) => {
2447
3037
  const [ok, err, result] = await tryFn(
@@ -2471,10 +3061,28 @@ class MultiBackupDriver extends BaseBackupDriver {
2471
3061
  const successResults = allResults.filter((r) => r.status === "success");
2472
3062
  const failedResults = allResults.filter((r) => r.status === "failed");
2473
3063
  if (strategy === "all" && failedResults.length > 0) {
2474
- throw new Error(`Some destinations failed: ${failedResults.map((r) => `${r.destination}: ${r.error}`).join("; ")}`);
3064
+ throw new BackupError('Some destinations failed with strategy "all"', {
3065
+ operation: "upload",
3066
+ driver: "multi",
3067
+ strategy: "all",
3068
+ backupId,
3069
+ totalDestinations: this.drivers.length,
3070
+ successCount: successResults.length,
3071
+ failedCount: failedResults.length,
3072
+ failures: failedResults,
3073
+ suggestion: 'All destinations must succeed with "all" strategy. Use "any" strategy to tolerate failures, or fix failing destinations.'
3074
+ });
2475
3075
  }
2476
3076
  if (strategy === "any" && successResults.length === 0) {
2477
- throw new Error(`All destinations failed: ${failedResults.map((r) => `${r.destination}: ${r.error}`).join("; ")}`);
3077
+ throw new BackupError('All destinations failed with strategy "any"', {
3078
+ operation: "upload",
3079
+ driver: "multi",
3080
+ strategy: "any",
3081
+ backupId,
3082
+ totalDestinations: this.drivers.length,
3083
+ failures: failedResults,
3084
+ suggestion: 'At least one destination must succeed with "any" strategy. Check all destination configurations.'
3085
+ });
2478
3086
  }
2479
3087
  return allResults;
2480
3088
  }
@@ -2494,7 +3102,14 @@ class MultiBackupDriver extends BaseBackupDriver {
2494
3102
  this.log(`Download failed from destination ${destMetadata.destination}: ${err.message}`);
2495
3103
  }
2496
3104
  }
2497
- throw new Error(`Failed to download backup from any destination`);
3105
+ throw new BackupError("Failed to download backup from any destination", {
3106
+ operation: "download",
3107
+ driver: "multi",
3108
+ backupId,
3109
+ targetPath,
3110
+ attemptedDestinations: destinations.length,
3111
+ suggestion: "Check if backup exists in at least one destination and destinations are accessible"
3112
+ });
2498
3113
  }
2499
3114
  async delete(backupId, metadata) {
2500
3115
  const destinations = Array.isArray(metadata.destinations) ? metadata.destinations : [metadata];
@@ -2516,7 +3131,14 @@ class MultiBackupDriver extends BaseBackupDriver {
2516
3131
  }
2517
3132
  }
2518
3133
  if (successCount === 0 && errors.length > 0) {
2519
- throw new Error(`Failed to delete from any destination: ${errors.join("; ")}`);
3134
+ throw new BackupError("Failed to delete from any destination", {
3135
+ operation: "delete",
3136
+ driver: "multi",
3137
+ backupId,
3138
+ attemptedDestinations: destinations.length,
3139
+ failures: errors,
3140
+ suggestion: "Check if backup exists in destinations and destinations are accessible with delete permissions"
3141
+ });
2520
3142
  }
2521
3143
  if (errors.length > 0) {
2522
3144
  this.log(`Partial delete success, some errors: ${errors.join("; ")}`);
@@ -2616,32 +3238,62 @@ const BACKUP_DRIVERS = {
2616
3238
  function createBackupDriver(driver, config = {}) {
2617
3239
  const DriverClass = BACKUP_DRIVERS[driver];
2618
3240
  if (!DriverClass) {
2619
- throw new Error(`Unknown backup driver: ${driver}. Available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`);
3241
+ throw new BackupError(`Unknown backup driver: ${driver}`, {
3242
+ operation: "createBackupDriver",
3243
+ driver,
3244
+ availableDrivers: Object.keys(BACKUP_DRIVERS),
3245
+ suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
3246
+ });
2620
3247
  }
2621
3248
  return new DriverClass(config);
2622
3249
  }
2623
3250
  function validateBackupConfig(driver, config = {}) {
2624
3251
  if (!driver || typeof driver !== "string") {
2625
- throw new Error("Driver type must be a non-empty string");
3252
+ throw new BackupError("Driver type must be a non-empty string", {
3253
+ operation: "validateBackupConfig",
3254
+ driver,
3255
+ suggestion: "Provide a valid driver type string (filesystem, s3, or multi)"
3256
+ });
2626
3257
  }
2627
3258
  if (!BACKUP_DRIVERS[driver]) {
2628
- throw new Error(`Unknown backup driver: ${driver}. Available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`);
3259
+ throw new BackupError(`Unknown backup driver: ${driver}`, {
3260
+ operation: "validateBackupConfig",
3261
+ driver,
3262
+ availableDrivers: Object.keys(BACKUP_DRIVERS),
3263
+ suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
3264
+ });
2629
3265
  }
2630
3266
  switch (driver) {
2631
3267
  case "filesystem":
2632
3268
  if (!config.path) {
2633
- throw new Error('FilesystemBackupDriver requires "path" configuration');
3269
+ throw new BackupError('FilesystemBackupDriver requires "path" configuration', {
3270
+ operation: "validateBackupConfig",
3271
+ driver: "filesystem",
3272
+ config,
3273
+ suggestion: 'Provide a "path" property in config: { path: "/path/to/backups" }'
3274
+ });
2634
3275
  }
2635
3276
  break;
2636
3277
  case "s3":
2637
3278
  break;
2638
3279
  case "multi":
2639
3280
  if (!Array.isArray(config.destinations) || config.destinations.length === 0) {
2640
- throw new Error('MultiBackupDriver requires non-empty "destinations" array');
3281
+ throw new BackupError('MultiBackupDriver requires non-empty "destinations" array', {
3282
+ operation: "validateBackupConfig",
3283
+ driver: "multi",
3284
+ config,
3285
+ suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }] }'
3286
+ });
2641
3287
  }
2642
3288
  config.destinations.forEach((dest, index) => {
2643
3289
  if (!dest.driver) {
2644
- throw new Error(`Destination ${index} must have a "driver" property`);
3290
+ throw new BackupError(`Destination ${index} must have a "driver" property`, {
3291
+ operation: "validateBackupConfig",
3292
+ driver: "multi",
3293
+ destinationIndex: index,
3294
+ destination: dest,
3295
+ suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} }'
3296
+ });
2645
3297
  }
2646
3298
  if (dest.driver !== "multi") {
2647
3299
  validateBackupConfig(dest.driver, dest.config || {});
@@ -3297,6 +3949,36 @@ class BackupPlugin extends Plugin {
3297
3949
  }
3298
3950
  }
3299
3951
 
3952
+ class CacheError extends S3dbError {
3953
+ constructor(message, details = {}) {
3954
+ const { driver = "unknown", operation = "unknown", resourceName, key, ...rest } = details;
3955
+ let description = details.description;
3956
+ if (!description) {
3957
+ description = `
3958
+ Cache Operation Error
3959
+
3960
+ Driver: ${driver}
3961
+ Operation: ${operation}
3962
+ ${resourceName ? `Resource: ${resourceName}` : ""}
3963
+ ${key ? `Key: ${key}` : ""}
3964
+
3965
+ Common causes:
3966
+ 1. Invalid cache key format
3967
+ 2. Cache driver not properly initialized
3968
+ 3. Resource not found or not cached
3969
+ 4. Memory limits exceeded
3970
+ 5. Filesystem permissions issues
3971
+
3972
+ Solution:
3973
+ Check cache configuration and ensure the cache driver is properly initialized.
3974
+
3975
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/cache.md
3976
+ `.trim();
3977
+ }
3978
+ super(message, { ...rest, driver, operation, resourceName, key, description });
3979
+ }
3980
+ }
3981
+
3300
3982
  class Cache extends EventEmitter {
3301
3983
  constructor(config = {}) {
3302
3984
  super();
@@ -3313,7 +3995,13 @@ class Cache extends EventEmitter {
3313
3995
  }
3314
3996
  validateKey(key) {
3315
3997
  if (key === null || key === void 0 || typeof key !== "string" || !key) {
3316
- throw new Error("Invalid key");
3998
+ throw new CacheError("Invalid cache key", {
3999
+ operation: "validateKey",
4000
+ driver: this.constructor.name,
4001
+ key,
4002
+ keyType: typeof key,
4003
+ suggestion: "Cache key must be a non-empty string"
4004
+ });
3317
4005
  }
3318
4006
  }
3319
4007
  // generic class methods
@@ -3400,7 +4088,11 @@ class ResourceReader extends EventEmitter {
3400
4088
  constructor({ resource, batchSize = 10, concurrency = 5 }) {
3401
4089
  super();
3402
4090
  if (!resource) {
3403
- throw new Error("Resource is required for ResourceReader");
4091
+ throw new StreamError("Resource is required for ResourceReader", {
4092
+ operation: "constructor",
4093
+ resource: resource?.name,
4094
+ suggestion: "Pass a valid Resource instance when creating ResourceReader"
4095
+ });
3404
4096
  }
3405
4097
  this.resource = resource;
3406
4098
  this.client = resource.client;
@@ -3524,7 +4216,10 @@ class ResourceWriter extends EventEmitter {
3524
4216
  function streamToString(stream) {
3525
4217
  return new Promise((resolve, reject) => {
3526
4218
  if (!stream) {
3527
- return reject(new Error("streamToString: stream is undefined"));
4219
+ return reject(new StreamError("Stream is undefined", {
4220
+ operation: "streamToString",
4221
+ suggestion: "Ensure a valid stream is passed to streamToString()"
4222
+ }));
3528
4223
  }
3529
4224
  const chunks = [];
3530
4225
  stream.on("data", (chunk) => chunks.push(chunk));
@@ -3605,6 +4300,24 @@ class MemoryCache extends Cache {
3605
4300
  this.cache = {};
3606
4301
  this.meta = {};
3607
4302
  this.maxSize = config.maxSize !== void 0 ? config.maxSize : 1e3;
4303
+ if (config.maxMemoryBytes && config.maxMemoryBytes > 0 && config.maxMemoryPercent && config.maxMemoryPercent > 0) {
4304
+ throw new Error(
4305
+ "[MemoryCache] Cannot use both maxMemoryBytes and maxMemoryPercent. Choose one: maxMemoryBytes (absolute) or maxMemoryPercent (0...1 fraction)."
4306
+ );
4307
+ }
4308
+ if (config.maxMemoryPercent && config.maxMemoryPercent > 0) {
4309
+ if (config.maxMemoryPercent > 1) {
4310
+ throw new Error(
4311
+ `[MemoryCache] maxMemoryPercent must be between 0 and 1 (e.g., 0.1 for 10%). Received: ${config.maxMemoryPercent}`
4312
+ );
4313
+ }
4314
+ const totalMemory = os$1.totalmem();
4315
+ this.maxMemoryBytes = Math.floor(totalMemory * config.maxMemoryPercent);
4316
+ this.maxMemoryPercent = config.maxMemoryPercent;
4317
+ } else {
4318
+ this.maxMemoryBytes = config.maxMemoryBytes !== void 0 ? config.maxMemoryBytes : 0;
4319
+ this.maxMemoryPercent = 0;
4320
+ }
3608
4321
  this.ttl = config.ttl !== void 0 ? config.ttl : 3e5;
3609
4322
  this.enableCompression = config.enableCompression !== void 0 ? config.enableCompression : false;
3610
4323
  this.compressionThreshold = config.compressionThreshold !== void 0 ? config.compressionThreshold : 1024;
@@ -3614,23 +4327,18 @@ class MemoryCache extends Cache {
3614
4327
  totalCompressedSize: 0,
3615
4328
  compressionRatio: 0
3616
4329
  };
4330
+ this.currentMemoryBytes = 0;
4331
+ this.evictedDueToMemory = 0;
3617
4332
  }
3618
4333
  async _set(key, data) {
3619
- if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
3620
- const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
3621
- if (oldestKey) {
3622
- delete this.cache[oldestKey];
3623
- delete this.meta[oldestKey];
3624
- }
3625
- }
3626
4334
  let finalData = data;
3627
4335
  let compressed = false;
3628
4336
  let originalSize = 0;
3629
4337
  let compressedSize = 0;
4338
+ const serialized = JSON.stringify(data);
4339
+ originalSize = Buffer.byteLength(serialized, "utf8");
3630
4340
  if (this.enableCompression) {
3631
4341
  try {
3632
- const serialized = JSON.stringify(data);
3633
- originalSize = Buffer.byteLength(serialized, "utf8");
3634
4342
  if (originalSize >= this.compressionThreshold) {
3635
4343
  const compressedBuffer = zlib.gzipSync(Buffer.from(serialized, "utf8"));
3636
4344
  finalData = {
@@ -3649,13 +4357,42 @@ class MemoryCache extends Cache {
3649
4357
  console.warn(`[MemoryCache] Compression failed for key '${key}':`, error.message);
3650
4358
  }
3651
4359
  }
4360
+ const itemSize = compressed ? compressedSize : originalSize;
4361
+ if (Object.prototype.hasOwnProperty.call(this.cache, key)) {
4362
+ const oldSize = this.meta[key]?.compressedSize || 0;
4363
+ this.currentMemoryBytes -= oldSize;
4364
+ }
4365
+ if (this.maxMemoryBytes > 0) {
4366
+ while (this.currentMemoryBytes + itemSize > this.maxMemoryBytes && Object.keys(this.cache).length > 0) {
4367
+ const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
4368
+ if (oldestKey) {
4369
+ const evictedSize = this.meta[oldestKey]?.compressedSize || 0;
4370
+ delete this.cache[oldestKey];
4371
+ delete this.meta[oldestKey];
4372
+ this.currentMemoryBytes -= evictedSize;
4373
+ this.evictedDueToMemory++;
4374
+ } else {
4375
+ break;
4376
+ }
4377
+ }
4378
+ }
4379
+ if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
4380
+ const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
4381
+ if (oldestKey) {
4382
+ const evictedSize = this.meta[oldestKey]?.compressedSize || 0;
4383
+ delete this.cache[oldestKey];
4384
+ delete this.meta[oldestKey];
4385
+ this.currentMemoryBytes -= evictedSize;
4386
+ }
4387
+ }
3652
4388
  this.cache[key] = finalData;
3653
4389
  this.meta[key] = {
3654
4390
  ts: Date.now(),
3655
4391
  compressed,
3656
4392
  originalSize,
3657
- compressedSize: compressed ? compressedSize : originalSize
4393
+ compressedSize: itemSize
3658
4394
  };
4395
+ this.currentMemoryBytes += itemSize;
3659
4396
  return data;
3660
4397
  }
3661
4398
  async _get(key) {
@@ -3663,7 +4400,9 @@ class MemoryCache extends Cache {
3663
4400
  if (this.ttl > 0) {
3664
4401
  const now = Date.now();
3665
4402
  const meta = this.meta[key];
3666
- if (meta && now - meta.ts > this.ttl * 1e3) {
4403
+ if (meta && now - meta.ts > this.ttl) {
4404
+ const itemSize = meta.compressedSize || 0;
4405
+ this.currentMemoryBytes -= itemSize;
3667
4406
  delete this.cache[key];
3668
4407
  delete this.meta[key];
3669
4408
  return null;
@@ -3685,6 +4424,10 @@ class MemoryCache extends Cache {
3685
4424
  return rawData;
3686
4425
  }
3687
4426
  async _del(key) {
4427
+ if (Object.prototype.hasOwnProperty.call(this.cache, key)) {
4428
+ const itemSize = this.meta[key]?.compressedSize || 0;
4429
+ this.currentMemoryBytes -= itemSize;
4430
+ }
3688
4431
  delete this.cache[key];
3689
4432
  delete this.meta[key];
3690
4433
  return true;
@@ -3693,10 +4436,13 @@ class MemoryCache extends Cache {
3693
4436
  if (!prefix) {
3694
4437
  this.cache = {};
3695
4438
  this.meta = {};
4439
+ this.currentMemoryBytes = 0;
3696
4440
  return true;
3697
4441
  }
3698
4442
  for (const key of Object.keys(this.cache)) {
3699
4443
  if (key.startsWith(prefix)) {
4444
+ const itemSize = this.meta[key]?.compressedSize || 0;
4445
+ this.currentMemoryBytes -= itemSize;
3700
4446
  delete this.cache[key];
3701
4447
  delete this.meta[key];
3702
4448
  }
@@ -3734,6 +4480,53 @@ class MemoryCache extends Cache {
3734
4480
  }
3735
4481
  };
3736
4482
  }
4483
+ /**
4484
+ * Get memory usage statistics
4485
+ * @returns {Object} Memory stats including current usage, limits, and eviction counts
4486
+ */
4487
+ getMemoryStats() {
4488
+ const totalItems = Object.keys(this.cache).length;
4489
+ const memoryUsagePercent = this.maxMemoryBytes > 0 ? (this.currentMemoryBytes / this.maxMemoryBytes * 100).toFixed(2) : 0;
4490
+ const systemMemory = {
4491
+ total: os$1.totalmem(),
4492
+ free: os$1.freemem(),
4493
+ used: os$1.totalmem() - os$1.freemem()
4494
+ };
4495
+ const cachePercentOfTotal = systemMemory.total > 0 ? (this.currentMemoryBytes / systemMemory.total * 100).toFixed(2) : 0;
4496
+ return {
4497
+ currentMemoryBytes: this.currentMemoryBytes,
4498
+ maxMemoryBytes: this.maxMemoryBytes,
4499
+ maxMemoryPercent: this.maxMemoryPercent,
4500
+ memoryUsagePercent: parseFloat(memoryUsagePercent),
4501
+ cachePercentOfSystemMemory: parseFloat(cachePercentOfTotal),
4502
+ totalItems,
4503
+ maxSize: this.maxSize,
4504
+ evictedDueToMemory: this.evictedDueToMemory,
4505
+ averageItemSize: totalItems > 0 ? Math.round(this.currentMemoryBytes / totalItems) : 0,
4506
+ memoryUsage: {
4507
+ current: this._formatBytes(this.currentMemoryBytes),
4508
+ max: this.maxMemoryBytes > 0 ? this._formatBytes(this.maxMemoryBytes) : "unlimited",
4509
+ available: this.maxMemoryBytes > 0 ? this._formatBytes(this.maxMemoryBytes - this.currentMemoryBytes) : "unlimited"
4510
+ },
4511
+ systemMemory: {
4512
+ total: this._formatBytes(systemMemory.total),
4513
+ free: this._formatBytes(systemMemory.free),
4514
+ used: this._formatBytes(systemMemory.used),
4515
+ cachePercent: `${cachePercentOfTotal}%`
4516
+ }
4517
+ };
4518
+ }
4519
+ /**
4520
+ * Format bytes to human-readable format
4521
+ * @private
4522
+ */
4523
+ _formatBytes(bytes) {
4524
+ if (bytes === 0) return "0 B";
4525
+ const k = 1024;
4526
+ const sizes = ["B", "KB", "MB", "GB"];
4527
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
4528
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
4529
+ }
3737
4530
  }
3738
4531
 
3739
4532
  class FilesystemCache extends Cache {
@@ -4563,8 +5356,10 @@ class CachePlugin extends Plugin {
4563
5356
  config: {
4564
5357
  ttl: options.ttl,
4565
5358
  maxSize: options.maxSize,
5359
+ maxMemoryBytes: options.maxMemoryBytes,
5360
+ maxMemoryPercent: options.maxMemoryPercent,
4566
5361
  ...options.config
4567
- // Driver-specific config (can override ttl/maxSize)
5362
+ // Driver-specific config (can override ttl/maxSize/maxMemoryBytes/maxMemoryPercent)
4568
5363
  },
4569
5364
  // Resource filtering
4570
5365
  include: options.include || null,
@@ -4918,7 +5713,13 @@ class CachePlugin extends Plugin {
4918
5713
  async warmCache(resourceName, options = {}) {
4919
5714
  const resource = this.database.resources[resourceName];
4920
5715
  if (!resource) {
4921
- throw new Error(`Resource '${resourceName}' not found`);
5716
+ throw new CacheError("Resource not found for cache warming", {
5717
+ operation: "warmCache",
5718
+ driver: this.driver?.constructor.name,
5719
+ resourceName,
5720
+ availableResources: Object.keys(this.database.resources),
5721
+ suggestion: "Check resource name spelling or ensure resource has been created"
5722
+ });
4922
5723
  }
4923
5724
  const { includePartitions = true, sampleSize = 100 } = options;
4924
5725
  if (this.driver instanceof PartitionAwareFilesystemCache && resource.warmPartitionCache) {
@@ -7023,6 +7824,80 @@ async function getLastNMonths(resourceName, field, months = 12, options, fieldHa
7023
7824
  }
7024
7825
  return data;
7025
7826
  }
7827
+ async function getRawEvents(resourceName, field, options, fieldHandlers) {
7828
+ const resourceHandlers = fieldHandlers.get(resourceName);
7829
+ if (!resourceHandlers) {
7830
+ throw new Error(`No eventual consistency configured for resource: ${resourceName}`);
7831
+ }
7832
+ const handler = resourceHandlers.get(field);
7833
+ if (!handler) {
7834
+ throw new Error(`No eventual consistency configured for field: ${resourceName}.${field}`);
7835
+ }
7836
+ if (!handler.transactionResource) {
7837
+ throw new Error("Transaction resource not initialized");
7838
+ }
7839
+ const {
7840
+ recordId,
7841
+ startDate,
7842
+ endDate,
7843
+ cohortDate,
7844
+ cohortHour,
7845
+ cohortMonth,
7846
+ applied,
7847
+ operation,
7848
+ limit
7849
+ } = options;
7850
+ const query = {};
7851
+ if (recordId !== void 0) {
7852
+ query.originalId = recordId;
7853
+ }
7854
+ if (applied !== void 0) {
7855
+ query.applied = applied;
7856
+ }
7857
+ const [ok, err, allTransactions] = await tryFn(
7858
+ () => handler.transactionResource.query(query)
7859
+ );
7860
+ if (!ok || !allTransactions) {
7861
+ return [];
7862
+ }
7863
+ let filtered = allTransactions;
7864
+ if (operation !== void 0) {
7865
+ filtered = filtered.filter((t) => t.operation === operation);
7866
+ }
7867
+ if (cohortDate) {
7868
+ filtered = filtered.filter((t) => t.cohortDate === cohortDate);
7869
+ }
7870
+ if (cohortHour) {
7871
+ filtered = filtered.filter((t) => t.cohortHour === cohortHour);
7872
+ }
7873
+ if (cohortMonth) {
7874
+ filtered = filtered.filter((t) => t.cohortMonth === cohortMonth);
7875
+ }
7876
+ if (startDate && endDate) {
7877
+ const isHourly = startDate.length > 10;
7878
+ const cohortField = isHourly ? "cohortHour" : "cohortDate";
7879
+ filtered = filtered.filter(
7880
+ (t) => t[cohortField] && t[cohortField] >= startDate && t[cohortField] <= endDate
7881
+ );
7882
+ } else if (startDate) {
7883
+ const isHourly = startDate.length > 10;
7884
+ const cohortField = isHourly ? "cohortHour" : "cohortDate";
7885
+ filtered = filtered.filter((t) => t[cohortField] && t[cohortField] >= startDate);
7886
+ } else if (endDate) {
7887
+ const isHourly = endDate.length > 10;
7888
+ const cohortField = isHourly ? "cohortHour" : "cohortDate";
7889
+ filtered = filtered.filter((t) => t[cohortField] && t[cohortField] <= endDate);
7890
+ }
7891
+ filtered.sort((a, b) => {
7892
+ const aTime = new Date(a.timestamp || a.createdAt).getTime();
7893
+ const bTime = new Date(b.timestamp || b.createdAt).getTime();
7894
+ return bTime - aTime;
7895
+ });
7896
+ if (limit && limit > 0) {
7897
+ filtered = filtered.slice(0, limit);
7898
+ }
7899
+ return filtered;
7900
+ }
7026
7901
 
7027
7902
  function addHelperMethods(resource, plugin, config) {
7028
7903
  resource.set = async (id, field, value) => {
@@ -7780,6 +8655,214 @@ class EventualConsistencyPlugin extends Plugin {
7780
8655
  async getLastNMonths(resourceName, field, months = 12, options = {}) {
7781
8656
  return await getLastNMonths(resourceName, field, months, options, this.fieldHandlers);
7782
8657
  }
8658
+ /**
8659
+ * Get raw transaction events for custom aggregation
8660
+ *
8661
+ * This method provides direct access to the underlying transaction events,
8662
+ * allowing developers to perform custom aggregations beyond the pre-built analytics.
8663
+ * Useful for complex queries, custom metrics, or when you need the raw event data.
8664
+ *
8665
+ * @param {string} resourceName - Resource name
8666
+ * @param {string} field - Field name
8667
+ * @param {Object} options - Query options
8668
+ * @param {string} options.recordId - Filter by specific record ID
8669
+ * @param {string} options.startDate - Start date filter (YYYY-MM-DD or YYYY-MM-DDTHH)
8670
+ * @param {string} options.endDate - End date filter (YYYY-MM-DD or YYYY-MM-DDTHH)
8671
+ * @param {string} options.cohortDate - Filter by cohort date (YYYY-MM-DD)
8672
+ * @param {string} options.cohortHour - Filter by cohort hour (YYYY-MM-DDTHH)
8673
+ * @param {string} options.cohortMonth - Filter by cohort month (YYYY-MM)
8674
+ * @param {boolean} options.applied - Filter by applied status (true/false/undefined for both)
8675
+ * @param {string} options.operation - Filter by operation type ('add', 'sub', 'set')
8676
+ * @param {number} options.limit - Maximum number of events to return
8677
+ * @returns {Promise<Array>} Raw transaction events
8678
+ *
8679
+ * @example
8680
+ * // Get all events for a specific record
8681
+ * const events = await plugin.getRawEvents('wallets', 'balance', {
8682
+ * recordId: 'wallet1'
8683
+ * });
8684
+ *
8685
+ * @example
8686
+ * // Get events for a specific time range
8687
+ * const events = await plugin.getRawEvents('wallets', 'balance', {
8688
+ * startDate: '2025-10-01',
8689
+ * endDate: '2025-10-31'
8690
+ * });
8691
+ *
8692
+ * @example
8693
+ * // Get only pending (unapplied) transactions
8694
+ * const pending = await plugin.getRawEvents('wallets', 'balance', {
8695
+ * applied: false
8696
+ * });
8697
+ */
8698
+ async getRawEvents(resourceName, field, options = {}) {
8699
+ return await getRawEvents(resourceName, field, options, this.fieldHandlers);
8700
+ }
8701
+ /**
8702
+ * Get diagnostics information about the plugin state
8703
+ *
8704
+ * This method provides comprehensive diagnostic information about the EventualConsistencyPlugin,
8705
+ * including configured resources, field handlers, timers, and overall health status.
8706
+ * Useful for debugging initialization issues, configuration problems, or runtime errors.
8707
+ *
8708
+ * @param {Object} options - Diagnostic options
8709
+ * @param {string} options.resourceName - Optional: limit diagnostics to specific resource
8710
+ * @param {string} options.field - Optional: limit diagnostics to specific field
8711
+ * @param {boolean} options.includeStats - Include transaction statistics (default: false)
8712
+ * @returns {Promise<Object>} Diagnostic information
8713
+ *
8714
+ * @example
8715
+ * // Get overall plugin diagnostics
8716
+ * const diagnostics = await plugin.getDiagnostics();
8717
+ * console.log(diagnostics);
8718
+ *
8719
+ * @example
8720
+ * // Get diagnostics for specific resource/field with stats
8721
+ * const diagnostics = await plugin.getDiagnostics({
8722
+ * resourceName: 'wallets',
8723
+ * field: 'balance',
8724
+ * includeStats: true
8725
+ * });
8726
+ */
8727
+ async getDiagnostics(options = {}) {
8728
+ const { resourceName, field, includeStats = false } = options;
8729
+ const diagnostics = {
8730
+ plugin: {
8731
+ name: "EventualConsistencyPlugin",
8732
+ initialized: this.database !== null && this.database !== void 0,
8733
+ verbose: this.config.verbose || false,
8734
+ timezone: this.config.cohort?.timezone || "UTC",
8735
+ consolidation: {
8736
+ mode: this.config.consolidation?.mode || "timer",
8737
+ interval: this.config.consolidation?.interval || 6e4,
8738
+ batchSize: this.config.consolidation?.batchSize || 100
8739
+ },
8740
+ garbageCollection: {
8741
+ enabled: this.config.garbageCollection?.enabled !== false,
8742
+ retentionDays: this.config.garbageCollection?.retentionDays || 30,
8743
+ interval: this.config.garbageCollection?.interval || 36e5
8744
+ }
8745
+ },
8746
+ resources: [],
8747
+ errors: [],
8748
+ warnings: []
8749
+ };
8750
+ for (const [resName, resourceHandlers] of this.fieldHandlers.entries()) {
8751
+ if (resourceName && resName !== resourceName) {
8752
+ continue;
8753
+ }
8754
+ const resourceDiag = {
8755
+ name: resName,
8756
+ fields: []
8757
+ };
8758
+ for (const [fieldName, handler] of resourceHandlers.entries()) {
8759
+ if (field && fieldName !== field) {
8760
+ continue;
8761
+ }
8762
+ const fieldDiag = {
8763
+ name: fieldName,
8764
+ type: handler.type || "counter",
8765
+ analyticsEnabled: handler.analyticsResource !== null && handler.analyticsResource !== void 0,
8766
+ resources: {
8767
+ transaction: handler.transactionResource?.name || null,
8768
+ target: handler.targetResource?.name || null,
8769
+ analytics: handler.analyticsResource?.name || null
8770
+ },
8771
+ timers: {
8772
+ consolidation: handler.consolidationTimer !== null && handler.consolidationTimer !== void 0,
8773
+ garbageCollection: handler.garbageCollectionTimer !== null && handler.garbageCollectionTimer !== void 0
8774
+ }
8775
+ };
8776
+ if (!handler.transactionResource) {
8777
+ diagnostics.errors.push({
8778
+ resource: resName,
8779
+ field: fieldName,
8780
+ issue: "Missing transaction resource",
8781
+ suggestion: "Ensure plugin is installed and resources are created after plugin installation"
8782
+ });
8783
+ }
8784
+ if (!handler.targetResource) {
8785
+ diagnostics.warnings.push({
8786
+ resource: resName,
8787
+ field: fieldName,
8788
+ issue: "Missing target resource",
8789
+ suggestion: "Target resource may not have been created yet"
8790
+ });
8791
+ }
8792
+ if (handler.analyticsResource && !handler.analyticsResource.name) {
8793
+ diagnostics.errors.push({
8794
+ resource: resName,
8795
+ field: fieldName,
8796
+ issue: "Invalid analytics resource",
8797
+ suggestion: "Analytics resource exists but has no name - possible initialization failure"
8798
+ });
8799
+ }
8800
+ if (includeStats && handler.transactionResource) {
8801
+ try {
8802
+ const [okPending, errPending, pendingTxns] = await handler.transactionResource.query({ applied: false }).catch(() => [false, null, []]);
8803
+ const [okApplied, errApplied, appliedTxns] = await handler.transactionResource.query({ applied: true }).catch(() => [false, null, []]);
8804
+ fieldDiag.stats = {
8805
+ pendingTransactions: okPending ? pendingTxns?.length || 0 : "error",
8806
+ appliedTransactions: okApplied ? appliedTxns?.length || 0 : "error",
8807
+ totalTransactions: okPending && okApplied ? (pendingTxns?.length || 0) + (appliedTxns?.length || 0) : "error"
8808
+ };
8809
+ if (handler.analyticsResource) {
8810
+ const [okAnalytics, errAnalytics, analyticsRecords] = await handler.analyticsResource.list().catch(() => [false, null, []]);
8811
+ fieldDiag.stats.analyticsRecords = okAnalytics ? analyticsRecords?.length || 0 : "error";
8812
+ }
8813
+ } catch (error) {
8814
+ diagnostics.warnings.push({
8815
+ resource: resName,
8816
+ field: fieldName,
8817
+ issue: "Failed to fetch statistics",
8818
+ error: error.message
8819
+ });
8820
+ }
8821
+ }
8822
+ resourceDiag.fields.push(fieldDiag);
8823
+ }
8824
+ if (resourceDiag.fields.length > 0) {
8825
+ diagnostics.resources.push(resourceDiag);
8826
+ }
8827
+ }
8828
+ diagnostics.health = {
8829
+ status: diagnostics.errors.length === 0 ? diagnostics.warnings.length === 0 ? "healthy" : "warning" : "error",
8830
+ totalResources: diagnostics.resources.length,
8831
+ totalFields: diagnostics.resources.reduce((sum, r) => sum + r.fields.length, 0),
8832
+ errorCount: diagnostics.errors.length,
8833
+ warningCount: diagnostics.warnings.length
8834
+ };
8835
+ return diagnostics;
8836
+ }
8837
+ }
8838
+
8839
+ class FulltextError extends S3dbError {
8840
+ constructor(message, details = {}) {
8841
+ const { resourceName, query, operation = "unknown", ...rest } = details;
8842
+ let description = details.description;
8843
+ if (!description) {
8844
+ description = `
8845
+ Fulltext Search Operation Error
8846
+
8847
+ Operation: ${operation}
8848
+ ${resourceName ? `Resource: ${resourceName}` : ""}
8849
+ ${query ? `Query: ${query}` : ""}
8850
+
8851
+ Common causes:
8852
+ 1. Resource not indexed for fulltext search
8853
+ 2. Invalid query syntax
8854
+ 3. Index not built yet
8855
+ 4. Search configuration missing
8856
+ 5. Field not indexed
8857
+
8858
+ Solution:
8859
+ Ensure resource is configured for fulltext search and index is built.
8860
+
8861
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/fulltext.md
8862
+ `.trim();
8863
+ }
8864
+ super(message, { ...rest, resourceName, query, operation, description });
8865
+ }
7783
8866
  }
7784
8867
 
7785
8868
  class FullTextPlugin extends Plugin {
@@ -8088,7 +9171,13 @@ class FullTextPlugin extends Plugin {
8088
9171
  }
8089
9172
  const resource = this.database.resources[resourceName];
8090
9173
  if (!resource) {
8091
- throw new Error(`Resource '${resourceName}' not found`);
9174
+ throw new FulltextError(`Resource '${resourceName}' not found`, {
9175
+ operation: "searchRecords",
9176
+ resourceName,
9177
+ query,
9178
+ availableResources: Object.keys(this.database.resources),
9179
+ suggestion: "Check resource name or ensure resource is created before searching"
9180
+ });
8092
9181
  }
8093
9182
  const recordIds = searchResults.map((result2) => result2.recordId);
8094
9183
  const records = await resource.getMany(recordIds);
@@ -8105,7 +9194,12 @@ class FullTextPlugin extends Plugin {
8105
9194
  async rebuildIndex(resourceName) {
8106
9195
  const resource = this.database.resources[resourceName];
8107
9196
  if (!resource) {
8108
- throw new Error(`Resource '${resourceName}' not found`);
9197
+ throw new FulltextError(`Resource '${resourceName}' not found`, {
9198
+ operation: "rebuildIndex",
9199
+ resourceName,
9200
+ availableResources: Object.keys(this.database.resources),
9201
+ suggestion: "Check resource name or ensure resource is created before rebuilding index"
9202
+ });
8109
9203
  }
8110
9204
  for (const [key] of this.indexes.entries()) {
8111
9205
  if (key.startsWith(`${resourceName}:`)) {
@@ -8890,6 +9984,35 @@ function createConsumer(driver, config) {
8890
9984
  return new ConsumerClass(config);
8891
9985
  }
8892
9986
 
9987
+ class QueueError extends S3dbError {
9988
+ constructor(message, details = {}) {
9989
+ const { queueName, operation = "unknown", messageId, ...rest } = details;
9990
+ let description = details.description;
9991
+ if (!description) {
9992
+ description = `
9993
+ Queue Operation Error
9994
+
9995
+ Operation: ${operation}
9996
+ ${queueName ? `Queue: ${queueName}` : ""}
9997
+ ${messageId ? `Message ID: ${messageId}` : ""}
9998
+
9999
+ Common causes:
10000
+ 1. Queue not properly configured
10001
+ 2. Message handler not registered
10002
+ 3. Queue resource not found
10003
+ 4. SQS/RabbitMQ connection failed
10004
+ 5. Message processing timeout
10005
+
10006
+ Solution:
10007
+ Check queue configuration and message handler registration.
10008
+
10009
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue.md
10010
+ `.trim();
10011
+ }
10012
+ super(message, { ...rest, queueName, operation, messageId, description });
10013
+ }
10014
+ }
10015
+
8893
10016
  class QueueConsumerPlugin extends Plugin {
8894
10017
  constructor(options = {}) {
8895
10018
  super(options);
@@ -8950,13 +10073,32 @@ class QueueConsumerPlugin extends Plugin {
8950
10073
  let action = body.action || msg.action;
8951
10074
  let data = body.data || msg.data;
8952
10075
  if (!resource) {
8953
- throw new Error("QueueConsumerPlugin: resource not found in message");
10076
+ throw new QueueError("Resource not found in message", {
10077
+ operation: "handleMessage",
10078
+ queueName: configuredResource,
10079
+ messageBody: body,
10080
+ suggestion: 'Ensure message includes a "resource" field specifying the target resource name'
10081
+ });
8954
10082
  }
8955
10083
  if (!action) {
8956
- throw new Error("QueueConsumerPlugin: action not found in message");
10084
+ throw new QueueError("Action not found in message", {
10085
+ operation: "handleMessage",
10086
+ queueName: configuredResource,
10087
+ resource,
10088
+ messageBody: body,
10089
+ suggestion: 'Ensure message includes an "action" field (insert, update, or delete)'
10090
+ });
8957
10091
  }
8958
10092
  const resourceObj = this.database.resources[resource];
8959
- if (!resourceObj) throw new Error(`QueueConsumerPlugin: resource '${resource}' not found`);
10093
+ if (!resourceObj) {
10094
+ throw new QueueError(`Resource '${resource}' not found`, {
10095
+ operation: "handleMessage",
10096
+ queueName: configuredResource,
10097
+ resource,
10098
+ availableResources: Object.keys(this.database.resources),
10099
+ suggestion: "Check resource name or ensure resource is created before consuming messages"
10100
+ });
10101
+ }
8960
10102
  let result;
8961
10103
  const [ok, err, res] = await tryFn(async () => {
8962
10104
  if (action === "insert") {
@@ -8967,7 +10109,14 @@ class QueueConsumerPlugin extends Plugin {
8967
10109
  } else if (action === "delete") {
8968
10110
  result = await resourceObj.delete(data.id);
8969
10111
  } else {
8970
- throw new Error(`QueueConsumerPlugin: unsupported action '${action}'`);
10112
+ throw new QueueError(`Unsupported action '${action}'`, {
10113
+ operation: "handleMessage",
10114
+ queueName: configuredResource,
10115
+ resource,
10116
+ action,
10117
+ supportedActions: ["insert", "update", "delete"],
10118
+ suggestion: "Use one of the supported actions: insert, update, or delete"
10119
+ });
8971
10120
  }
8972
10121
  return result;
8973
10122
  });
@@ -8980,6 +10129,35 @@ class QueueConsumerPlugin extends Plugin {
8980
10129
  }
8981
10130
  }
8982
10131
 
10132
+ class ReplicationError extends S3dbError {
10133
+ constructor(message, details = {}) {
10134
+ const { replicatorClass = "unknown", operation = "unknown", resourceName, ...rest } = details;
10135
+ let description = details.description;
10136
+ if (!description) {
10137
+ description = `
10138
+ Replication Operation Error
10139
+
10140
+ Replicator: ${replicatorClass}
10141
+ Operation: ${operation}
10142
+ ${resourceName ? `Resource: ${resourceName}` : ""}
10143
+
10144
+ Common causes:
10145
+ 1. Invalid replicator configuration
10146
+ 2. Target system not accessible
10147
+ 3. Resource not configured for replication
10148
+ 4. Invalid operation type
10149
+ 5. Transformation function errors
10150
+
10151
+ Solution:
10152
+ Check replicator configuration and ensure target system is accessible.
10153
+
10154
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md
10155
+ `.trim();
10156
+ }
10157
+ super(message, { ...rest, replicatorClass, operation, resourceName, description });
10158
+ }
10159
+ }
10160
+
8983
10161
  class BaseReplicator extends EventEmitter {
8984
10162
  constructor(config = {}) {
8985
10163
  super();
@@ -9005,7 +10183,12 @@ class BaseReplicator extends EventEmitter {
9005
10183
  * @returns {Promise<Object>} replicator result
9006
10184
  */
9007
10185
  async replicate(resourceName, operation, data, id) {
9008
- throw new Error(`replicate() method must be implemented by ${this.name}`);
10186
+ throw new ReplicationError("replicate() method must be implemented by subclass", {
10187
+ operation: "replicate",
10188
+ replicatorClass: this.name,
10189
+ resourceName,
10190
+ suggestion: "Extend BaseReplicator and implement the replicate() method"
10191
+ });
9009
10192
  }
9010
10193
  /**
9011
10194
  * Replicate multiple records in batch
@@ -9014,14 +10197,24 @@ class BaseReplicator extends EventEmitter {
9014
10197
  * @returns {Promise<Object>} Batch replicator result
9015
10198
  */
9016
10199
  async replicateBatch(resourceName, records) {
9017
- throw new Error(`replicateBatch() method must be implemented by ${this.name}`);
10200
+ throw new ReplicationError("replicateBatch() method must be implemented by subclass", {
10201
+ operation: "replicateBatch",
10202
+ replicatorClass: this.name,
10203
+ resourceName,
10204
+ batchSize: records?.length,
10205
+ suggestion: "Extend BaseReplicator and implement the replicateBatch() method"
10206
+ });
9018
10207
  }
9019
10208
  /**
9020
10209
  * Test the connection to the target
9021
10210
  * @returns {Promise<boolean>} True if connection is successful
9022
10211
  */
9023
10212
  async testConnection() {
9024
- throw new Error(`testConnection() method must be implemented by ${this.name}`);
10213
+ throw new ReplicationError("testConnection() method must be implemented by subclass", {
10214
+ operation: "testConnection",
10215
+ replicatorClass: this.name,
10216
+ suggestion: "Extend BaseReplicator and implement the testConnection() method"
10217
+ });
9025
10218
  }
9026
10219
  /**
9027
10220
  * Get replicator status and statistics
@@ -10193,7 +11386,17 @@ class Client extends EventEmitter {
10193
11386
  });
10194
11387
  this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
10195
11388
  if (errors.length > 0) {
10196
- throw new Error("Some objects could not be moved");
11389
+ throw new UnknownError("Some objects could not be moved", {
11390
+ bucket: this.config.bucket,
11391
+ operation: "moveAllObjects",
11392
+ prefixFrom,
11393
+ prefixTo,
11394
+ totalKeys: keys.length,
11395
+ failedCount: errors.length,
11396
+ successCount: results.length,
11397
+ errors: errors.map((e) => ({ message: e.message, raw: e.raw })),
11398
+ suggestion: "Check S3 permissions and retry failed objects individually"
11399
+ });
10197
11400
  }
10198
11401
  return results;
10199
11402
  }
@@ -10907,7 +12110,14 @@ async function handleInsert$4({ resource, data, mappedData, originalData }) {
10907
12110
  }
10908
12111
  });
10909
12112
  if (totalSize > effectiveLimit) {
10910
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12113
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on insert", {
12114
+ totalSize,
12115
+ effectiveLimit,
12116
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12117
+ excess: totalSize - effectiveLimit,
12118
+ resourceName: resource.name,
12119
+ operation: "insert"
12120
+ });
10911
12121
  }
10912
12122
  return { mappedData, body: "" };
10913
12123
  }
@@ -10922,7 +12132,15 @@ async function handleUpdate$4({ resource, id, data, mappedData, originalData })
10922
12132
  }
10923
12133
  });
10924
12134
  if (totalSize > effectiveLimit) {
10925
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12135
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on update", {
12136
+ totalSize,
12137
+ effectiveLimit,
12138
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12139
+ excess: totalSize - effectiveLimit,
12140
+ resourceName: resource.name,
12141
+ operation: "update",
12142
+ id
12143
+ });
10926
12144
  }
10927
12145
  return { mappedData, body: JSON.stringify(mappedData) };
10928
12146
  }
@@ -10937,7 +12155,15 @@ async function handleUpsert$4({ resource, id, data, mappedData }) {
10937
12155
  }
10938
12156
  });
10939
12157
  if (totalSize > effectiveLimit) {
10940
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12158
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on upsert", {
12159
+ totalSize,
12160
+ effectiveLimit,
12161
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12162
+ excess: totalSize - effectiveLimit,
12163
+ resourceName: resource.name,
12164
+ operation: "upsert",
12165
+ id
12166
+ });
10941
12167
  }
10942
12168
  return { mappedData, body: "" };
10943
12169
  }
@@ -11279,7 +12505,11 @@ const behaviors = {
11279
12505
  function getBehavior(behaviorName) {
11280
12506
  const behavior = behaviors[behaviorName];
11281
12507
  if (!behavior) {
11282
- throw new Error(`Unknown behavior: ${behaviorName}. Available behaviors: ${Object.keys(behaviors).join(", ")}`);
12508
+ throw new BehaviorError(`Unknown behavior: ${behaviorName}`, {
12509
+ behavior: behaviorName,
12510
+ availableBehaviors: Object.keys(behaviors),
12511
+ operation: "getBehavior"
12512
+ });
11283
12513
  }
11284
12514
  return behavior;
11285
12515
  }
@@ -11401,6 +12631,7 @@ ${errorDetails}`,
11401
12631
  idGenerator: customIdGenerator,
11402
12632
  idSize = 22,
11403
12633
  versioningEnabled = false,
12634
+ strictValidation = true,
11404
12635
  events = {},
11405
12636
  asyncEvents = true,
11406
12637
  asyncPartitions = true,
@@ -11414,6 +12645,7 @@ ${errorDetails}`,
11414
12645
  this.parallelism = parallelism;
11415
12646
  this.passphrase = passphrase ?? "secret";
11416
12647
  this.versioningEnabled = versioningEnabled;
12648
+ this.strictValidation = strictValidation;
11417
12649
  this.setAsyncMode(asyncEvents);
11418
12650
  this.idGenerator = this.configureIdGenerator(customIdGenerator, idSize);
11419
12651
  if (typeof customIdGenerator === "number" && customIdGenerator > 0) {
@@ -11661,9 +12893,12 @@ ${errorDetails}`,
11661
12893
  }
11662
12894
  /**
11663
12895
  * Validate that all partition fields exist in current resource attributes
11664
- * @throws {Error} If partition fields don't exist in current schema
12896
+ * @throws {Error} If partition fields don't exist in current schema (only when strictValidation is true)
11665
12897
  */
11666
12898
  validatePartitions() {
12899
+ if (!this.strictValidation) {
12900
+ return;
12901
+ }
11667
12902
  if (!this.config.partitions) {
11668
12903
  return;
11669
12904
  }
@@ -13798,7 +15033,7 @@ class Database extends EventEmitter {
13798
15033
  this.id = idGenerator(7);
13799
15034
  this.version = "1";
13800
15035
  this.s3dbVersion = (() => {
13801
- const [ok, err, version] = tryFn(() => true ? "11.2.2" : "latest");
15036
+ const [ok, err, version] = tryFn(() => true ? "11.2.4" : "latest");
13802
15037
  return ok ? version : "latest";
13803
15038
  })();
13804
15039
  this.resources = {};
@@ -13813,6 +15048,7 @@ class Database extends EventEmitter {
13813
15048
  this.passphrase = options.passphrase || "secret";
13814
15049
  this.versioningEnabled = options.versioningEnabled || false;
13815
15050
  this.persistHooks = options.persistHooks || false;
15051
+ this.strictValidation = options.strictValidation !== false;
13816
15052
  this._initHooks();
13817
15053
  let connectionString = options.connectionString;
13818
15054
  if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
@@ -13934,6 +15170,7 @@ class Database extends EventEmitter {
13934
15170
  asyncEvents: versionData.asyncEvents !== void 0 ? versionData.asyncEvents : true,
13935
15171
  hooks: this.persistHooks ? this._deserializeHooks(versionData.hooks || {}) : versionData.hooks || {},
13936
15172
  versioningEnabled: this.versioningEnabled,
15173
+ strictValidation: this.strictValidation,
13937
15174
  map: versionData.map,
13938
15175
  idGenerator: restoredIdGenerator,
13939
15176
  idSize: restoredIdSize
@@ -14141,7 +15378,12 @@ class Database extends EventEmitter {
14141
15378
  const pluginName = name.toLowerCase().replace("plugin", "");
14142
15379
  const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
14143
15380
  if (!plugin) {
14144
- throw new Error(`Plugin '${name}' not found`);
15381
+ throw new DatabaseError(`Plugin '${name}' not found`, {
15382
+ operation: "uninstallPlugin",
15383
+ pluginName: name,
15384
+ availablePlugins: Object.keys(this.pluginRegistry),
15385
+ suggestion: "Check plugin name or list available plugins using Object.keys(db.pluginRegistry)"
15386
+ });
14145
15387
  }
14146
15388
  if (plugin.stop) {
14147
15389
  await plugin.stop();
@@ -14595,6 +15837,7 @@ class Database extends EventEmitter {
14595
15837
  autoDecrypt: config.autoDecrypt !== void 0 ? config.autoDecrypt : true,
14596
15838
  hooks: hooks || {},
14597
15839
  versioningEnabled: this.versioningEnabled,
15840
+ strictValidation: this.strictValidation,
14598
15841
  map: config.map,
14599
15842
  idGenerator: config.idGenerator,
14600
15843
  idSize: config.idSize,
@@ -14773,10 +16016,20 @@ class Database extends EventEmitter {
14773
16016
  addHook(event, fn) {
14774
16017
  if (!this._hooks) this._initHooks();
14775
16018
  if (!this._hooks.has(event)) {
14776
- throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(", ")}`);
16019
+ throw new DatabaseError(`Unknown hook event: ${event}`, {
16020
+ operation: "addHook",
16021
+ invalidEvent: event,
16022
+ availableEvents: this._hookEvents,
16023
+ suggestion: `Use one of the available hook events: ${this._hookEvents.join(", ")}`
16024
+ });
14777
16025
  }
14778
16026
  if (typeof fn !== "function") {
14779
- throw new Error("Hook function must be a function");
16027
+ throw new DatabaseError("Hook function must be a function", {
16028
+ operation: "addHook",
16029
+ event,
16030
+ receivedType: typeof fn,
16031
+ suggestion: "Provide a function that will be called when the hook event occurs"
16032
+ });
14780
16033
  }
14781
16034
  this._hooks.get(event).push(fn);
14782
16035
  }
@@ -14914,7 +16167,11 @@ class S3dbReplicator extends BaseReplicator {
14914
16167
  this.targetDatabase = new S3db(targetConfig);
14915
16168
  await this.targetDatabase.connect();
14916
16169
  } else {
14917
- throw new Error("S3dbReplicator: No client or connectionString provided");
16170
+ throw new ReplicationError("S3dbReplicator requires client or connectionString", {
16171
+ operation: "initialize",
16172
+ replicatorClass: "S3dbReplicator",
16173
+ suggestion: 'Provide either a client instance or connectionString in config: { client: db } or { connectionString: "s3://..." }'
16174
+ });
14918
16175
  }
14919
16176
  this.emit("connected", {
14920
16177
  replicator: this.name,
@@ -14945,7 +16202,13 @@ class S3dbReplicator extends BaseReplicator {
14945
16202
  const normResource = normalizeResourceName$1(resource);
14946
16203
  const entry = this.resourcesMap[normResource];
14947
16204
  if (!entry) {
14948
- throw new Error(`[S3dbReplicator] Resource not configured: ${resource}`);
16205
+ throw new ReplicationError("Resource not configured for replication", {
16206
+ operation: "replicate",
16207
+ replicatorClass: "S3dbReplicator",
16208
+ resourceName: resource,
16209
+ configuredResources: Object.keys(this.resourcesMap),
16210
+ suggestion: 'Add resource to replicator resources map: { resources: { [resourceName]: "destination" } }'
16211
+ });
14949
16212
  }
14950
16213
  if (Array.isArray(entry)) {
14951
16214
  const results = [];
@@ -15013,7 +16276,14 @@ class S3dbReplicator extends BaseReplicator {
15013
16276
  } else if (operation === "delete") {
15014
16277
  result = await destResourceObj.delete(recordId);
15015
16278
  } else {
15016
- throw new Error(`Invalid operation: ${operation}. Supported operations are: insert, update, delete`);
16279
+ throw new ReplicationError(`Invalid replication operation: ${operation}`, {
16280
+ operation: "replicate",
16281
+ replicatorClass: "S3dbReplicator",
16282
+ invalidOperation: operation,
16283
+ supportedOperations: ["insert", "update", "delete"],
16284
+ resourceName: sourceResource,
16285
+ suggestion: "Use one of the supported operations: insert, update, delete"
16286
+ });
15017
16287
  }
15018
16288
  return result;
15019
16289
  }
@@ -15081,7 +16351,13 @@ class S3dbReplicator extends BaseReplicator {
15081
16351
  const norm = normalizeResourceName$1(resource);
15082
16352
  const found = available.find((r) => normalizeResourceName$1(r) === norm);
15083
16353
  if (!found) {
15084
- throw new Error(`[S3dbReplicator] Destination resource not found: ${resource}. Available: ${available.join(", ")}`);
16354
+ throw new ReplicationError("Destination resource not found in target database", {
16355
+ operation: "_getDestResourceObj",
16356
+ replicatorClass: "S3dbReplicator",
16357
+ destinationResource: resource,
16358
+ availableResources: available,
16359
+ suggestion: "Create the resource in target database or check resource name spelling"
16360
+ });
15085
16361
  }
15086
16362
  return db.resources[found];
15087
16363
  }
@@ -15130,7 +16406,13 @@ class S3dbReplicator extends BaseReplicator {
15130
16406
  }
15131
16407
  async testConnection() {
15132
16408
  const [ok, err] = await tryFn(async () => {
15133
- if (!this.targetDatabase) throw new Error("No target database configured");
16409
+ if (!this.targetDatabase) {
16410
+ throw new ReplicationError("No target database configured for connection test", {
16411
+ operation: "testConnection",
16412
+ replicatorClass: "S3dbReplicator",
16413
+ suggestion: "Initialize replicator with client or connectionString before testing connection"
16414
+ });
16415
+ }
15134
16416
  if (typeof this.targetDatabase.connect === "function") {
15135
16417
  await this.targetDatabase.connect();
15136
16418
  }
@@ -15517,7 +16799,12 @@ const REPLICATOR_DRIVERS = {
15517
16799
  function createReplicator(driver, config = {}, resources = [], client = null) {
15518
16800
  const ReplicatorClass = REPLICATOR_DRIVERS[driver];
15519
16801
  if (!ReplicatorClass) {
15520
- throw new Error(`Unknown replicator driver: ${driver}. Available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`);
16802
+ throw new ReplicationError(`Unknown replicator driver: ${driver}`, {
16803
+ operation: "createReplicator",
16804
+ driver,
16805
+ availableDrivers: Object.keys(REPLICATOR_DRIVERS),
16806
+ suggestion: `Use one of the available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`
16807
+ });
15521
16808
  }
15522
16809
  return new ReplicatorClass(config, resources, client);
15523
16810
  }
@@ -15529,12 +16816,40 @@ class ReplicatorPlugin extends Plugin {
15529
16816
  constructor(options = {}) {
15530
16817
  super();
15531
16818
  if (!options.replicators || !Array.isArray(options.replicators)) {
15532
- throw new Error("ReplicatorPlugin: replicators array is required");
16819
+ throw new ReplicationError("ReplicatorPlugin requires replicators array", {
16820
+ operation: "constructor",
16821
+ pluginName: "ReplicatorPlugin",
16822
+ providedOptions: Object.keys(options),
16823
+ suggestion: 'Provide replicators array: new ReplicatorPlugin({ replicators: [{ driver: "s3db", resources: [...] }] })'
16824
+ });
15533
16825
  }
15534
16826
  for (const rep of options.replicators) {
15535
- if (!rep.driver) throw new Error("ReplicatorPlugin: each replicator must have a driver");
15536
- if (!rep.resources || typeof rep.resources !== "object") throw new Error("ReplicatorPlugin: each replicator must have resources config");
15537
- if (Object.keys(rep.resources).length === 0) throw new Error("ReplicatorPlugin: each replicator must have at least one resource configured");
16827
+ if (!rep.driver) {
16828
+ throw new ReplicationError("Each replicator must have a driver", {
16829
+ operation: "constructor",
16830
+ pluginName: "ReplicatorPlugin",
16831
+ replicatorConfig: rep,
16832
+ suggestion: 'Each replicator entry must specify a driver: { driver: "s3db", resources: {...} }'
16833
+ });
16834
+ }
16835
+ if (!rep.resources || typeof rep.resources !== "object") {
16836
+ throw new ReplicationError("Each replicator must have resources config", {
16837
+ operation: "constructor",
16838
+ pluginName: "ReplicatorPlugin",
16839
+ driver: rep.driver,
16840
+ replicatorConfig: rep,
16841
+ suggestion: 'Provide resources as object or array: { driver: "s3db", resources: ["users"] } or { resources: { users: "people" } }'
16842
+ });
16843
+ }
16844
+ if (Object.keys(rep.resources).length === 0) {
16845
+ throw new ReplicationError("Each replicator must have at least one resource configured", {
16846
+ operation: "constructor",
16847
+ pluginName: "ReplicatorPlugin",
16848
+ driver: rep.driver,
16849
+ replicatorConfig: rep,
16850
+ suggestion: 'Add at least one resource to replicate: { driver: "s3db", resources: ["users"] }'
16851
+ });
16852
+ }
15538
16853
  }
15539
16854
  this.config = {
15540
16855
  replicators: options.replicators || [],
@@ -15960,7 +17275,13 @@ class ReplicatorPlugin extends Plugin {
15960
17275
  async syncAllData(replicatorId) {
15961
17276
  const replicator = this.replicators.find((r) => r.id === replicatorId);
15962
17277
  if (!replicator) {
15963
- throw new Error(`Replicator not found: ${replicatorId}`);
17278
+ throw new ReplicationError("Replicator not found", {
17279
+ operation: "syncAllData",
17280
+ pluginName: "ReplicatorPlugin",
17281
+ replicatorId,
17282
+ availableReplicators: this.replicators.map((r) => r.id),
17283
+ suggestion: "Check replicator ID or use getReplicatorStats() to list available replicators"
17284
+ });
15964
17285
  }
15965
17286
  this.stats.lastSync = (/* @__PURE__ */ new Date()).toISOString();
15966
17287
  for (const resourceName in this.database.resources) {
@@ -16490,6 +17811,35 @@ class S3QueuePlugin extends Plugin {
16490
17811
  }
16491
17812
  }
16492
17813
 
17814
+ class SchedulerError extends S3dbError {
17815
+ constructor(message, details = {}) {
17816
+ const { taskId, operation = "unknown", cronExpression, ...rest } = details;
17817
+ let description = details.description;
17818
+ if (!description) {
17819
+ description = `
17820
+ Scheduler Operation Error
17821
+
17822
+ Operation: ${operation}
17823
+ ${taskId ? `Task ID: ${taskId}` : ""}
17824
+ ${cronExpression ? `Cron: ${cronExpression}` : ""}
17825
+
17826
+ Common causes:
17827
+ 1. Invalid cron expression format
17828
+ 2. Task not found or already exists
17829
+ 3. Scheduler not properly initialized
17830
+ 4. Job execution failure
17831
+ 5. Resource conflicts
17832
+
17833
+ Solution:
17834
+ Check task configuration and ensure scheduler is properly initialized.
17835
+
17836
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/scheduler.md
17837
+ `.trim();
17838
+ }
17839
+ super(message, { ...rest, taskId, operation, cronExpression, description });
17840
+ }
17841
+ }
17842
+
16493
17843
  class SchedulerPlugin extends Plugin {
16494
17844
  constructor(options = {}) {
16495
17845
  super();
@@ -16523,17 +17873,36 @@ class SchedulerPlugin extends Plugin {
16523
17873
  }
16524
17874
  _validateConfiguration() {
16525
17875
  if (Object.keys(this.config.jobs).length === 0) {
16526
- throw new Error("SchedulerPlugin: At least one job must be defined");
17876
+ throw new SchedulerError("At least one job must be defined", {
17877
+ operation: "validateConfiguration",
17878
+ jobCount: 0,
17879
+ suggestion: 'Provide at least one job in the jobs configuration: { jobs: { myJob: { schedule: "* * * * *", action: async () => {...} } } }'
17880
+ });
16527
17881
  }
16528
17882
  for (const [jobName, job] of Object.entries(this.config.jobs)) {
16529
17883
  if (!job.schedule) {
16530
- throw new Error(`SchedulerPlugin: Job '${jobName}' must have a schedule`);
17884
+ throw new SchedulerError(`Job '${jobName}' must have a schedule`, {
17885
+ operation: "validateConfiguration",
17886
+ taskId: jobName,
17887
+ providedConfig: Object.keys(job),
17888
+ suggestion: 'Add a schedule property with a valid cron expression: { schedule: "0 * * * *", action: async () => {...} }'
17889
+ });
16531
17890
  }
16532
17891
  if (!job.action || typeof job.action !== "function") {
16533
- throw new Error(`SchedulerPlugin: Job '${jobName}' must have an action function`);
17892
+ throw new SchedulerError(`Job '${jobName}' must have an action function`, {
17893
+ operation: "validateConfiguration",
17894
+ taskId: jobName,
17895
+ actionType: typeof job.action,
17896
+ suggestion: 'Provide an action function: { schedule: "...", action: async (db, ctx) => {...} }'
17897
+ });
16534
17898
  }
16535
17899
  if (!this._isValidCronExpression(job.schedule)) {
16536
- throw new Error(`SchedulerPlugin: Job '${jobName}' has invalid cron expression: ${job.schedule}`);
17900
+ throw new SchedulerError(`Job '${jobName}' has invalid cron expression`, {
17901
+ operation: "validateConfiguration",
17902
+ taskId: jobName,
17903
+ cronExpression: job.schedule,
17904
+ suggestion: "Use valid cron format (5 fields: minute hour day month weekday) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
17905
+ });
16537
17906
  }
16538
17907
  }
16539
17908
  }
@@ -16831,10 +18200,20 @@ class SchedulerPlugin extends Plugin {
16831
18200
  async runJob(jobName, context = {}) {
16832
18201
  const job = this.jobs.get(jobName);
16833
18202
  if (!job) {
16834
- throw new Error(`Job '${jobName}' not found`);
18203
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18204
+ operation: "runJob",
18205
+ taskId: jobName,
18206
+ availableJobs: Array.from(this.jobs.keys()),
18207
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18208
+ });
16835
18209
  }
16836
18210
  if (this.activeJobs.has(jobName)) {
16837
- throw new Error(`Job '${jobName}' is already running`);
18211
+ throw new SchedulerError(`Job '${jobName}' is already running`, {
18212
+ operation: "runJob",
18213
+ taskId: jobName,
18214
+ executionId: this.activeJobs.get(jobName),
18215
+ suggestion: "Wait for current execution to complete or check job status with getJobStatus()"
18216
+ });
16838
18217
  }
16839
18218
  await this._executeJob(jobName);
16840
18219
  }
@@ -16844,7 +18223,12 @@ class SchedulerPlugin extends Plugin {
16844
18223
  enableJob(jobName) {
16845
18224
  const job = this.jobs.get(jobName);
16846
18225
  if (!job) {
16847
- throw new Error(`Job '${jobName}' not found`);
18226
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18227
+ operation: "enableJob",
18228
+ taskId: jobName,
18229
+ availableJobs: Array.from(this.jobs.keys()),
18230
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18231
+ });
16848
18232
  }
16849
18233
  job.enabled = true;
16850
18234
  this._scheduleNextExecution(jobName);
@@ -16856,7 +18240,12 @@ class SchedulerPlugin extends Plugin {
16856
18240
  disableJob(jobName) {
16857
18241
  const job = this.jobs.get(jobName);
16858
18242
  if (!job) {
16859
- throw new Error(`Job '${jobName}' not found`);
18243
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18244
+ operation: "disableJob",
18245
+ taskId: jobName,
18246
+ availableJobs: Array.from(this.jobs.keys()),
18247
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18248
+ });
16860
18249
  }
16861
18250
  job.enabled = false;
16862
18251
  const timer = this.timers.get(jobName);
@@ -16955,13 +18344,28 @@ class SchedulerPlugin extends Plugin {
16955
18344
  */
16956
18345
  addJob(jobName, jobConfig) {
16957
18346
  if (this.jobs.has(jobName)) {
16958
- throw new Error(`Job '${jobName}' already exists`);
18347
+ throw new SchedulerError(`Job '${jobName}' already exists`, {
18348
+ operation: "addJob",
18349
+ taskId: jobName,
18350
+ existingJobs: Array.from(this.jobs.keys()),
18351
+ suggestion: "Use a different job name or remove the existing job first with removeJob()"
18352
+ });
16959
18353
  }
16960
18354
  if (!jobConfig.schedule || !jobConfig.action) {
16961
- throw new Error("Job must have schedule and action");
18355
+ throw new SchedulerError("Job must have schedule and action", {
18356
+ operation: "addJob",
18357
+ taskId: jobName,
18358
+ providedConfig: Object.keys(jobConfig),
18359
+ suggestion: 'Provide both schedule and action: { schedule: "0 * * * *", action: async (db, ctx) => {...} }'
18360
+ });
16962
18361
  }
16963
18362
  if (!this._isValidCronExpression(jobConfig.schedule)) {
16964
- throw new Error(`Invalid cron expression: ${jobConfig.schedule}`);
18363
+ throw new SchedulerError("Invalid cron expression", {
18364
+ operation: "addJob",
18365
+ taskId: jobName,
18366
+ cronExpression: jobConfig.schedule,
18367
+ suggestion: "Use valid cron format (5 fields) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
18368
+ });
16965
18369
  }
16966
18370
  const job = {
16967
18371
  ...jobConfig,
@@ -16995,7 +18399,12 @@ class SchedulerPlugin extends Plugin {
16995
18399
  removeJob(jobName) {
16996
18400
  const job = this.jobs.get(jobName);
16997
18401
  if (!job) {
16998
- throw new Error(`Job '${jobName}' not found`);
18402
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18403
+ operation: "removeJob",
18404
+ taskId: jobName,
18405
+ availableJobs: Array.from(this.jobs.keys()),
18406
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18407
+ });
16999
18408
  }
17000
18409
  const timer = this.timers.get(jobName);
17001
18410
  if (timer) {
@@ -17049,6 +18458,36 @@ class SchedulerPlugin extends Plugin {
17049
18458
  }
17050
18459
  }
17051
18460
 
18461
+ class StateMachineError extends S3dbError {
18462
+ constructor(message, details = {}) {
18463
+ const { currentState, targetState, resourceName, operation = "unknown", ...rest } = details;
18464
+ let description = details.description;
18465
+ if (!description) {
18466
+ description = `
18467
+ State Machine Operation Error
18468
+
18469
+ Operation: ${operation}
18470
+ ${currentState ? `Current State: ${currentState}` : ""}
18471
+ ${targetState ? `Target State: ${targetState}` : ""}
18472
+ ${resourceName ? `Resource: ${resourceName}` : ""}
18473
+
18474
+ Common causes:
18475
+ 1. Invalid state transition
18476
+ 2. State machine not configured
18477
+ 3. Transition conditions not met
18478
+ 4. State not defined in configuration
18479
+ 5. Missing transition handler
18480
+
18481
+ Solution:
18482
+ Check state machine configuration and valid transitions.
18483
+
18484
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-machine.md
18485
+ `.trim();
18486
+ }
18487
+ super(message, { ...rest, currentState, targetState, resourceName, operation, description });
18488
+ }
18489
+ }
18490
+
17052
18491
  class StateMachinePlugin extends Plugin {
17053
18492
  constructor(options = {}) {
17054
18493
  super();
@@ -17069,17 +18508,36 @@ class StateMachinePlugin extends Plugin {
17069
18508
  }
17070
18509
  _validateConfiguration() {
17071
18510
  if (!this.config.stateMachines || Object.keys(this.config.stateMachines).length === 0) {
17072
- throw new Error("StateMachinePlugin: At least one state machine must be defined");
18511
+ throw new StateMachineError("At least one state machine must be defined", {
18512
+ operation: "validateConfiguration",
18513
+ machineCount: 0,
18514
+ suggestion: "Provide at least one state machine in the stateMachines configuration"
18515
+ });
17073
18516
  }
17074
18517
  for (const [machineName, machine] of Object.entries(this.config.stateMachines)) {
17075
18518
  if (!machine.states || Object.keys(machine.states).length === 0) {
17076
- throw new Error(`StateMachinePlugin: Machine '${machineName}' must have states defined`);
18519
+ throw new StateMachineError(`Machine '${machineName}' must have states defined`, {
18520
+ operation: "validateConfiguration",
18521
+ machineId: machineName,
18522
+ suggestion: "Define at least one state in the states configuration"
18523
+ });
17077
18524
  }
17078
18525
  if (!machine.initialState) {
17079
- throw new Error(`StateMachinePlugin: Machine '${machineName}' must have an initialState`);
18526
+ throw new StateMachineError(`Machine '${machineName}' must have an initialState`, {
18527
+ operation: "validateConfiguration",
18528
+ machineId: machineName,
18529
+ availableStates: Object.keys(machine.states),
18530
+ suggestion: "Specify an initialState property matching one of the defined states"
18531
+ });
17080
18532
  }
17081
18533
  if (!machine.states[machine.initialState]) {
17082
- throw new Error(`StateMachinePlugin: Initial state '${machine.initialState}' not found in machine '${machineName}'`);
18534
+ throw new StateMachineError(`Initial state '${machine.initialState}' not found in machine '${machineName}'`, {
18535
+ operation: "validateConfiguration",
18536
+ machineId: machineName,
18537
+ initialState: machine.initialState,
18538
+ availableStates: Object.keys(machine.states),
18539
+ suggestion: "Set initialState to one of the defined states"
18540
+ });
17083
18541
  }
17084
18542
  }
17085
18543
  }
@@ -17136,12 +18594,25 @@ class StateMachinePlugin extends Plugin {
17136
18594
  async send(machineId, entityId, event, context = {}) {
17137
18595
  const machine = this.machines.get(machineId);
17138
18596
  if (!machine) {
17139
- throw new Error(`State machine '${machineId}' not found`);
18597
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18598
+ operation: "send",
18599
+ machineId,
18600
+ availableMachines: Array.from(this.machines.keys()),
18601
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18602
+ });
17140
18603
  }
17141
18604
  const currentState = await this.getState(machineId, entityId);
17142
18605
  const stateConfig = machine.config.states[currentState];
17143
18606
  if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
17144
- throw new Error(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`);
18607
+ throw new StateMachineError(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`, {
18608
+ operation: "send",
18609
+ machineId,
18610
+ entityId,
18611
+ event,
18612
+ currentState,
18613
+ validEvents: stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [],
18614
+ suggestion: "Use getValidEvents() to check which events are valid for the current state"
18615
+ });
17145
18616
  }
17146
18617
  const targetState = stateConfig.on[event];
17147
18618
  if (stateConfig.guards && stateConfig.guards[event]) {
@@ -17152,7 +18623,16 @@ class StateMachinePlugin extends Plugin {
17152
18623
  () => guard(context, event, { database: this.database, machineId, entityId })
17153
18624
  );
17154
18625
  if (!guardOk || !guardResult) {
17155
- throw new Error(`Transition blocked by guard '${guardName}': ${guardErr?.message || "Guard returned false"}`);
18626
+ throw new StateMachineError(`Transition blocked by guard '${guardName}'`, {
18627
+ operation: "send",
18628
+ machineId,
18629
+ entityId,
18630
+ event,
18631
+ currentState,
18632
+ guardName,
18633
+ guardError: guardErr?.message || "Guard returned false",
18634
+ suggestion: "Check guard conditions or modify the context to satisfy guard requirements"
18635
+ });
17156
18636
  }
17157
18637
  }
17158
18638
  }
@@ -17262,7 +18742,12 @@ class StateMachinePlugin extends Plugin {
17262
18742
  async getState(machineId, entityId) {
17263
18743
  const machine = this.machines.get(machineId);
17264
18744
  if (!machine) {
17265
- throw new Error(`State machine '${machineId}' not found`);
18745
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18746
+ operation: "getState",
18747
+ machineId,
18748
+ availableMachines: Array.from(this.machines.keys()),
18749
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18750
+ });
17266
18751
  }
17267
18752
  if (machine.currentStates.has(entityId)) {
17268
18753
  return machine.currentStates.get(entityId);
@@ -17288,7 +18773,12 @@ class StateMachinePlugin extends Plugin {
17288
18773
  async getValidEvents(machineId, stateOrEntityId) {
17289
18774
  const machine = this.machines.get(machineId);
17290
18775
  if (!machine) {
17291
- throw new Error(`State machine '${machineId}' not found`);
18776
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18777
+ operation: "getValidEvents",
18778
+ machineId,
18779
+ availableMachines: Array.from(this.machines.keys()),
18780
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18781
+ });
17292
18782
  }
17293
18783
  let state;
17294
18784
  if (machine.config.states[stateOrEntityId]) {
@@ -17337,7 +18827,12 @@ class StateMachinePlugin extends Plugin {
17337
18827
  async initializeEntity(machineId, entityId, context = {}) {
17338
18828
  const machine = this.machines.get(machineId);
17339
18829
  if (!machine) {
17340
- throw new Error(`State machine '${machineId}' not found`);
18830
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18831
+ operation: "initializeEntity",
18832
+ machineId,
18833
+ availableMachines: Array.from(this.machines.keys()),
18834
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18835
+ });
17341
18836
  }
17342
18837
  const initialState = machine.config.initialState;
17343
18838
  machine.currentStates.set(entityId, initialState);
@@ -17356,7 +18851,14 @@ class StateMachinePlugin extends Plugin {
17356
18851
  })
17357
18852
  );
17358
18853
  if (!ok && err && !err.message?.includes("already exists")) {
17359
- throw new Error(`Failed to initialize entity state: ${err.message}`);
18854
+ throw new StateMachineError("Failed to initialize entity state", {
18855
+ operation: "initializeEntity",
18856
+ machineId,
18857
+ entityId,
18858
+ initialState,
18859
+ original: err,
18860
+ suggestion: "Check state resource configuration and database permissions"
18861
+ });
17360
18862
  }
17361
18863
  }
17362
18864
  const initialStateConfig = machine.config.states[initialState];
@@ -17385,7 +18887,12 @@ class StateMachinePlugin extends Plugin {
17385
18887
  visualize(machineId) {
17386
18888
  const machine = this.machines.get(machineId);
17387
18889
  if (!machine) {
17388
- throw new Error(`State machine '${machineId}' not found`);
18890
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18891
+ operation: "visualize",
18892
+ machineId,
18893
+ availableMachines: Array.from(this.machines.keys()),
18894
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18895
+ });
17389
18896
  }
17390
18897
  let dot = `digraph ${machineId} {
17391
18898
  `;
@@ -17430,10 +18937,12 @@ class StateMachinePlugin extends Plugin {
17430
18937
  }
17431
18938
 
17432
18939
  exports.AVAILABLE_BEHAVIORS = AVAILABLE_BEHAVIORS;
18940
+ exports.AnalyticsNotEnabledError = AnalyticsNotEnabledError;
17433
18941
  exports.AuditPlugin = AuditPlugin;
17434
18942
  exports.AuthenticationError = AuthenticationError;
17435
18943
  exports.BackupPlugin = BackupPlugin;
17436
18944
  exports.BaseError = BaseError;
18945
+ exports.BehaviorError = BehaviorError;
17437
18946
  exports.CachePlugin = CachePlugin;
17438
18947
  exports.Client = Client;
17439
18948
  exports.ConnectionString = ConnectionString;
@@ -17448,15 +18957,19 @@ exports.ErrorMap = ErrorMap;
17448
18957
  exports.EventualConsistencyPlugin = EventualConsistencyPlugin;
17449
18958
  exports.FullTextPlugin = FullTextPlugin;
17450
18959
  exports.InvalidResourceItem = InvalidResourceItem;
18960
+ exports.MetadataLimitError = MetadataLimitError;
17451
18961
  exports.MetricsPlugin = MetricsPlugin;
17452
18962
  exports.MissingMetadata = MissingMetadata;
17453
18963
  exports.NoSuchBucket = NoSuchBucket;
17454
18964
  exports.NoSuchKey = NoSuchKey;
17455
18965
  exports.NotFound = NotFound;
18966
+ exports.PartitionDriverError = PartitionDriverError;
17456
18967
  exports.PartitionError = PartitionError;
17457
18968
  exports.PermissionError = PermissionError;
17458
18969
  exports.Plugin = Plugin;
18970
+ exports.PluginError = PluginError;
17459
18971
  exports.PluginObject = PluginObject;
18972
+ exports.PluginStorageError = PluginStorageError;
17460
18973
  exports.QueueConsumerPlugin = QueueConsumerPlugin;
17461
18974
  exports.ReplicatorPlugin = ReplicatorPlugin;
17462
18975
  exports.Resource = Resource;
@@ -17473,6 +18986,7 @@ exports.SchedulerPlugin = SchedulerPlugin;
17473
18986
  exports.Schema = Schema;
17474
18987
  exports.SchemaError = SchemaError;
17475
18988
  exports.StateMachinePlugin = StateMachinePlugin;
18989
+ exports.StreamError = StreamError;
17476
18990
  exports.UnknownError = UnknownError;
17477
18991
  exports.ValidationError = ValidationError;
17478
18992
  exports.Validator = Validator;