s3db.js 11.2.3 → 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 (42) hide show
  1. package/dist/s3db.cjs.js +1177 -128
  2. package/dist/s3db.cjs.js.map +1 -1
  3. package/dist/s3db.es.js +1172 -129
  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 +19 -4
  12. package/src/errors.js +306 -27
  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.errors.js +47 -0
  26. package/src/plugins/cache.plugin.js +8 -1
  27. package/src/plugins/fulltext.errors.js +46 -0
  28. package/src/plugins/fulltext.plugin.js +15 -3
  29. package/src/plugins/metrics.errors.js +46 -0
  30. package/src/plugins/queue-consumer.plugin.js +31 -4
  31. package/src/plugins/queue.errors.js +46 -0
  32. package/src/plugins/replicator.errors.js +46 -0
  33. package/src/plugins/replicator.plugin.js +40 -5
  34. package/src/plugins/replicators/base-replicator.class.js +19 -3
  35. package/src/plugins/replicators/index.js +9 -3
  36. package/src/plugins/replicators/s3db-replicator.class.js +38 -8
  37. package/src/plugins/scheduler.errors.js +46 -0
  38. package/src/plugins/scheduler.plugin.js +79 -19
  39. package/src/plugins/state-machine.errors.js +47 -0
  40. package/src/plugins/state-machine.plugin.js +86 -17
  41. package/src/stream/index.js +6 -1
  42. package/src/stream/resource-reader.class.js +6 -1
package/dist/s3db.es.js CHANGED
@@ -218,7 +218,7 @@ function calculateEffectiveLimit(config = {}) {
218
218
  }
219
219
 
220
220
  class BaseError extends Error {
221
- constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, suggestion, description, ...rest }) {
221
+ constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, description, ...rest }) {
222
222
  if (verbose) message = message + `
223
223
 
224
224
  Verbose:
@@ -243,7 +243,6 @@ ${JSON.stringify(rest, null, 2)}`;
243
243
  this.commandName = commandName;
244
244
  this.commandInput = commandInput;
245
245
  this.metadata = metadata;
246
- this.suggestion = suggestion;
247
246
  this.description = description;
248
247
  this.data = { bucket, key, ...rest, verbose, message };
249
248
  }
@@ -261,7 +260,6 @@ ${JSON.stringify(rest, null, 2)}`;
261
260
  commandName: this.commandName,
262
261
  commandInput: this.commandInput,
263
262
  metadata: this.metadata,
264
- suggestion: this.suggestion,
265
263
  description: this.description,
266
264
  data: this.data,
267
265
  original: this.original,
@@ -402,26 +400,26 @@ function mapAwsError(err, context = {}) {
402
400
  const metadata = err.$metadata ? { ...err.$metadata } : void 0;
403
401
  const commandName = context.commandName;
404
402
  const commandInput = context.commandInput;
405
- let suggestion;
403
+ let description;
406
404
  if (code === "NoSuchKey" || code === "NotFound") {
407
- suggestion = "Check if the key exists in the specified bucket and if your credentials have permission.";
408
- return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, suggestion });
405
+ description = "The specified key does not exist in the bucket. Check if the key exists and if your credentials have permission to access it.";
406
+ return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, description });
409
407
  }
410
408
  if (code === "NoSuchBucket") {
411
- suggestion = "Check if the bucket exists and if your credentials have permission.";
412
- return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, suggestion });
409
+ description = "The specified bucket does not exist. Check if the bucket name is correct and if your credentials have permission to access it.";
410
+ return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, description });
413
411
  }
414
412
  if (code === "AccessDenied" || err.statusCode === 403 || code === "Forbidden") {
415
- suggestion = "Check your credentials and bucket policy.";
416
- return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput, suggestion });
413
+ description = "Access denied. Check your AWS credentials, IAM permissions, and bucket policy.";
414
+ return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput, description });
417
415
  }
418
416
  if (code === "ValidationError" || err.statusCode === 400) {
419
- suggestion = "Check the request parameters and payload.";
420
- return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput, suggestion });
417
+ description = "Validation error. Check the request parameters and payload format.";
418
+ return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput, description });
421
419
  }
422
420
  if (code === "MissingMetadata") {
423
- suggestion = "Check if the object metadata is present and valid.";
424
- return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, suggestion });
421
+ description = "Object metadata is missing or invalid. Check if the object was uploaded correctly.";
422
+ return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, description });
425
423
  }
426
424
  const errorDetails = [
427
425
  `Unknown error: ${err.message || err.toString()}`,
@@ -429,27 +427,31 @@ function mapAwsError(err, context = {}) {
429
427
  err.statusCode && `Status: ${err.statusCode}`,
430
428
  err.stack && `Stack: ${err.stack.split("\n")[0]}`
431
429
  ].filter(Boolean).join(" | ");
432
- suggestion = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
433
- return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, suggestion });
430
+ description = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
431
+ return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, description });
434
432
  }
435
433
  class ConnectionStringError extends S3dbError {
436
434
  constructor(message, details = {}) {
437
- super(message, { ...details, suggestion: "Check the connection string format and credentials." });
435
+ const description = details.description || "Invalid connection string format. Check the connection string syntax and credentials.";
436
+ super(message, { ...details, description });
438
437
  }
439
438
  }
440
439
  class CryptoError extends S3dbError {
441
440
  constructor(message, details = {}) {
442
- super(message, { ...details, suggestion: "Check if the crypto library is available and input is valid." });
441
+ const description = details.description || "Cryptography operation failed. Check if the crypto library is available and input is valid.";
442
+ super(message, { ...details, description });
443
443
  }
444
444
  }
445
445
  class SchemaError extends S3dbError {
446
446
  constructor(message, details = {}) {
447
- super(message, { ...details, suggestion: "Check schema definition and input data." });
447
+ const description = details.description || "Schema validation failed. Check schema definition and input data format.";
448
+ super(message, { ...details, description });
448
449
  }
449
450
  }
450
451
  class ResourceError extends S3dbError {
451
452
  constructor(message, details = {}) {
452
- super(message, { ...details, suggestion: details.suggestion || "Check resource configuration, attributes, and operation context." });
453
+ const description = details.description || "Resource operation failed. Check resource configuration, attributes, and operation context.";
454
+ super(message, { ...details, description });
453
455
  Object.assign(this, details);
454
456
  }
455
457
  }
@@ -478,13 +480,12 @@ ${details.strictValidation === false ? " \u2022 Update partition definition to
478
480
  \u2022 Update partition definition to use existing fields, OR
479
481
  \u2022 Use strictValidation: false to skip this check during testing`}
480
482
 
481
- Docs: https://docs.s3db.js.org/resources/partitions#validation
483
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partitions
482
484
  `.trim();
483
485
  }
484
486
  super(message, {
485
487
  ...details,
486
- description,
487
- suggestion: details.suggestion || "Check partition definition, fields, and input values."
488
+ description
488
489
  });
489
490
  }
490
491
  }
@@ -543,7 +544,7 @@ Example fix:
543
544
  await db.connect(); // Plugin initialized here
544
545
  await db.createResource({ name: '${resourceName}', ... }); // Analytics resource created here
545
546
 
546
- Docs: https://docs.s3db.js.org/plugins/eventual-consistency#troubleshooting
547
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/eventual-consistency.md
547
548
  `.trim();
548
549
  super(message, {
549
550
  ...rest,
@@ -553,8 +554,260 @@ Docs: https://docs.s3db.js.org/plugins/eventual-consistency#troubleshooting
553
554
  configuredResources,
554
555
  registeredResources,
555
556
  pluginInitialized,
556
- description,
557
- suggestion: "Ensure resources are created after plugin initialization. Check plugin configuration and resource creation order."
557
+ description
558
+ });
559
+ }
560
+ }
561
+ class PluginError extends S3dbError {
562
+ constructor(message, details = {}) {
563
+ const {
564
+ pluginName = "Unknown",
565
+ operation = "unknown",
566
+ ...rest
567
+ } = details;
568
+ let description = details.description;
569
+ if (!description) {
570
+ description = `
571
+ Plugin Error
572
+
573
+ Plugin: ${pluginName}
574
+ Operation: ${operation}
575
+
576
+ Possible causes:
577
+ 1. Plugin not properly initialized
578
+ 2. Plugin configuration is invalid
579
+ 3. Plugin dependencies not met
580
+ 4. Plugin method called before installation
581
+
582
+ Solution:
583
+ Ensure plugin is added to database and connect() is called before usage.
584
+
585
+ Example:
586
+ const db = new Database({
587
+ bucket: 'my-bucket',
588
+ plugins: [new ${pluginName}({ /* config */ })]
589
+ });
590
+
591
+ await db.connect(); // Plugin installed here
592
+ // Now plugin methods are available
593
+
594
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md
595
+ `.trim();
596
+ }
597
+ super(message, {
598
+ ...rest,
599
+ pluginName,
600
+ operation,
601
+ description
602
+ });
603
+ }
604
+ }
605
+ class PluginStorageError extends S3dbError {
606
+ constructor(message, details = {}) {
607
+ const {
608
+ pluginSlug = "unknown",
609
+ key = "",
610
+ operation = "unknown",
611
+ ...rest
612
+ } = details;
613
+ let description = details.description;
614
+ if (!description) {
615
+ description = `
616
+ Plugin Storage Error
617
+
618
+ Plugin: ${pluginSlug}
619
+ Key: ${key}
620
+ Operation: ${operation}
621
+
622
+ Possible causes:
623
+ 1. Storage not initialized (plugin not installed)
624
+ 2. Invalid key format
625
+ 3. S3 operation failed
626
+ 4. Permissions issue
627
+
628
+ Solution:
629
+ Ensure plugin has access to storage and key is valid.
630
+
631
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md#plugin-storage
632
+ `.trim();
633
+ }
634
+ super(message, {
635
+ ...rest,
636
+ pluginSlug,
637
+ key,
638
+ operation,
639
+ description
640
+ });
641
+ }
642
+ }
643
+ class PartitionDriverError extends S3dbError {
644
+ constructor(message, details = {}) {
645
+ const {
646
+ driver = "unknown",
647
+ operation = "unknown",
648
+ queueSize,
649
+ maxQueueSize,
650
+ ...rest
651
+ } = details;
652
+ let description = details.description;
653
+ if (!description && queueSize !== void 0 && maxQueueSize !== void 0) {
654
+ description = `
655
+ Partition Driver Error
656
+
657
+ Driver: ${driver}
658
+ Operation: ${operation}
659
+ Queue Status: ${queueSize}/${maxQueueSize}
660
+
661
+ Possible causes:
662
+ 1. Queue is full (backpressure)
663
+ 2. Driver not properly configured
664
+ 3. SQS permissions issue (if using SQS driver)
665
+
666
+ Solution:
667
+ ${queueSize >= maxQueueSize ? "Wait for queue to drain or increase maxQueueSize" : "Check driver configuration and permissions"}
668
+
669
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
670
+ `.trim();
671
+ } else if (!description) {
672
+ description = `
673
+ Partition Driver Error
674
+
675
+ Driver: ${driver}
676
+ Operation: ${operation}
677
+
678
+ Check driver configuration and permissions.
679
+
680
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
681
+ `.trim();
682
+ }
683
+ super(message, {
684
+ ...rest,
685
+ driver,
686
+ operation,
687
+ queueSize,
688
+ maxQueueSize,
689
+ description
690
+ });
691
+ }
692
+ }
693
+ class BehaviorError extends S3dbError {
694
+ constructor(message, details = {}) {
695
+ const {
696
+ behavior = "unknown",
697
+ availableBehaviors = [],
698
+ ...rest
699
+ } = details;
700
+ let description = details.description;
701
+ if (!description) {
702
+ description = `
703
+ Behavior Error
704
+
705
+ Requested: ${behavior}
706
+ Available: ${availableBehaviors.join(", ") || "body-overflow, body-only, truncate-data, enforce-limits, user-managed"}
707
+
708
+ Possible causes:
709
+ 1. Behavior name misspelled
710
+ 2. Custom behavior not registered
711
+
712
+ Solution:
713
+ Use one of the available behaviors or register custom behavior.
714
+
715
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#behaviors
716
+ `.trim();
717
+ }
718
+ super(message, {
719
+ ...rest,
720
+ behavior,
721
+ availableBehaviors,
722
+ description
723
+ });
724
+ }
725
+ }
726
+ class StreamError extends S3dbError {
727
+ constructor(message, details = {}) {
728
+ const {
729
+ operation = "unknown",
730
+ resource,
731
+ ...rest
732
+ } = details;
733
+ let description = details.description;
734
+ if (!description) {
735
+ description = `
736
+ Stream Error
737
+
738
+ Operation: ${operation}
739
+ ${resource ? `Resource: ${resource}` : ""}
740
+
741
+ Possible causes:
742
+ 1. Stream not properly initialized
743
+ 2. Resource not available
744
+ 3. Network error during streaming
745
+
746
+ Solution:
747
+ Check stream configuration and resource availability.
748
+
749
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#streaming
750
+ `.trim();
751
+ }
752
+ super(message, {
753
+ ...rest,
754
+ operation,
755
+ resource,
756
+ description
757
+ });
758
+ }
759
+ }
760
+ class MetadataLimitError extends S3dbError {
761
+ constructor(message, details = {}) {
762
+ const {
763
+ totalSize,
764
+ effectiveLimit,
765
+ absoluteLimit = 2047,
766
+ excess,
767
+ resourceName,
768
+ operation,
769
+ ...rest
770
+ } = details;
771
+ let description = details.description;
772
+ if (!description && totalSize && effectiveLimit) {
773
+ description = `
774
+ S3 Metadata Size Limit Exceeded
775
+
776
+ Current Size: ${totalSize} bytes
777
+ Effective Limit: ${effectiveLimit} bytes
778
+ Absolute Limit: ${absoluteLimit} bytes
779
+ ${excess ? `Excess: ${excess} bytes` : ""}
780
+ ${resourceName ? `Resource: ${resourceName}` : ""}
781
+ ${operation ? `Operation: ${operation}` : ""}
782
+
783
+ S3 has a hard limit of 2KB (2047 bytes) for object metadata.
784
+
785
+ Solutions:
786
+ 1. Use 'body-overflow' behavior to store excess in body
787
+ 2. Use 'body-only' behavior to store everything in body
788
+ 3. Reduce number of fields
789
+ 4. Use shorter field values
790
+ 5. Enable advanced metadata encoding
791
+
792
+ Example:
793
+ await db.createResource({
794
+ name: '${resourceName || "myResource"}',
795
+ behavior: 'body-overflow', // Automatically handles overflow
796
+ attributes: { ... }
797
+ });
798
+
799
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#metadata-size-limits
800
+ `.trim();
801
+ }
802
+ super(message, {
803
+ ...rest,
804
+ totalSize,
805
+ effectiveLimit,
806
+ absoluteLimit,
807
+ excess,
808
+ resourceName,
809
+ operation,
810
+ description
558
811
  });
559
812
  }
560
813
  }
