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.
- package/dist/s3db.cjs.js +1177 -128
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1172 -129
- package/dist/s3db.es.js.map +1 -1
- package/package.json +1 -1
- package/src/behaviors/enforce-limits.js +28 -4
- package/src/behaviors/index.js +6 -1
- package/src/client.class.js +11 -1
- package/src/concerns/partition-queue.js +7 -1
- package/src/concerns/plugin-storage.js +75 -13
- package/src/database.class.js +19 -4
- package/src/errors.js +306 -27
- package/src/partition-drivers/base-partition-driver.js +12 -2
- package/src/partition-drivers/index.js +7 -1
- package/src/partition-drivers/memory-partition-driver.js +20 -5
- package/src/partition-drivers/sqs-partition-driver.js +6 -1
- package/src/plugins/audit.errors.js +46 -0
- package/src/plugins/backup/base-backup-driver.class.js +36 -6
- package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
- package/src/plugins/backup/index.js +40 -9
- package/src/plugins/backup/multi-backup-driver.class.js +69 -9
- package/src/plugins/backup/s3-backup-driver.class.js +48 -6
- package/src/plugins/backup.errors.js +45 -0
- package/src/plugins/cache/cache.class.js +8 -1
- package/src/plugins/cache.errors.js +47 -0
- package/src/plugins/cache.plugin.js +8 -1
- package/src/plugins/fulltext.errors.js +46 -0
- package/src/plugins/fulltext.plugin.js +15 -3
- package/src/plugins/metrics.errors.js +46 -0
- package/src/plugins/queue-consumer.plugin.js +31 -4
- package/src/plugins/queue.errors.js +46 -0
- package/src/plugins/replicator.errors.js +46 -0
- package/src/plugins/replicator.plugin.js +40 -5
- package/src/plugins/replicators/base-replicator.class.js +19 -3
- package/src/plugins/replicators/index.js +9 -3
- package/src/plugins/replicators/s3db-replicator.class.js +38 -8
- package/src/plugins/scheduler.errors.js +46 -0
- package/src/plugins/scheduler.plugin.js +79 -19
- package/src/plugins/state-machine.errors.js +47 -0
- package/src/plugins/state-machine.plugin.js +86 -17
- package/src/stream/index.js +6 -1
- 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,
|
|
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
|
|
403
|
+
let description;
|
|
406
404
|
if (code === "NoSuchKey" || code === "NotFound") {
|
|
407
|
-
|
|
408
|
-
return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput,
|
|
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
|
-
|
|
412
|
-
return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput,
|
|
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
|
-
|
|
416
|
-
return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput,
|
|
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
|
-
|
|
420
|
-
return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput,
|
|
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
|
-
|
|
424
|
-
return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput,
|
|
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
|
-
|
|
433
|
-
return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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://
|
|
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://
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1401
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
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)
|
|
15996
|
-
|
|
15997
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|