@@ -898,10 +1151,17 @@ class PluginStorage {
898
1151
  */
899
1152
  constructor(client, pluginSlug) {
900
1153
  if (!client) {
901
- throw new Error("PluginStorage requires a client instance");
1154
+ throw new PluginStorageError("PluginStorage requires a client instance", {
1155
+ operation: "constructor",
1156
+ pluginSlug,
1157
+ suggestion: "Pass a valid S3db Client instance when creating PluginStorage"
1158
+ });
902
1159
  }
903
1160
  if (!pluginSlug) {
904
- throw new Error("PluginStorage requires a pluginSlug");
1161
+ throw new PluginStorageError("PluginStorage requires a pluginSlug", {
1162
+ operation: "constructor",
1163
+ suggestion: 'Provide a plugin slug (e.g., "eventual-consistency", "cache", "audit")'
1164
+ });
905
1165
  }
906
1166
  this.client = client;
907
1167
  this.pluginSlug = pluginSlug;
@@ -954,7 +1214,15 @@ class PluginStorage {
954
1214
  }
955
1215
  const [ok, err] = await tryFn(() => this.client.putObject(putParams));
956
1216
  if (!ok) {
957
- throw new Error(`PluginStorage.set failed for key ${key}: ${err.message}`);
1217
+ throw new PluginStorageError(`Failed to save plugin data`, {
1218
+ pluginSlug: this.pluginSlug,
1219
+ key,
1220
+ operation: "set",
1221
+ behavior,
1222
+ ttl,
1223
+ original: err,
1224
+ suggestion: "Check S3 permissions and key format"
1225
+ });
958
1226
  }
959
1227
  }
960
1228
  /**
@@ -976,7 +1244,13 @@ class PluginStorage {
976
1244
  if (err.name === "NoSuchKey" || err.Code === "NoSuchKey") {
977
1245
  return null;
978
1246
  }
979
- throw new Error(`PluginStorage.get failed for key ${key}: ${err.message}`);
1247
+ throw new PluginStorageError(`Failed to retrieve plugin data`, {
1248
+ pluginSlug: this.pluginSlug,
1249
+ key,
1250
+ operation: "get",
1251
+ original: err,
1252
+ suggestion: "Check if the key exists and S3 permissions are correct"
1253
+ });
980
1254
  }
981
1255
  const metadata = response.Metadata || {};
982
1256
  const parsedMetadata = this._parseMetadataValues(metadata);
@@ -989,7 +1263,13 @@ class PluginStorage {
989
1263
  data = { ...parsedMetadata, ...body };
990
1264
  }
991
1265
  } catch (parseErr) {
992
- throw new Error(`PluginStorage.get failed to parse body for key ${key}: ${parseErr.message}`);
1266
+ throw new PluginStorageError(`Failed to parse JSON body`, {
1267
+ pluginSlug: this.pluginSlug,
1268
+ key,
1269
+ operation: "get",
1270
+ original: parseErr,
1271
+ suggestion: "Body content may be corrupted. Check S3 object integrity"
1272
+ });
993
1273
  }
994
1274
  }
995
1275
  const expiresAt = data._expiresat || data._expiresAt;
@@ -1050,7 +1330,15 @@ class PluginStorage {
1050
1330
  () => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
1051
1331
  );
1052
1332
  if (!ok) {
1053
- throw new Error(`PluginStorage.list failed: ${err.message}`);
1333
+ throw new PluginStorageError(`Failed to list plugin data`, {
1334
+ pluginSlug: this.pluginSlug,
1335
+ operation: "list",
1336
+ prefix,
1337
+ fullPrefix,
1338
+ limit,
1339
+ original: err,
1340
+ suggestion: "Check S3 permissions and bucket configuration"
1341
+ });
1054
1342
  }
1055
1343
  const keys = result.Contents?.map((item) => item.Key) || [];
1056
1344
  return this._removeKeyPrefix(keys);
@@ -1070,7 +1358,16 @@ class PluginStorage {
1070
1358
  () => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
1071
1359
  );
1072
1360
  if (!ok) {
1073
- throw new Error(`PluginStorage.listForResource failed: ${err.message}`);
1361
+ throw new PluginStorageError(`Failed to list resource data`, {
1362
+ pluginSlug: this.pluginSlug,
1363
+ operation: "listForResource",
1364
+ resourceName,
1365
+ subPrefix,
1366
+ fullPrefix,
1367
+ limit,
1368
+ original: err,
1369
+ suggestion: "Check resource name and S3 permissions"
1370
+ });
1074
1371
  }
1075
1372
  const keys = result.Contents?.map((item) => item.Key) || [];
1076
1373
  return this._removeKeyPrefix(keys);
@@ -1210,7 +1507,13 @@ class PluginStorage {
1210
1507
  async delete(key) {
1211
1508
  const [ok, err] = await tryFn(() => this.client.deleteObject(key));
1212
1509
  if (!ok) {
1213
- throw new Error(`PluginStorage.delete failed for key ${key}: ${err.message}`);
1510
+ throw new PluginStorageError(`Failed to delete plugin data`, {
1511
+ pluginSlug: this.pluginSlug,
1512
+ key,
1513
+ operation: "delete",
1514
+ original: err,
1515
+ suggestion: "Check S3 delete permissions"
1516
+ });
1214
1517
  }
1215
1518
  }
1216
1519
  /**
@@ -1397,16 +1700,28 @@ class PluginStorage {
1397
1700
  const valueSize = calculateUTF8Bytes(encoded);
1398
1701
  currentSize += keySize + valueSize;
1399
1702
  if (currentSize > effectiveLimit) {
1400
- throw new Error(
1401
- `Data exceeds metadata limit (${currentSize} > ${effectiveLimit} bytes). Use 'body-overflow' or 'body-only' behavior.`
1402
- );
1703
+ throw new MetadataLimitError(`Data exceeds metadata limit with enforce-limits behavior`, {
1704
+ totalSize: currentSize,
1705
+ effectiveLimit,
1706
+ absoluteLimit: S3_METADATA_LIMIT,
1707
+ excess: currentSize - effectiveLimit,
1708
+ operation: "PluginStorage.set",
1709
+ pluginSlug: this.pluginSlug,
1710
+ suggestion: "Use 'body-overflow' or 'body-only' behavior to handle large data"
1711
+ });
1403
1712
  }
1404
1713
  metadata[key] = jsonValue;
1405
1714
  }
1406
1715
  break;
1407
1716
  }
1408
1717
  default:
1409
- throw new Error(`Unknown behavior: ${behavior}. Use 'body-overflow', 'body-only', or 'enforce-limits'.`);
1718
+ throw new BehaviorError(`Unknown behavior: ${behavior}`, {
1719
+ behavior,
1720
+ availableBehaviors: ["body-overflow", "body-only", "enforce-limits"],
1721
+ operation: "PluginStorage._applyBehavior",
1722
+ pluginSlug: this.pluginSlug,
1723
+ suggestion: "Use 'body-overflow', 'body-only', or 'enforce-limits'"
1724
+ });
1410
1725
  }
1411
1726
  return { metadata, body };
1412
1727
  }
@@ -1971,6 +2286,35 @@ class AuditPlugin extends Plugin {
1971
2286
  }
1972
2287
  }
1973
2288
 
2289
+ class BackupError extends S3dbError {
2290
+ constructor(message, details = {}) {
2291
+ const { driver = "unknown", operation = "unknown", backupId, ...rest } = details;
2292
+ let description = details.description;
2293
+ if (!description) {
2294
+ description = `
2295
+ Backup Operation Error
2296
+
2297
+ Driver: ${driver}
2298
+ Operation: ${operation}
2299
+ ${backupId ? `Backup ID: ${backupId}` : ""}
2300
+
2301
+ Common causes:
2302
+ 1. Invalid backup driver configuration
2303
+ 2. Destination storage not accessible
2304
+ 3. Insufficient permissions
2305
+ 4. Network connectivity issues
2306
+ 5. Invalid backup file format
2307
+
2308
+ Solution:
2309
+ Check driver configuration and ensure destination storage is accessible.
2310
+
2311
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/backup.md
2312
+ `.trim();
2313
+ }
2314
+ super(message, { ...rest, driver, operation, backupId, description });
2315
+ }
2316
+ }
2317
+
1974
2318
  class BaseBackupDriver {
1975
2319
  constructor(config = {}) {
1976
2320
  this.config = {
@@ -2001,7 +2345,12 @@ class BaseBackupDriver {
2001
2345
  * @returns {Object} Upload result with destination info
2002
2346
  */
2003
2347
  async upload(filePath, backupId, manifest) {
2004
- throw new Error("upload() method must be implemented by subclass");
2348
+ throw new BackupError("upload() method must be implemented by subclass", {
2349
+ operation: "upload",
2350
+ driver: this.constructor.name,
2351
+ backupId,
2352
+ suggestion: "Extend BaseBackupDriver and implement the upload() method"
2353
+ });
2005
2354
  }
2006
2355
  /**
2007
2356
  * Download a backup file from the destination
@@ -2011,7 +2360,12 @@ class BaseBackupDriver {
2011
2360
  * @returns {string} Path to downloaded file
2012
2361
  */
2013
2362
  async download(backupId, targetPath, metadata) {
2014
- throw new Error("download() method must be implemented by subclass");
2363
+ throw new BackupError("download() method must be implemented by subclass", {
2364
+ operation: "download",
2365
+ driver: this.constructor.name,
2366
+ backupId,
2367
+ suggestion: "Extend BaseBackupDriver and implement the download() method"
2368
+ });
2015
2369
  }
2016
2370
  /**
2017
2371
  * Delete a backup from the destination
@@ -2019,7 +2373,12 @@ class BaseBackupDriver {
2019
2373
  * @param {Object} metadata - Backup metadata
2020
2374
  */
2021
2375
  async delete(backupId, metadata) {
2022
- throw new Error("delete() method must be implemented by subclass");
2376
+ throw new BackupError("delete() method must be implemented by subclass", {
2377
+ operation: "delete",
2378
+ driver: this.constructor.name,
2379
+ backupId,
2380
+ suggestion: "Extend BaseBackupDriver and implement the delete() method"
2381
+ });
2023
2382
  }
2024
2383
  /**
2025
2384
  * List backups available in the destination
@@ -2027,7 +2386,11 @@ class BaseBackupDriver {
2027
2386
  * @returns {Array} List of backup metadata
2028
2387
  */
2029
2388
  async list(options = {}) {
2030
- throw new Error("list() method must be implemented by subclass");
2389
+ throw new BackupError("list() method must be implemented by subclass", {
2390
+ operation: "list",
2391
+ driver: this.constructor.name,
2392
+ suggestion: "Extend BaseBackupDriver and implement the list() method"
2393
+ });
2031
2394
  }
2032
2395
  /**
2033
2396
  * Verify backup integrity
@@ -2037,14 +2400,23 @@ class BaseBackupDriver {
2037
2400
  * @returns {boolean} True if backup is valid
2038
2401
  */
2039
2402
  async verify(backupId, expectedChecksum, metadata) {
2040
- throw new Error("verify() method must be implemented by subclass");
2403
+ throw new BackupError("verify() method must be implemented by subclass", {
2404
+ operation: "verify",
2405
+ driver: this.constructor.name,
2406
+ backupId,
2407
+ suggestion: "Extend BaseBackupDriver and implement the verify() method"
2408
+ });
2041
2409
  }
2042
2410
  /**
2043
2411
  * Get driver type identifier
2044
2412
  * @returns {string} Driver type
2045
2413
  */
2046
2414
  getType() {
2047
- throw new Error("getType() method must be implemented by subclass");
2415
+ throw new BackupError("getType() method must be implemented by subclass", {
2416
+ operation: "getType",
2417
+ driver: this.constructor.name,
2418
+ suggestion: "Extend BaseBackupDriver and implement the getType() method"
2419
+ });
2048
2420
  }
2049
2421
  /**
2050
2422
  * Get driver-specific storage info
@@ -2086,7 +2458,11 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2086
2458
  }
2087
2459
  async onSetup() {
2088
2460
  if (!this.config.path) {
2089
- throw new Error("FilesystemBackupDriver: path configuration is required");
2461
+ throw new BackupError("FilesystemBackupDriver: path configuration is required", {
2462
+ operation: "onSetup",
2463
+ driver: "filesystem",
2464
+ suggestion: 'Provide a path in config: new FilesystemBackupDriver({ path: "/path/to/backups" })'
2465
+ });
2090
2466
  }
2091
2467
  this.log(`Initialized with path: ${this.config.path}`);
2092
2468
  }
@@ -2110,11 +2486,26 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2110
2486
  () => mkdir(targetDir, { recursive: true, mode: this.config.directoryPermissions })
2111
2487
  );
2112
2488
  if (!createDirOk) {
2113
- throw new Error(`Failed to create backup directory: ${createDirErr.message}`);
2489
+ throw new BackupError("Failed to create backup directory", {
2490
+ operation: "upload",
2491
+ driver: "filesystem",
2492
+ backupId,
2493
+ targetDir,
2494
+ original: createDirErr,
2495
+ suggestion: "Check directory permissions and disk space"
2496
+ });
2114
2497
  }
2115
2498
  const [copyOk, copyErr] = await tryFn(() => copyFile(filePath, targetPath));
2116
2499
  if (!copyOk) {
2117
- throw new Error(`Failed to copy backup file: ${copyErr.message}`);
2500
+ throw new BackupError("Failed to copy backup file", {
2501
+ operation: "upload",
2502
+ driver: "filesystem",
2503
+ backupId,
2504
+ filePath,
2505
+ targetPath,
2506
+ original: copyErr,
2507
+ suggestion: "Check file permissions and disk space"
2508
+ });
2118
2509
  }
2119
2510
  const [manifestOk, manifestErr] = await tryFn(
2120
2511
  () => import('fs/promises').then((fs) => fs.writeFile(
@@ -2125,7 +2516,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2125
2516
  );
2126
2517
  if (!manifestOk) {
2127
2518
  await tryFn(() => unlink(targetPath));
2128
- throw new Error(`Failed to write manifest: ${manifestErr.message}`);
2519
+ throw new BackupError("Failed to write manifest file", {
2520
+ operation: "upload",
2521
+ driver: "filesystem",
2522
+ backupId,
2523
+ manifestPath,
2524
+ original: manifestErr,
2525
+ suggestion: "Check directory permissions and disk space"
2526
+ });
2129
2527
  }
2130
2528
  const [statOk, , stats] = await tryFn(() => stat(targetPath));
2131
2529
  const size = statOk ? stats.size : 0;
@@ -2144,13 +2542,27 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2144
2542
  );
2145
2543
  const [existsOk] = await tryFn(() => access(sourcePath));
2146
2544
  if (!existsOk) {
2147
- throw new Error(`Backup file not found: ${sourcePath}`);
2545
+ throw new BackupError("Backup file not found", {
2546
+ operation: "download",
2547
+ driver: "filesystem",
2548
+ backupId,
2549
+ sourcePath,
2550
+ suggestion: "Check if backup exists using list() method"
2551
+ });
2148
2552
  }
2149
2553
  const targetDir = path.dirname(targetPath);
2150
2554
  await tryFn(() => mkdir(targetDir, { recursive: true }));
2151
2555
  const [copyOk, copyErr] = await tryFn(() => copyFile(sourcePath, targetPath));
2152
2556
  if (!copyOk) {
2153
- throw new Error(`Failed to download backup: ${copyErr.message}`);
2557
+ throw new BackupError("Failed to download backup", {
2558
+ operation: "download",
2559
+ driver: "filesystem",
2560
+ backupId,
2561
+ sourcePath,
2562
+ targetPath,
2563
+ original: copyErr,
2564
+ suggestion: "Check file permissions and disk space"
2565
+ });
2154
2566
  }
2155
2567
  this.log(`Downloaded backup ${backupId} from ${sourcePath} to ${targetPath}`);
2156
2568
  return targetPath;
@@ -2167,7 +2579,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
2167
2579
  const [deleteBackupOk] = await tryFn(() => unlink(backupPath));
2168
2580
  const [deleteManifestOk] = await tryFn(() => unlink(manifestPath));
2169
2581
  if (!deleteBackupOk && !deleteManifestOk) {
2170
- throw new Error(`Failed to delete backup files for ${backupId}`);
2582
+ throw new BackupError("Failed to delete backup files", {
2583
+ operation: "delete",
2584
+ driver: "filesystem",
2585
+ backupId,
2586
+ backupPath,
2587
+ manifestPath,
2588
+ suggestion: "Check file permissions"
2589
+ });
2171
2590
  }
2172
2591
  this.log(`Deleted backup ${backupId}`);
2173
2592
  }
@@ -2272,10 +2691,18 @@ class S3BackupDriver extends BaseBackupDriver {
2272
2691
  this.config.bucket = this.database.bucket;
2273
2692
  }
2274
2693
  if (!this.config.client) {
2275
- throw new Error("S3BackupDriver: client is required (either via config or database)");
2694
+ throw new BackupError("S3BackupDriver: client is required", {
2695
+ operation: "onSetup",
2696
+ driver: "s3",
2697
+ suggestion: "Provide a client in config or ensure database has a client configured"
2698
+ });
2276
2699
  }
2277
2700
  if (!this.config.bucket) {
2278
- throw new Error("S3BackupDriver: bucket is required (either via config or database)");
2701
+ throw new BackupError("S3BackupDriver: bucket is required", {
2702
+ operation: "onSetup",
2703
+ driver: "s3",
2704
+ suggestion: "Provide a bucket in config or ensure database has a bucket configured"
2705
+ });
2279
2706
  }
2280
2707
  this.log(`Initialized with bucket: ${this.config.bucket}, path: ${this.config.path}`);
2281
2708
  }
@@ -2317,7 +2744,15 @@ class S3BackupDriver extends BaseBackupDriver {
2317
2744
  });
2318
2745
  });
2319
2746
  if (!uploadOk) {
2320
- throw new Error(`Failed to upload backup file: ${uploadErr.message}`);
2747
+ throw new BackupError("Failed to upload backup file to S3", {
2748
+ operation: "upload",
2749
+ driver: "s3",
2750
+ backupId,
2751
+ bucket: this.config.bucket,
2752
+ key: backupKey,
2753
+ original: uploadErr,
2754
+ suggestion: "Check S3 permissions and bucket configuration"
2755
+ });
2321
2756
  }
2322
2757
  const [manifestOk, manifestErr] = await tryFn(
2323
2758
  () => this.config.client.uploadObject({
@@ -2338,7 +2773,15 @@ class S3BackupDriver extends BaseBackupDriver {
2338
2773
  bucket: this.config.bucket,
2339
2774
  key: backupKey
2340
2775
  }));
2341
- throw new Error(`Failed to upload manifest: ${manifestErr.message}`);
2776
+ throw new BackupError("Failed to upload manifest to S3", {
2777
+ operation: "upload",
2778
+ driver: "s3",
2779
+ backupId,
2780
+ bucket: this.config.bucket,
2781
+ manifestKey,
2782
+ original: manifestErr,
2783
+ suggestion: "Check S3 permissions and bucket configuration"
2784
+ });
2342
2785
  }
2343
2786
  this.log(`Uploaded backup ${backupId} to s3://${this.config.bucket}/${backupKey} (${fileSize} bytes)`);
2344
2787
  return {
@@ -2361,7 +2804,16 @@ class S3BackupDriver extends BaseBackupDriver {
2361
2804
  })
2362
2805
  );
2363
2806
  if (!downloadOk) {
2364
- throw new Error(`Failed to download backup: ${downloadErr.message}`);
2807
+ throw new BackupError("Failed to download backup from S3", {
2808
+ operation: "download",
2809
+ driver: "s3",
2810
+ backupId,
2811
+ bucket: this.config.bucket,
2812
+ key: backupKey,
2813
+ targetPath,
2814
+ original: downloadErr,
2815
+ suggestion: "Check if backup exists and S3 permissions are correct"
2816
+ });
2365
2817
  }
2366
2818
  this.log(`Downloaded backup ${backupId} from s3://${this.config.bucket}/${backupKey} to ${targetPath}`);
2367
2819
  return targetPath;
@@ -2382,7 +2834,15 @@ class S3BackupDriver extends BaseBackupDriver {
2382
2834
  })
2383
2835
  );
2384
2836
  if (!deleteBackupOk && !deleteManifestOk) {
2385
- throw new Error(`Failed to delete backup objects for ${backupId}`);
2837
+ throw new BackupError("Failed to delete backup from S3", {
2838
+ operation: "delete",
2839
+ driver: "s3",
2840
+ backupId,
2841
+ bucket: this.config.bucket,
2842
+ backupKey,
2843
+ manifestKey,
2844
+ suggestion: "Check S3 delete permissions"
2845
+ });
2386
2846
  }
2387
2847
  this.log(`Deleted backup ${backupId} from S3`);
2388
2848
  }
@@ -2495,11 +2955,22 @@ class MultiBackupDriver extends BaseBackupDriver {
2495
2955
  }
2496
2956
  async onSetup() {
2497
2957
  if (!Array.isArray(this.config.destinations) || this.config.destinations.length === 0) {
2498
- throw new Error("MultiBackupDriver: destinations array is required and must not be empty");
2958
+ throw new BackupError("MultiBackupDriver requires non-empty destinations array", {
2959
+ operation: "onSetup",
2960
+ driver: "multi",
2961
+ destinationsProvided: this.config.destinations,
2962
+ suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }, { driver: "filesystem", config: {...} }] }'
2963
+ });
2499
2964
  }
2500
2965
  for (const [index, destConfig] of this.config.destinations.entries()) {
2501
2966
  if (!destConfig.driver) {
2502
- throw new Error(`MultiBackupDriver: destination[${index}] must have a driver type`);
2967
+ throw new BackupError(`Destination ${index} missing driver type`, {
2968
+ operation: "onSetup",
2969
+ driver: "multi",
2970
+ destinationIndex: index,
2971
+ destination: destConfig,
2972
+ suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} } or { driver: "filesystem", config: {...} }'
2973
+ });
2503
2974
  }
2504
2975
  try {
2505
2976
  const driver = createBackupDriver(destConfig.driver, destConfig.config || {});
@@ -2511,7 +2982,15 @@ class MultiBackupDriver extends BaseBackupDriver {
2511
2982
  });
2512
2983
  this.log(`Setup destination ${index}: ${destConfig.driver}`);
2513
2984
  } catch (error) {
2514
- throw new Error(`Failed to setup destination ${index} (${destConfig.driver}): ${error.message}`);
2985
+ throw new BackupError(`Failed to setup destination ${index}`, {
2986
+ operation: "onSetup",
2987
+ driver: "multi",
2988
+ destinationIndex: index,
2989
+ destinationDriver: destConfig.driver,
2990
+ destinationConfig: destConfig.config,
2991
+ original: error,
2992
+ suggestion: "Check destination driver configuration and ensure dependencies are available"
2993
+ });
2515
2994
  }
2516
2995
  }
2517
2996
  if (this.config.requireAll === false) {
@@ -2540,7 +3019,15 @@ class MultiBackupDriver extends BaseBackupDriver {
2540
3019
  this.log(`Priority upload failed to destination ${index}: ${err.message}`);
2541
3020
  }
2542
3021
  }
2543
- throw new Error(`All priority destinations failed: ${errors.map((e) => `${e.destination}: ${e.error}`).join("; ")}`);
3022
+ throw new BackupError("All priority destinations failed", {
3023
+ operation: "upload",
3024
+ driver: "multi",
3025
+ strategy: "priority",
3026
+ backupId,
3027
+ totalDestinations: this.drivers.length,
3028
+ failures: errors,
3029
+ suggestion: "Check destination configurations and ensure at least one destination is accessible"
3030
+ });
2544
3031
  }
2545
3032
  const uploadPromises = this.drivers.map(async ({ driver, config, index }) => {
2546
3033
  const [ok, err, result] = await tryFn(
@@ -2570,10 +3057,28 @@ class MultiBackupDriver extends BaseBackupDriver {
2570
3057
  const successResults = allResults.filter((r) => r.status === "success");
2571
3058
  const failedResults = allResults.filter((r) => r.status === "failed");
2572
3059
  if (strategy === "all" && failedResults.length > 0) {
2573
- throw new Error(`Some destinations failed: ${failedResults.map((r) => `${r.destination}: ${r.error}`).join("; ")}`);
3060
+ throw new BackupError('Some destinations failed with strategy "all"', {
3061
+ operation: "upload",
3062
+ driver: "multi",
3063
+ strategy: "all",
3064
+ backupId,
3065
+ totalDestinations: this.drivers.length,
3066
+ successCount: successResults.length,
3067
+ failedCount: failedResults.length,
3068
+ failures: failedResults,
3069
+ suggestion: 'All destinations must succeed with "all" strategy. Use "any" strategy to tolerate failures, or fix failing destinations.'
3070
+ });
2574
3071
  }
2575
3072
  if (strategy === "any" && successResults.length === 0) {
2576
- throw new Error(`All destinations failed: ${failedResults.map((r) => `${r.destination}: ${r.error}`).join("; ")}`);
3073
+ throw new BackupError('All destinations failed with strategy "any"', {
3074
+ operation: "upload",
3075
+ driver: "multi",
3076
+ strategy: "any",
3077
+ backupId,
3078
+ totalDestinations: this.drivers.length,
3079
+ failures: failedResults,
3080
+ suggestion: 'At least one destination must succeed with "any" strategy. Check all destination configurations.'
3081
+ });
2577
3082
  }
2578
3083
  return allResults;
2579
3084
  }
@@ -2593,7 +3098,14 @@ class MultiBackupDriver extends BaseBackupDriver {
2593
3098
  this.log(`Download failed from destination ${destMetadata.destination}: ${err.message}`);
2594
3099
  }
2595
3100
  }
2596
- throw new Error(`Failed to download backup from any destination`);
3101
+ throw new BackupError("Failed to download backup from any destination", {
3102
+ operation: "download",
3103
+ driver: "multi",
3104
+ backupId,
3105
+ targetPath,
3106
+ attemptedDestinations: destinations.length,
3107
+ suggestion: "Check if backup exists in at least one destination and destinations are accessible"
3108
+ });
2597
3109
  }
2598
3110
  async delete(backupId, metadata) {
2599
3111
  const destinations = Array.isArray(metadata.destinations) ? metadata.destinations : [metadata];
@@ -2615,7 +3127,14 @@ class MultiBackupDriver extends BaseBackupDriver {
2615
3127
  }
2616
3128
  }
2617
3129
  if (successCount === 0 && errors.length > 0) {
2618
- throw new Error(`Failed to delete from any destination: ${errors.join("; ")}`);
3130
+ throw new BackupError("Failed to delete from any destination", {
3131
+ operation: "delete",
3132
+ driver: "multi",
3133
+ backupId,
3134
+ attemptedDestinations: destinations.length,
3135
+ failures: errors,
3136
+ suggestion: "Check if backup exists in destinations and destinations are accessible with delete permissions"
3137
+ });
2619
3138
  }
2620
3139
  if (errors.length > 0) {
2621
3140
  this.log(`Partial delete success, some errors: ${errors.join("; ")}`);
@@ -2715,32 +3234,62 @@ const BACKUP_DRIVERS = {
2715
3234
  function createBackupDriver(driver, config = {}) {
2716
3235
  const DriverClass = BACKUP_DRIVERS[driver];
2717
3236
  if (!DriverClass) {
2718
- throw new Error(`Unknown backup driver: ${driver}. Available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`);
3237
+ throw new BackupError(`Unknown backup driver: ${driver}`, {
3238
+ operation: "createBackupDriver",
3239
+ driver,
3240
+ availableDrivers: Object.keys(BACKUP_DRIVERS),
3241
+ suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
3242
+ });
2719
3243
  }
2720
3244
  return new DriverClass(config);
2721
3245
  }
2722
3246
  function validateBackupConfig(driver, config = {}) {
2723
3247
  if (!driver || typeof driver !== "string") {
2724
- throw new Error("Driver type must be a non-empty string");
3248
+ throw new BackupError("Driver type must be a non-empty string", {
3249
+ operation: "validateBackupConfig",
3250
+ driver,
3251
+ suggestion: "Provide a valid driver type string (filesystem, s3, or multi)"
3252
+ });
2725
3253
  }
2726
3254
  if (!BACKUP_DRIVERS[driver]) {
2727
- throw new Error(`Unknown backup driver: ${driver}. Available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`);
3255
+ throw new BackupError(`Unknown backup driver: ${driver}`, {
3256
+ operation: "validateBackupConfig",
3257
+ driver,
3258
+ availableDrivers: Object.keys(BACKUP_DRIVERS),
3259
+ suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
3260
+ });
2728
3261
  }
2729
3262
  switch (driver) {
2730
3263
  case "filesystem":
2731
3264
  if (!config.path) {
2732
- throw new Error('FilesystemBackupDriver requires "path" configuration');
3265
+ throw new BackupError('FilesystemBackupDriver requires "path" configuration', {
3266
+ operation: "validateBackupConfig",
3267
+ driver: "filesystem",
3268
+ config,
3269
+ suggestion: 'Provide a "path" property in config: { path: "/path/to/backups" }'
3270
+ });
2733
3271
  }
2734
3272
  break;
2735
3273
  case "s3":
2736
3274
  break;
2737
3275
  case "multi":
2738
3276
  if (!Array.isArray(config.destinations) || config.destinations.length === 0) {
2739
- throw new Error('MultiBackupDriver requires non-empty "destinations" array');
3277
+ throw new BackupError('MultiBackupDriver requires non-empty "destinations" array', {
3278
+ operation: "validateBackupConfig",
3279
+ driver: "multi",
3280
+ config,
3281
+ suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }] }'
3282
+ });
2740
3283
  }
2741
3284
  config.destinations.forEach((dest, index) => {
2742
3285
  if (!dest.driver) {
2743
- throw new Error(`Destination ${index} must have a "driver" property`);
3286
+ throw new BackupError(`Destination ${index} must have a "driver" property`, {
3287
+ operation: "validateBackupConfig",
3288
+ driver: "multi",
3289
+ destinationIndex: index,
3290
+ destination: dest,
3291
+ suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} }'
3292
+ });
2744
3293
  }
2745
3294
  if (dest.driver !== "multi") {
2746
3295
  validateBackupConfig(dest.driver, dest.config || {});
@@ -3396,6 +3945,36 @@ class BackupPlugin extends Plugin {
3396
3945
  }
3397
3946
  }
3398
3947
 
3948
+ class CacheError extends S3dbError {
3949
+ constructor(message, details = {}) {
3950
+ const { driver = "unknown", operation = "unknown", resourceName, key, ...rest } = details;
3951
+ let description = details.description;
3952
+ if (!description) {
3953
+ description = `
3954
+ Cache Operation Error
3955
+
3956
+ Driver: ${driver}
3957
+ Operation: ${operation}
3958
+ ${resourceName ? `Resource: ${resourceName}` : ""}
3959
+ ${key ? `Key: ${key}` : ""}
3960
+
3961
+ Common causes:
3962
+ 1. Invalid cache key format
3963
+ 2. Cache driver not properly initialized
3964
+ 3. Resource not found or not cached
3965
+ 4. Memory limits exceeded
3966
+ 5. Filesystem permissions issues
3967
+
3968
+ Solution:
3969
+ Check cache configuration and ensure the cache driver is properly initialized.
3970
+
3971
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/cache.md
3972
+ `.trim();
3973
+ }
3974
+ super(message, { ...rest, driver, operation, resourceName, key, description });
3975
+ }
3976
+ }
3977
+
3399
3978
  class Cache extends EventEmitter {
3400
3979
  constructor(config = {}) {
3401
3980
  super();
@@ -3412,7 +3991,13 @@ class Cache extends EventEmitter {
3412
3991
  }
3413
3992
  validateKey(key) {
3414
3993
  if (key === null || key === void 0 || typeof key !== "string" || !key) {
3415
- throw new Error("Invalid key");
3994
+ throw new CacheError("Invalid cache key", {
3995
+ operation: "validateKey",
3996
+ driver: this.constructor.name,
3997
+ key,
3998
+ keyType: typeof key,
3999
+ suggestion: "Cache key must be a non-empty string"
4000
+ });
3416
4001
  }
3417
4002
  }
3418
4003
  // generic class methods
@@ -3499,7 +4084,11 @@ class ResourceReader extends EventEmitter {
3499
4084
  constructor({ resource, batchSize = 10, concurrency = 5 }) {
3500
4085
  super();
3501
4086
  if (!resource) {
3502
- throw new Error("Resource is required for ResourceReader");
4087
+ throw new StreamError("Resource is required for ResourceReader", {
4088
+ operation: "constructor",
4089
+ resource: resource?.name,
4090
+ suggestion: "Pass a valid Resource instance when creating ResourceReader"
4091
+ });
3503
4092
  }
3504
4093
  this.resource = resource;
3505
4094
  this.client = resource.client;
@@ -3623,7 +4212,10 @@ class ResourceWriter extends EventEmitter {
3623
4212
  function streamToString(stream) {
3624
4213
  return new Promise((resolve, reject) => {
3625
4214
  if (!stream) {
3626
- return reject(new Error("streamToString: stream is undefined"));
4215
+ return reject(new StreamError("Stream is undefined", {
4216
+ operation: "streamToString",
4217
+ suggestion: "Ensure a valid stream is passed to streamToString()"
4218
+ }));
3627
4219
  }
3628
4220
  const chunks = [];
3629
4221
  stream.on("data", (chunk) => chunks.push(chunk));
@@ -5117,7 +5709,13 @@ class CachePlugin extends Plugin {
5117
5709
  async warmCache(resourceName, options = {}) {
5118
5710
  const resource = this.database.resources[resourceName];
5119
5711
  if (!resource) {
5120
- throw new Error(`Resource '${resourceName}' not found`);
5712
+ throw new CacheError("Resource not found for cache warming", {
5713
+ operation: "warmCache",
5714
+ driver: this.driver?.constructor.name,
5715
+ resourceName,
5716
+ availableResources: Object.keys(this.database.resources),
5717
+ suggestion: "Check resource name spelling or ensure resource has been created"
5718
+ });
5121
5719
  }
5122
5720
  const { includePartitions = true, sampleSize = 100 } = options;
5123
5721
  if (this.driver instanceof PartitionAwareFilesystemCache && resource.warmPartitionCache) {
@@ -8234,6 +8832,35 @@ class EventualConsistencyPlugin extends Plugin {
8234
8832
  }
8235
8833
  }
8236
8834
 
8835
+ class FulltextError extends S3dbError {
8836
+ constructor(message, details = {}) {
8837
+ const { resourceName, query, operation = "unknown", ...rest } = details;
8838
+ let description = details.description;
8839
+ if (!description) {
8840
+ description = `
8841
+ Fulltext Search Operation Error
8842
+
8843
+ Operation: ${operation}
8844
+ ${resourceName ? `Resource: ${resourceName}` : ""}
8845
+ ${query ? `Query: ${query}` : ""}
8846
+
8847
+ Common causes:
8848
+ 1. Resource not indexed for fulltext search
8849
+ 2. Invalid query syntax
8850
+ 3. Index not built yet
8851
+ 4. Search configuration missing
8852
+ 5. Field not indexed
8853
+
8854
+ Solution:
8855
+ Ensure resource is configured for fulltext search and index is built.
8856
+
8857
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/fulltext.md
8858
+ `.trim();
8859
+ }
8860
+ super(message, { ...rest, resourceName, query, operation, description });
8861
+ }
8862
+ }
8863
+
8237
8864
  class FullTextPlugin extends Plugin {
8238
8865
  constructor(options = {}) {
8239
8866
  super();
@@ -8540,7 +9167,13 @@ class FullTextPlugin extends Plugin {
8540
9167
  }
8541
9168
  const resource = this.database.resources[resourceName];
8542
9169
  if (!resource) {
8543
- throw new Error(`Resource '${resourceName}' not found`);
9170
+ throw new FulltextError(`Resource '${resourceName}' not found`, {
9171
+ operation: "searchRecords",
9172
+ resourceName,
9173
+ query,
9174
+ availableResources: Object.keys(this.database.resources),
9175
+ suggestion: "Check resource name or ensure resource is created before searching"
9176
+ });
8544
9177
  }
8545
9178
  const recordIds = searchResults.map((result2) => result2.recordId);
8546
9179
  const records = await resource.getMany(recordIds);
@@ -8557,7 +9190,12 @@ class FullTextPlugin extends Plugin {
8557
9190
  async rebuildIndex(resourceName) {
8558
9191
  const resource = this.database.resources[resourceName];
8559
9192
  if (!resource) {
8560
- throw new Error(`Resource '${resourceName}' not found`);
9193
+ throw new FulltextError(`Resource '${resourceName}' not found`, {
9194
+ operation: "rebuildIndex",
9195
+ resourceName,
9196
+ availableResources: Object.keys(this.database.resources),
9197
+ suggestion: "Check resource name or ensure resource is created before rebuilding index"
9198
+ });
8561
9199
  }
8562
9200
  for (const [key] of this.indexes.entries()) {
8563
9201
  if (key.startsWith(`${resourceName}:`)) {
@@ -9342,6 +9980,35 @@ function createConsumer(driver, config) {
9342
9980
  return new ConsumerClass(config);
9343
9981
  }
9344
9982
 
9983
+ class QueueError extends S3dbError {
9984
+ constructor(message, details = {}) {
9985
+ const { queueName, operation = "unknown", messageId, ...rest } = details;
9986
+ let description = details.description;
9987
+ if (!description) {
9988
+ description = `
9989
+ Queue Operation Error
9990
+
9991
+ Operation: ${operation}
9992
+ ${queueName ? `Queue: ${queueName}` : ""}
9993
+ ${messageId ? `Message ID: ${messageId}` : ""}
9994
+
9995
+ Common causes:
9996
+ 1. Queue not properly configured
9997
+ 2. Message handler not registered
9998
+ 3. Queue resource not found
9999
+ 4. SQS/RabbitMQ connection failed
10000
+ 5. Message processing timeout
10001
+
10002
+ Solution:
10003
+ Check queue configuration and message handler registration.
10004
+
10005
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue.md
10006
+ `.trim();
10007
+ }
10008
+ super(message, { ...rest, queueName, operation, messageId, description });
10009
+ }
10010
+ }
10011
+
9345
10012
  class QueueConsumerPlugin extends Plugin {
9346
10013
  constructor(options = {}) {
9347
10014
  super(options);
@@ -9402,13 +10069,32 @@ class QueueConsumerPlugin extends Plugin {
9402
10069
  let action = body.action || msg.action;
9403
10070
  let data = body.data || msg.data;
9404
10071
  if (!resource) {
9405
- throw new Error("QueueConsumerPlugin: resource not found in message");
10072
+ throw new QueueError("Resource not found in message", {
10073
+ operation: "handleMessage",
10074
+ queueName: configuredResource,
10075
+ messageBody: body,
10076
+ suggestion: 'Ensure message includes a "resource" field specifying the target resource name'
10077
+ });
9406
10078
  }
9407
10079
  if (!action) {
9408
- throw new Error("QueueConsumerPlugin: action not found in message");
10080
+ throw new QueueError("Action not found in message", {
10081
+ operation: "handleMessage",
10082
+ queueName: configuredResource,
10083
+ resource,
10084
+ messageBody: body,
10085
+ suggestion: 'Ensure message includes an "action" field (insert, update, or delete)'
10086
+ });
9409
10087
  }
9410
10088
  const resourceObj = this.database.resources[resource];
9411
- if (!resourceObj) throw new Error(`QueueConsumerPlugin: resource '${resource}' not found`);
10089
+ if (!resourceObj) {
10090
+ throw new QueueError(`Resource '${resource}' not found`, {
10091
+ operation: "handleMessage",
10092
+ queueName: configuredResource,
10093
+ resource,
10094
+ availableResources: Object.keys(this.database.resources),
10095
+ suggestion: "Check resource name or ensure resource is created before consuming messages"
10096
+ });
10097
+ }
9412
10098
  let result;
9413
10099
  const [ok, err, res] = await tryFn(async () => {
9414
10100
  if (action === "insert") {
@@ -9419,7 +10105,14 @@ class QueueConsumerPlugin extends Plugin {
9419
10105
  } else if (action === "delete") {
9420
10106
  result = await resourceObj.delete(data.id);
9421
10107
  } else {
9422
- throw new Error(`QueueConsumerPlugin: unsupported action '${action}'`);
10108
+ throw new QueueError(`Unsupported action '${action}'`, {
10109
+ operation: "handleMessage",
10110
+ queueName: configuredResource,
10111
+ resource,
10112
+ action,
10113
+ supportedActions: ["insert", "update", "delete"],
10114
+ suggestion: "Use one of the supported actions: insert, update, or delete"
10115
+ });
9423
10116
  }
9424
10117
  return result;
9425
10118
  });
@@ -9432,6 +10125,35 @@ class QueueConsumerPlugin extends Plugin {
9432
10125
  }
9433
10126
  }
9434
10127
 
10128
+ class ReplicationError extends S3dbError {
10129
+ constructor(message, details = {}) {
10130
+ const { replicatorClass = "unknown", operation = "unknown", resourceName, ...rest } = details;
10131
+ let description = details.description;
10132
+ if (!description) {
10133
+ description = `
10134
+ Replication Operation Error
10135
+
10136
+ Replicator: ${replicatorClass}
10137
+ Operation: ${operation}
10138
+ ${resourceName ? `Resource: ${resourceName}` : ""}
10139
+
10140
+ Common causes:
10141
+ 1. Invalid replicator configuration
10142
+ 2. Target system not accessible
10143
+ 3. Resource not configured for replication
10144
+ 4. Invalid operation type
10145
+ 5. Transformation function errors
10146
+
10147
+ Solution:
10148
+ Check replicator configuration and ensure target system is accessible.
10149
+
10150
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md
10151
+ `.trim();
10152
+ }
10153
+ super(message, { ...rest, replicatorClass, operation, resourceName, description });
10154
+ }
10155
+ }
10156
+
9435
10157
  class BaseReplicator extends EventEmitter {
9436
10158
  constructor(config = {}) {
9437
10159
  super();
@@ -9457,7 +10179,12 @@ class BaseReplicator extends EventEmitter {
9457
10179
  * @returns {Promise<Object>} replicator result
9458
10180
  */
9459
10181
  async replicate(resourceName, operation, data, id) {
9460
- throw new Error(`replicate() method must be implemented by ${this.name}`);
10182
+ throw new ReplicationError("replicate() method must be implemented by subclass", {
10183
+ operation: "replicate",
10184
+ replicatorClass: this.name,
10185
+ resourceName,
10186
+ suggestion: "Extend BaseReplicator and implement the replicate() method"
10187
+ });
9461
10188
  }
9462
10189
  /**
9463
10190
  * Replicate multiple records in batch
@@ -9466,14 +10193,24 @@ class BaseReplicator extends EventEmitter {
9466
10193
  * @returns {Promise<Object>} Batch replicator result
9467
10194
  */
9468
10195
  async replicateBatch(resourceName, records) {
9469
- throw new Error(`replicateBatch() method must be implemented by ${this.name}`);
10196
+ throw new ReplicationError("replicateBatch() method must be implemented by subclass", {
10197
+ operation: "replicateBatch",
10198
+ replicatorClass: this.name,
10199
+ resourceName,
10200
+ batchSize: records?.length,
10201
+ suggestion: "Extend BaseReplicator and implement the replicateBatch() method"
10202
+ });
9470
10203
  }
9471
10204
  /**
9472
10205
  * Test the connection to the target
9473
10206
  * @returns {Promise<boolean>} True if connection is successful
9474
10207
  */
9475
10208
  async testConnection() {
9476
- throw new Error(`testConnection() method must be implemented by ${this.name}`);
10209
+ throw new ReplicationError("testConnection() method must be implemented by subclass", {
10210
+ operation: "testConnection",
10211
+ replicatorClass: this.name,
10212
+ suggestion: "Extend BaseReplicator and implement the testConnection() method"
10213
+ });
9477
10214
  }
9478
10215
  /**
9479
10216
  * Get replicator status and statistics
@@ -10645,7 +11382,17 @@ class Client extends EventEmitter {
10645
11382
  });
10646
11383
  this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
10647
11384
  if (errors.length > 0) {
10648
- throw new Error("Some objects could not be moved");
11385
+ throw new UnknownError("Some objects could not be moved", {
11386
+ bucket: this.config.bucket,
11387
+ operation: "moveAllObjects",
11388
+ prefixFrom,
11389
+ prefixTo,
11390
+ totalKeys: keys.length,
11391
+ failedCount: errors.length,
11392
+ successCount: results.length,
11393
+ errors: errors.map((e) => ({ message: e.message, raw: e.raw })),
11394
+ suggestion: "Check S3 permissions and retry failed objects individually"
11395
+ });
10649
11396
  }
10650
11397
  return results;
10651
11398
  }
@@ -11359,7 +12106,14 @@ async function handleInsert$4({ resource, data, mappedData, originalData }) {
11359
12106
  }
11360
12107
  });
11361
12108
  if (totalSize > effectiveLimit) {
11362
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12109
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on insert", {
12110
+ totalSize,
12111
+ effectiveLimit,
12112
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12113
+ excess: totalSize - effectiveLimit,
12114
+ resourceName: resource.name,
12115
+ operation: "insert"
12116
+ });
11363
12117
  }
11364
12118
  return { mappedData, body: "" };
11365
12119
  }
@@ -11374,7 +12128,15 @@ async function handleUpdate$4({ resource, id, data, mappedData, originalData })
11374
12128
  }
11375
12129
  });
11376
12130
  if (totalSize > effectiveLimit) {
11377
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12131
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on update", {
12132
+ totalSize,
12133
+ effectiveLimit,
12134
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12135
+ excess: totalSize - effectiveLimit,
12136
+ resourceName: resource.name,
12137
+ operation: "update",
12138
+ id
12139
+ });
11378
12140
  }
11379
12141
  return { mappedData, body: JSON.stringify(mappedData) };
11380
12142
  }
@@ -11389,7 +12151,15 @@ async function handleUpsert$4({ resource, id, data, mappedData }) {
11389
12151
  }
11390
12152
  });
11391
12153
  if (totalSize > effectiveLimit) {
11392
- throw new Error(`S3 metadata size exceeds 2KB limit. Current size: ${totalSize} bytes, effective limit: ${effectiveLimit} bytes, absolute limit: ${S3_METADATA_LIMIT_BYTES} bytes`);
12154
+ throw new MetadataLimitError("Metadata size exceeds 2KB limit on upsert", {
12155
+ totalSize,
12156
+ effectiveLimit,
12157
+ absoluteLimit: S3_METADATA_LIMIT_BYTES,
12158
+ excess: totalSize - effectiveLimit,
12159
+ resourceName: resource.name,
12160
+ operation: "upsert",
12161
+ id
12162
+ });
11393
12163
  }
11394
12164
  return { mappedData, body: "" };
11395
12165
  }
@@ -11731,7 +12501,11 @@ const behaviors = {
11731
12501
  function getBehavior(behaviorName) {
11732
12502
  const behavior = behaviors[behaviorName];
11733
12503
  if (!behavior) {
11734
- throw new Error(`Unknown behavior: ${behaviorName}. Available behaviors: ${Object.keys(behaviors).join(", ")}`);
12504
+ throw new BehaviorError(`Unknown behavior: ${behaviorName}`, {
12505
+ behavior: behaviorName,
12506
+ availableBehaviors: Object.keys(behaviors),
12507
+ operation: "getBehavior"
12508
+ });
11735
12509
  }
11736
12510
  return behavior;
11737
12511
  }
@@ -14255,7 +15029,7 @@ class Database extends EventEmitter {
14255
15029
  this.id = idGenerator(7);
14256
15030
  this.version = "1";
14257
15031
  this.s3dbVersion = (() => {
14258
- const [ok, err, version] = tryFn(() => true ? "11.2.3" : "latest");
15032
+ const [ok, err, version] = tryFn(() => true ? "11.2.4" : "latest");
14259
15033
  return ok ? version : "latest";
14260
15034
  })();
14261
15035
  this.resources = {};
@@ -14600,7 +15374,12 @@ class Database extends EventEmitter {
14600
15374
  const pluginName = name.toLowerCase().replace("plugin", "");
14601
15375
  const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
14602
15376
  if (!plugin) {
14603
- throw new Error(`Plugin '${name}' not found`);
15377
+ throw new DatabaseError(`Plugin '${name}' not found`, {
15378
+ operation: "uninstallPlugin",
15379
+ pluginName: name,
15380
+ availablePlugins: Object.keys(this.pluginRegistry),
15381
+ suggestion: "Check plugin name or list available plugins using Object.keys(db.pluginRegistry)"
15382
+ });
14604
15383
  }
14605
15384
  if (plugin.stop) {
14606
15385
  await plugin.stop();
@@ -15233,10 +16012,20 @@ class Database extends EventEmitter {
15233
16012
  addHook(event, fn) {
15234
16013
  if (!this._hooks) this._initHooks();
15235
16014
  if (!this._hooks.has(event)) {
15236
- throw new Error(`Unknown hook event: ${event}. Available events: ${this._hookEvents.join(", ")}`);
16015
+ throw new DatabaseError(`Unknown hook event: ${event}`, {
16016
+ operation: "addHook",
16017
+ invalidEvent: event,
16018
+ availableEvents: this._hookEvents,
16019
+ suggestion: `Use one of the available hook events: ${this._hookEvents.join(", ")}`
16020
+ });
15237
16021
  }
15238
16022
  if (typeof fn !== "function") {
15239
- throw new Error("Hook function must be a function");
16023
+ throw new DatabaseError("Hook function must be a function", {
16024
+ operation: "addHook",
16025
+ event,
16026
+ receivedType: typeof fn,
16027
+ suggestion: "Provide a function that will be called when the hook event occurs"
16028
+ });
15240
16029
  }
15241
16030
  this._hooks.get(event).push(fn);
15242
16031
  }
@@ -15374,7 +16163,11 @@ class S3dbReplicator extends BaseReplicator {
15374
16163
  this.targetDatabase = new S3db(targetConfig);
15375
16164
  await this.targetDatabase.connect();
15376
16165
  } else {
15377
- throw new Error("S3dbReplicator: No client or connectionString provided");
16166
+ throw new ReplicationError("S3dbReplicator requires client or connectionString", {
16167
+ operation: "initialize",
16168
+ replicatorClass: "S3dbReplicator",
16169
+ suggestion: 'Provide either a client instance or connectionString in config: { client: db } or { connectionString: "s3://..." }'
16170
+ });
15378
16171
  }
15379
16172
  this.emit("connected", {
15380
16173
  replicator: this.name,
@@ -15405,7 +16198,13 @@ class S3dbReplicator extends BaseReplicator {
15405
16198
  const normResource = normalizeResourceName$1(resource);
15406
16199
  const entry = this.resourcesMap[normResource];
15407
16200
  if (!entry) {
15408
- throw new Error(`[S3dbReplicator] Resource not configured: ${resource}`);
16201
+ throw new ReplicationError("Resource not configured for replication", {
16202
+ operation: "replicate",
16203
+ replicatorClass: "S3dbReplicator",
16204
+ resourceName: resource,
16205
+ configuredResources: Object.keys(this.resourcesMap),
16206
+ suggestion: 'Add resource to replicator resources map: { resources: { [resourceName]: "destination" } }'
16207
+ });
15409
16208
  }
15410
16209
  if (Array.isArray(entry)) {
15411
16210
  const results = [];
@@ -15473,7 +16272,14 @@ class S3dbReplicator extends BaseReplicator {
15473
16272
  } else if (operation === "delete") {
15474
16273
  result = await destResourceObj.delete(recordId);
15475
16274
  } else {
15476
- throw new Error(`Invalid operation: ${operation}. Supported operations are: insert, update, delete`);
16275
+ throw new ReplicationError(`Invalid replication operation: ${operation}`, {
16276
+ operation: "replicate",
16277
+ replicatorClass: "S3dbReplicator",
16278
+ invalidOperation: operation,
16279
+ supportedOperations: ["insert", "update", "delete"],
16280
+ resourceName: sourceResource,
16281
+ suggestion: "Use one of the supported operations: insert, update, delete"
16282
+ });
15477
16283
  }
15478
16284
  return result;
15479
16285
  }
@@ -15541,7 +16347,13 @@ class S3dbReplicator extends BaseReplicator {
15541
16347
  const norm = normalizeResourceName$1(resource);
15542
16348
  const found = available.find((r) => normalizeResourceName$1(r) === norm);
15543
16349
  if (!found) {
15544
- throw new Error(`[S3dbReplicator] Destination resource not found: ${resource}. Available: ${available.join(", ")}`);
16350
+ throw new ReplicationError("Destination resource not found in target database", {
16351
+ operation: "_getDestResourceObj",
16352
+ replicatorClass: "S3dbReplicator",
16353
+ destinationResource: resource,
16354
+ availableResources: available,
16355
+ suggestion: "Create the resource in target database or check resource name spelling"
16356
+ });
15545
16357
  }
15546
16358
  return db.resources[found];
15547
16359
  }
@@ -15590,7 +16402,13 @@ class S3dbReplicator extends BaseReplicator {
15590
16402
  }
15591
16403
  async testConnection() {
15592
16404
  const [ok, err] = await tryFn(async () => {
15593
- if (!this.targetDatabase) throw new Error("No target database configured");
16405
+ if (!this.targetDatabase) {
16406
+ throw new ReplicationError("No target database configured for connection test", {
16407
+ operation: "testConnection",
16408
+ replicatorClass: "S3dbReplicator",
16409
+ suggestion: "Initialize replicator with client or connectionString before testing connection"
16410
+ });
16411
+ }
15594
16412
  if (typeof this.targetDatabase.connect === "function") {
15595
16413
  await this.targetDatabase.connect();
15596
16414
  }
@@ -15977,7 +16795,12 @@ const REPLICATOR_DRIVERS = {
15977
16795
  function createReplicator(driver, config = {}, resources = [], client = null) {
15978
16796
  const ReplicatorClass = REPLICATOR_DRIVERS[driver];
15979
16797
  if (!ReplicatorClass) {
15980
- throw new Error(`Unknown replicator driver: ${driver}. Available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`);
16798
+ throw new ReplicationError(`Unknown replicator driver: ${driver}`, {
16799
+ operation: "createReplicator",
16800
+ driver,
16801
+ availableDrivers: Object.keys(REPLICATOR_DRIVERS),
16802
+ suggestion: `Use one of the available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`
16803
+ });
15981
16804
  }
15982
16805
  return new ReplicatorClass(config, resources, client);
15983
16806
  }
@@ -15989,12 +16812,40 @@ class ReplicatorPlugin extends Plugin {
15989
16812
  constructor(options = {}) {
15990
16813
  super();
15991
16814
  if (!options.replicators || !Array.isArray(options.replicators)) {
15992
- throw new Error("ReplicatorPlugin: replicators array is required");
16815
+ throw new ReplicationError("ReplicatorPlugin requires replicators array", {
16816
+ operation: "constructor",
16817
+ pluginName: "ReplicatorPlugin",
16818
+ providedOptions: Object.keys(options),
16819
+ suggestion: 'Provide replicators array: new ReplicatorPlugin({ replicators: [{ driver: "s3db", resources: [...] }] })'
16820
+ });
15993
16821
  }
15994
16822
  for (const rep of options.replicators) {
15995
- if (!rep.driver) throw new Error("ReplicatorPlugin: each replicator must have a driver");
15996
- if (!rep.resources || typeof rep.resources !== "object") throw new Error("ReplicatorPlugin: each replicator must have resources config");
15997
- if (Object.keys(rep.resources).length === 0) throw new Error("ReplicatorPlugin: each replicator must have at least one resource configured");
16823
+ if (!rep.driver) {
16824
+ throw new ReplicationError("Each replicator must have a driver", {
16825
+ operation: "constructor",
16826
+ pluginName: "ReplicatorPlugin",
16827
+ replicatorConfig: rep,
16828
+ suggestion: 'Each replicator entry must specify a driver: { driver: "s3db", resources: {...} }'
16829
+ });
16830
+ }
16831
+ if (!rep.resources || typeof rep.resources !== "object") {
16832
+ throw new ReplicationError("Each replicator must have resources config", {
16833
+ operation: "constructor",
16834
+ pluginName: "ReplicatorPlugin",
16835
+ driver: rep.driver,
16836
+ replicatorConfig: rep,
16837
+ suggestion: 'Provide resources as object or array: { driver: "s3db", resources: ["users"] } or { resources: { users: "people" } }'
16838
+ });
16839
+ }
16840
+ if (Object.keys(rep.resources).length === 0) {
16841
+ throw new ReplicationError("Each replicator must have at least one resource configured", {
16842
+ operation: "constructor",
16843
+ pluginName: "ReplicatorPlugin",
16844
+ driver: rep.driver,
16845
+ replicatorConfig: rep,
16846
+ suggestion: 'Add at least one resource to replicate: { driver: "s3db", resources: ["users"] }'
16847
+ });
16848
+ }
15998
16849
  }
15999
16850
  this.config = {
16000
16851
  replicators: options.replicators || [],
@@ -16420,7 +17271,13 @@ class ReplicatorPlugin extends Plugin {
16420
17271
  async syncAllData(replicatorId) {
16421
17272
  const replicator = this.replicators.find((r) => r.id === replicatorId);
16422
17273
  if (!replicator) {
16423
- throw new Error(`Replicator not found: ${replicatorId}`);
17274
+ throw new ReplicationError("Replicator not found", {
17275
+ operation: "syncAllData",
17276
+ pluginName: "ReplicatorPlugin",
17277
+ replicatorId,
17278
+ availableReplicators: this.replicators.map((r) => r.id),
17279
+ suggestion: "Check replicator ID or use getReplicatorStats() to list available replicators"
17280
+ });
16424
17281
  }
16425
17282
  this.stats.lastSync = (/* @__PURE__ */ new Date()).toISOString();
16426
17283
  for (const resourceName in this.database.resources) {
@@ -16950,6 +17807,35 @@ class S3QueuePlugin extends Plugin {
16950
17807
  }
16951
17808
  }
16952
17809
 
17810
+ class SchedulerError extends S3dbError {
17811
+ constructor(message, details = {}) {
17812
+ const { taskId, operation = "unknown", cronExpression, ...rest } = details;
17813
+ let description = details.description;
17814
+ if (!description) {
17815
+ description = `
17816
+ Scheduler Operation Error
17817
+
17818
+ Operation: ${operation}
17819
+ ${taskId ? `Task ID: ${taskId}` : ""}
17820
+ ${cronExpression ? `Cron: ${cronExpression}` : ""}
17821
+
17822
+ Common causes:
17823
+ 1. Invalid cron expression format
17824
+ 2. Task not found or already exists
17825
+ 3. Scheduler not properly initialized
17826
+ 4. Job execution failure
17827
+ 5. Resource conflicts
17828
+
17829
+ Solution:
17830
+ Check task configuration and ensure scheduler is properly initialized.
17831
+
17832
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/scheduler.md
17833
+ `.trim();
17834
+ }
17835
+ super(message, { ...rest, taskId, operation, cronExpression, description });
17836
+ }
17837
+ }
17838
+
16953
17839
  class SchedulerPlugin extends Plugin {
16954
17840
  constructor(options = {}) {
16955
17841
  super();
@@ -16983,17 +17869,36 @@ class SchedulerPlugin extends Plugin {
16983
17869
  }
16984
17870
  _validateConfiguration() {
16985
17871
  if (Object.keys(this.config.jobs).length === 0) {
16986
- throw new Error("SchedulerPlugin: At least one job must be defined");
17872
+ throw new SchedulerError("At least one job must be defined", {
17873
+ operation: "validateConfiguration",
17874
+ jobCount: 0,
17875
+ suggestion: 'Provide at least one job in the jobs configuration: { jobs: { myJob: { schedule: "* * * * *", action: async () => {...} } } }'
17876
+ });
16987
17877
  }
16988
17878
  for (const [jobName, job] of Object.entries(this.config.jobs)) {
16989
17879
  if (!job.schedule) {
16990
- throw new Error(`SchedulerPlugin: Job '${jobName}' must have a schedule`);
17880
+ throw new SchedulerError(`Job '${jobName}' must have a schedule`, {
17881
+ operation: "validateConfiguration",
17882
+ taskId: jobName,
17883
+ providedConfig: Object.keys(job),
17884
+ suggestion: 'Add a schedule property with a valid cron expression: { schedule: "0 * * * *", action: async () => {...} }'
17885
+ });
16991
17886
  }
16992
17887
  if (!job.action || typeof job.action !== "function") {
16993
- throw new Error(`SchedulerPlugin: Job '${jobName}' must have an action function`);
17888
+ throw new SchedulerError(`Job '${jobName}' must have an action function`, {
17889
+ operation: "validateConfiguration",
17890
+ taskId: jobName,
17891
+ actionType: typeof job.action,
17892
+ suggestion: 'Provide an action function: { schedule: "...", action: async (db, ctx) => {...} }'
17893
+ });
16994
17894
  }
16995
17895
  if (!this._isValidCronExpression(job.schedule)) {
16996
- throw new Error(`SchedulerPlugin: Job '${jobName}' has invalid cron expression: ${job.schedule}`);
17896
+ throw new SchedulerError(`Job '${jobName}' has invalid cron expression`, {
17897
+ operation: "validateConfiguration",
17898
+ taskId: jobName,
17899
+ cronExpression: job.schedule,
17900
+ suggestion: "Use valid cron format (5 fields: minute hour day month weekday) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
17901
+ });
16997
17902
  }
16998
17903
  }
16999
17904
  }
@@ -17291,10 +18196,20 @@ class SchedulerPlugin extends Plugin {
17291
18196
  async runJob(jobName, context = {}) {
17292
18197
  const job = this.jobs.get(jobName);
17293
18198
  if (!job) {
17294
- throw new Error(`Job '${jobName}' not found`);
18199
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18200
+ operation: "runJob",
18201
+ taskId: jobName,
18202
+ availableJobs: Array.from(this.jobs.keys()),
18203
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18204
+ });
17295
18205
  }
17296
18206
  if (this.activeJobs.has(jobName)) {
17297
- throw new Error(`Job '${jobName}' is already running`);
18207
+ throw new SchedulerError(`Job '${jobName}' is already running`, {
18208
+ operation: "runJob",
18209
+ taskId: jobName,
18210
+ executionId: this.activeJobs.get(jobName),
18211
+ suggestion: "Wait for current execution to complete or check job status with getJobStatus()"
18212
+ });
17298
18213
  }
17299
18214
  await this._executeJob(jobName);
17300
18215
  }
@@ -17304,7 +18219,12 @@ class SchedulerPlugin extends Plugin {
17304
18219
  enableJob(jobName) {
17305
18220
  const job = this.jobs.get(jobName);
17306
18221
  if (!job) {
17307
- throw new Error(`Job '${jobName}' not found`);
18222
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18223
+ operation: "enableJob",
18224
+ taskId: jobName,
18225
+ availableJobs: Array.from(this.jobs.keys()),
18226
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18227
+ });
17308
18228
  }
17309
18229
  job.enabled = true;
17310
18230
  this._scheduleNextExecution(jobName);
@@ -17316,7 +18236,12 @@ class SchedulerPlugin extends Plugin {
17316
18236
  disableJob(jobName) {
17317
18237
  const job = this.jobs.get(jobName);
17318
18238
  if (!job) {
17319
- throw new Error(`Job '${jobName}' not found`);
18239
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18240
+ operation: "disableJob",
18241
+ taskId: jobName,
18242
+ availableJobs: Array.from(this.jobs.keys()),
18243
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18244
+ });
17320
18245
  }
17321
18246
  job.enabled = false;
17322
18247
  const timer = this.timers.get(jobName);
@@ -17415,13 +18340,28 @@ class SchedulerPlugin extends Plugin {
17415
18340
  */
17416
18341
  addJob(jobName, jobConfig) {
17417
18342
  if (this.jobs.has(jobName)) {
17418
- throw new Error(`Job '${jobName}' already exists`);
18343
+ throw new SchedulerError(`Job '${jobName}' already exists`, {
18344
+ operation: "addJob",
18345
+ taskId: jobName,
18346
+ existingJobs: Array.from(this.jobs.keys()),
18347
+ suggestion: "Use a different job name or remove the existing job first with removeJob()"
18348
+ });
17419
18349
  }
17420
18350
  if (!jobConfig.schedule || !jobConfig.action) {
17421
- throw new Error("Job must have schedule and action");
18351
+ throw new SchedulerError("Job must have schedule and action", {
18352
+ operation: "addJob",
18353
+ taskId: jobName,
18354
+ providedConfig: Object.keys(jobConfig),
18355
+ suggestion: 'Provide both schedule and action: { schedule: "0 * * * *", action: async (db, ctx) => {...} }'
18356
+ });
17422
18357
  }
17423
18358
  if (!this._isValidCronExpression(jobConfig.schedule)) {
17424
- throw new Error(`Invalid cron expression: ${jobConfig.schedule}`);
18359
+ throw new SchedulerError("Invalid cron expression", {
18360
+ operation: "addJob",
18361
+ taskId: jobName,
18362
+ cronExpression: jobConfig.schedule,
18363
+ suggestion: "Use valid cron format (5 fields) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
18364
+ });
17425
18365
  }
17426
18366
  const job = {
17427
18367
  ...jobConfig,
@@ -17455,7 +18395,12 @@ class SchedulerPlugin extends Plugin {
17455
18395
  removeJob(jobName) {
17456
18396
  const job = this.jobs.get(jobName);
17457
18397
  if (!job) {
17458
- throw new Error(`Job '${jobName}' not found`);
18398
+ throw new SchedulerError(`Job '${jobName}' not found`, {
18399
+ operation: "removeJob",
18400
+ taskId: jobName,
18401
+ availableJobs: Array.from(this.jobs.keys()),
18402
+ suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
18403
+ });
17459
18404
  }
17460
18405
  const timer = this.timers.get(jobName);
17461
18406
  if (timer) {
@@ -17509,6 +18454,36 @@ class SchedulerPlugin extends Plugin {
17509
18454
  }
17510
18455
  }
17511
18456
 
18457
+ class StateMachineError extends S3dbError {
18458
+ constructor(message, details = {}) {
18459
+ const { currentState, targetState, resourceName, operation = "unknown", ...rest } = details;
18460
+ let description = details.description;
18461
+ if (!description) {
18462
+ description = `
18463
+ State Machine Operation Error
18464
+
18465
+ Operation: ${operation}
18466
+ ${currentState ? `Current State: ${currentState}` : ""}
18467
+ ${targetState ? `Target State: ${targetState}` : ""}
18468
+ ${resourceName ? `Resource: ${resourceName}` : ""}
18469
+
18470
+ Common causes:
18471
+ 1. Invalid state transition
18472
+ 2. State machine not configured
18473
+ 3. Transition conditions not met
18474
+ 4. State not defined in configuration
18475
+ 5. Missing transition handler
18476
+
18477
+ Solution:
18478
+ Check state machine configuration and valid transitions.
18479
+
18480
+ Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-machine.md
18481
+ `.trim();
18482
+ }
18483
+ super(message, { ...rest, currentState, targetState, resourceName, operation, description });
18484
+ }
18485
+ }
18486
+
17512
18487
  class StateMachinePlugin extends Plugin {
17513
18488
  constructor(options = {}) {
17514
18489
  super();
@@ -17529,17 +18504,36 @@ class StateMachinePlugin extends Plugin {
17529
18504
  }
17530
18505
  _validateConfiguration() {
17531
18506
  if (!this.config.stateMachines || Object.keys(this.config.stateMachines).length === 0) {
17532
- throw new Error("StateMachinePlugin: At least one state machine must be defined");
18507
+ throw new StateMachineError("At least one state machine must be defined", {
18508
+ operation: "validateConfiguration",
18509
+ machineCount: 0,
18510
+ suggestion: "Provide at least one state machine in the stateMachines configuration"
18511
+ });
17533
18512
  }
17534
18513
  for (const [machineName, machine] of Object.entries(this.config.stateMachines)) {
17535
18514
  if (!machine.states || Object.keys(machine.states).length === 0) {
17536
- throw new Error(`StateMachinePlugin: Machine '${machineName}' must have states defined`);
18515
+ throw new StateMachineError(`Machine '${machineName}' must have states defined`, {
18516
+ operation: "validateConfiguration",
18517
+ machineId: machineName,
18518
+ suggestion: "Define at least one state in the states configuration"
18519
+ });
17537
18520
  }
17538
18521
  if (!machine.initialState) {
17539
- throw new Error(`StateMachinePlugin: Machine '${machineName}' must have an initialState`);
18522
+ throw new StateMachineError(`Machine '${machineName}' must have an initialState`, {
18523
+ operation: "validateConfiguration",
18524
+ machineId: machineName,
18525
+ availableStates: Object.keys(machine.states),
18526
+ suggestion: "Specify an initialState property matching one of the defined states"
18527
+ });
17540
18528
  }
17541
18529
  if (!machine.states[machine.initialState]) {
17542
- throw new Error(`StateMachinePlugin: Initial state '${machine.initialState}' not found in machine '${machineName}'`);
18530
+ throw new StateMachineError(`Initial state '${machine.initialState}' not found in machine '${machineName}'`, {
18531
+ operation: "validateConfiguration",
18532
+ machineId: machineName,
18533
+ initialState: machine.initialState,
18534
+ availableStates: Object.keys(machine.states),
18535
+ suggestion: "Set initialState to one of the defined states"
18536
+ });
17543
18537
  }
17544
18538
  }
17545
18539
  }
@@ -17596,12 +18590,25 @@ class StateMachinePlugin extends Plugin {
17596
18590
  async send(machineId, entityId, event, context = {}) {
17597
18591
  const machine = this.machines.get(machineId);
17598
18592
  if (!machine) {
17599
- throw new Error(`State machine '${machineId}' not found`);
18593
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18594
+ operation: "send",
18595
+ machineId,
18596
+ availableMachines: Array.from(this.machines.keys()),
18597
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18598
+ });
17600
18599
  }
17601
18600
  const currentState = await this.getState(machineId, entityId);
17602
18601
  const stateConfig = machine.config.states[currentState];
17603
18602
  if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
17604
- throw new Error(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`);
18603
+ throw new StateMachineError(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`, {
18604
+ operation: "send",
18605
+ machineId,
18606
+ entityId,
18607
+ event,
18608
+ currentState,
18609
+ validEvents: stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [],
18610
+ suggestion: "Use getValidEvents() to check which events are valid for the current state"
18611
+ });
17605
18612
  }
17606
18613
  const targetState = stateConfig.on[event];
17607
18614
  if (stateConfig.guards && stateConfig.guards[event]) {
@@ -17612,7 +18619,16 @@ class StateMachinePlugin extends Plugin {
17612
18619
  () => guard(context, event, { database: this.database, machineId, entityId })
17613
18620
  );
17614
18621
  if (!guardOk || !guardResult) {
17615
- throw new Error(`Transition blocked by guard '${guardName}': ${guardErr?.message || "Guard returned false"}`);
18622
+ throw new StateMachineError(`Transition blocked by guard '${guardName}'`, {
18623
+ operation: "send",
18624
+ machineId,
18625
+ entityId,
18626
+ event,
18627
+ currentState,
18628
+ guardName,
18629
+ guardError: guardErr?.message || "Guard returned false",
18630
+ suggestion: "Check guard conditions or modify the context to satisfy guard requirements"
18631
+ });
17616
18632
  }
17617
18633
  }
17618
18634
  }
@@ -17722,7 +18738,12 @@ class StateMachinePlugin extends Plugin {
17722
18738
  async getState(machineId, entityId) {
17723
18739
  const machine = this.machines.get(machineId);
17724
18740
  if (!machine) {
17725
- throw new Error(`State machine '${machineId}' not found`);
18741
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18742
+ operation: "getState",
18743
+ machineId,
18744
+ availableMachines: Array.from(this.machines.keys()),
18745
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18746
+ });
17726
18747
  }
17727
18748
  if (machine.currentStates.has(entityId)) {
17728
18749
  return machine.currentStates.get(entityId);
@@ -17748,7 +18769,12 @@ class StateMachinePlugin extends Plugin {
17748
18769
  async getValidEvents(machineId, stateOrEntityId) {
17749
18770
  const machine = this.machines.get(machineId);
17750
18771
  if (!machine) {
17751
- throw new Error(`State machine '${machineId}' not found`);
18772
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18773
+ operation: "getValidEvents",
18774
+ machineId,
18775
+ availableMachines: Array.from(this.machines.keys()),
18776
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18777
+ });
17752
18778
  }
17753
18779
  let state;
17754
18780
  if (machine.config.states[stateOrEntityId]) {
@@ -17797,7 +18823,12 @@ class StateMachinePlugin extends Plugin {
17797
18823
  async initializeEntity(machineId, entityId, context = {}) {
17798
18824
  const machine = this.machines.get(machineId);
17799
18825
  if (!machine) {
17800
- throw new Error(`State machine '${machineId}' not found`);
18826
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18827
+ operation: "initializeEntity",
18828
+ machineId,
18829
+ availableMachines: Array.from(this.machines.keys()),
18830
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18831
+ });
17801
18832
  }
17802
18833
  const initialState = machine.config.initialState;
17803
18834
  machine.currentStates.set(entityId, initialState);
@@ -17816,7 +18847,14 @@ class StateMachinePlugin extends Plugin {
17816
18847
  })
17817
18848
  );
17818
18849
  if (!ok && err && !err.message?.includes("already exists")) {
17819
- throw new Error(`Failed to initialize entity state: ${err.message}`);
18850
+ throw new StateMachineError("Failed to initialize entity state", {
18851
+ operation: "initializeEntity",
18852
+ machineId,
18853
+ entityId,
18854
+ initialState,
18855
+ original: err,
18856
+ suggestion: "Check state resource configuration and database permissions"
18857
+ });
17820
18858
  }
17821
18859
  }
17822
18860
  const initialStateConfig = machine.config.states[initialState];
@@ -17845,7 +18883,12 @@ class StateMachinePlugin extends Plugin {
17845
18883
  visualize(machineId) {
17846
18884
  const machine = this.machines.get(machineId);
17847
18885
  if (!machine) {
17848
- throw new Error(`State machine '${machineId}' not found`);
18886
+ throw new StateMachineError(`State machine '${machineId}' not found`, {
18887
+ operation: "visualize",
18888
+ machineId,
18889
+ availableMachines: Array.from(this.machines.keys()),
18890
+ suggestion: "Check machine ID or use getMachines() to list available machines"
18891
+ });
17849
18892
  }
17850
18893
  let dot = `digraph ${machineId} {
17851
18894
  `;
@@ -17889,5 +18932,5 @@ class StateMachinePlugin extends Plugin {
17889
18932
  }
17890
18933
  }
17891
18934
 
17892
- export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, AuditPlugin, AuthenticationError, BackupPlugin, BaseError, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, EncryptionError, ErrorMap, EventualConsistencyPlugin, FullTextPlugin, InvalidResourceItem, MetricsPlugin, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, PartitionError, PermissionError, Plugin, PluginObject, QueueConsumerPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3QueuePlugin, Database as S3db, S3dbError, SchedulerPlugin, Schema, SchemaError, StateMachinePlugin, UnknownError, ValidationError, Validator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes, clearUTF8Cache, clearUTF8Memo, clearUTF8Memory, decode, decodeDecimal, decrypt, S3db as default, encode, encodeDecimal, encrypt, getBehavior, getSizeBreakdown, idGenerator, mapAwsError, md5, passwordGenerator, sha256, streamToString, transformValue, tryFn, tryFnSync };
18935
+ export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, AuditPlugin, AuthenticationError, BackupPlugin, BaseError, BehaviorError, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, EncryptionError, ErrorMap, EventualConsistencyPlugin, FullTextPlugin, InvalidResourceItem, MetadataLimitError, MetricsPlugin, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, PartitionDriverError, PartitionError, PermissionError, Plugin, PluginError, PluginObject, PluginStorageError, QueueConsumerPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3QueuePlugin, Database as S3db, S3dbError, SchedulerPlugin, Schema, SchemaError, StateMachinePlugin, StreamError, UnknownError, ValidationError, Validator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes, clearUTF8Cache, clearUTF8Memo, clearUTF8Memory, decode, decodeDecimal, decrypt, S3db as default, encode, encodeDecimal, encrypt, getBehavior, getSizeBreakdown, idGenerator, mapAwsError, md5, passwordGenerator, sha256, streamToString, transformValue, tryFn, tryFnSync };
17893
18936
  //# sourceMappingURL=s3db.es.js.map