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.cjs.js
CHANGED
|
@@ -222,7 +222,7 @@ function calculateEffectiveLimit(config = {}) {
|
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
class BaseError extends Error {
|
|
225
|
-
constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata,
|
|
225
|
+
constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, description, ...rest }) {
|
|
226
226
|
if (verbose) message = message + `
|
|
227
227
|
|
|
228
228
|
Verbose:
|
|
@@ -247,7 +247,6 @@ ${JSON.stringify(rest, null, 2)}`;
|
|
|
247
247
|
this.commandName = commandName;
|
|
248
248
|
this.commandInput = commandInput;
|
|
249
249
|
this.metadata = metadata;
|
|
250
|
-
this.suggestion = suggestion;
|
|
251
250
|
this.description = description;
|
|
252
251
|
this.data = { bucket, key, ...rest, verbose, message };
|
|
253
252
|
}
|
|
@@ -265,7 +264,6 @@ ${JSON.stringify(rest, null, 2)}`;
|
|
|
265
264
|
commandName: this.commandName,
|
|
266
265
|
commandInput: this.commandInput,
|
|
267
266
|
metadata: this.metadata,
|
|
268
|
-
suggestion: this.suggestion,
|
|
269
267
|
description: this.description,
|
|
270
268
|
data: this.data,
|
|
271
269
|
original: this.original,
|
|
@@ -406,26 +404,26 @@ function mapAwsError(err, context = {}) {
|
|
|
406
404
|
const metadata = err.$metadata ? { ...err.$metadata } : void 0;
|
|
407
405
|
const commandName = context.commandName;
|
|
408
406
|
const commandInput = context.commandInput;
|
|
409
|
-
let
|
|
407
|
+
let description;
|
|
410
408
|
if (code === "NoSuchKey" || code === "NotFound") {
|
|
411
|
-
|
|
412
|
-
return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput,
|
|
409
|
+
description = "The specified key does not exist in the bucket. Check if the key exists and if your credentials have permission to access it.";
|
|
410
|
+
return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, description });
|
|
413
411
|
}
|
|
414
412
|
if (code === "NoSuchBucket") {
|
|
415
|
-
|
|
416
|
-
return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput,
|
|
413
|
+
description = "The specified bucket does not exist. Check if the bucket name is correct and if your credentials have permission to access it.";
|
|
414
|
+
return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, description });
|
|
417
415
|
}
|
|
418
416
|
if (code === "AccessDenied" || err.statusCode === 403 || code === "Forbidden") {
|
|
419
|
-
|
|
420
|
-
return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput,
|
|
417
|
+
description = "Access denied. Check your AWS credentials, IAM permissions, and bucket policy.";
|
|
418
|
+
return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput, description });
|
|
421
419
|
}
|
|
422
420
|
if (code === "ValidationError" || err.statusCode === 400) {
|
|
423
|
-
|
|
424
|
-
return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput,
|
|
421
|
+
description = "Validation error. Check the request parameters and payload format.";
|
|
422
|
+
return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput, description });
|
|
425
423
|
}
|
|
426
424
|
if (code === "MissingMetadata") {
|
|
427
|
-
|
|
428
|
-
return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput,
|
|
425
|
+
description = "Object metadata is missing or invalid. Check if the object was uploaded correctly.";
|
|
426
|
+
return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, description });
|
|
429
427
|
}
|
|
430
428
|
const errorDetails = [
|
|
431
429
|
`Unknown error: ${err.message || err.toString()}`,
|
|
@@ -433,27 +431,31 @@ function mapAwsError(err, context = {}) {
|
|
|
433
431
|
err.statusCode && `Status: ${err.statusCode}`,
|
|
434
432
|
err.stack && `Stack: ${err.stack.split("\n")[0]}`
|
|
435
433
|
].filter(Boolean).join(" | ");
|
|
436
|
-
|
|
437
|
-
return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput,
|
|
434
|
+
description = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
|
|
435
|
+
return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, description });
|
|
438
436
|
}
|
|
439
437
|
class ConnectionStringError extends S3dbError {
|
|
440
438
|
constructor(message, details = {}) {
|
|
441
|
-
|
|
439
|
+
const description = details.description || "Invalid connection string format. Check the connection string syntax and credentials.";
|
|
440
|
+
super(message, { ...details, description });
|
|
442
441
|
}
|
|
443
442
|
}
|
|
444
443
|
class CryptoError extends S3dbError {
|
|
445
444
|
constructor(message, details = {}) {
|
|
446
|
-
|
|
445
|
+
const description = details.description || "Cryptography operation failed. Check if the crypto library is available and input is valid.";
|
|
446
|
+
super(message, { ...details, description });
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
class SchemaError extends S3dbError {
|
|
450
450
|
constructor(message, details = {}) {
|
|
451
|
-
|
|
451
|
+
const description = details.description || "Schema validation failed. Check schema definition and input data format.";
|
|
452
|
+
super(message, { ...details, description });
|
|
452
453
|
}
|
|
453
454
|
}
|
|
454
455
|
class ResourceError extends S3dbError {
|
|
455
456
|
constructor(message, details = {}) {
|
|
456
|
-
|
|
457
|
+
const description = details.description || "Resource operation failed. Check resource configuration, attributes, and operation context.";
|
|
458
|
+
super(message, { ...details, description });
|
|
457
459
|
Object.assign(this, details);
|
|
458
460
|
}
|
|
459
461
|
}
|
|
@@ -482,13 +484,12 @@ ${details.strictValidation === false ? " \u2022 Update partition definition to
|
|
|
482
484
|
\u2022 Update partition definition to use existing fields, OR
|
|
483
485
|
\u2022 Use strictValidation: false to skip this check during testing`}
|
|
484
486
|
|
|
485
|
-
Docs: https://
|
|
487
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partitions
|
|
486
488
|
`.trim();
|
|
487
489
|
}
|
|
488
490
|
super(message, {
|
|
489
491
|
...details,
|
|
490
|
-
description
|
|
491
|
-
suggestion: details.suggestion || "Check partition definition, fields, and input values."
|
|
492
|
+
description
|
|
492
493
|
});
|
|
493
494
|
}
|
|
494
495
|
}
|
|
@@ -547,7 +548,7 @@ Example fix:
|
|
|
547
548
|
await db.connect(); // Plugin initialized here
|
|
548
549
|
await db.createResource({ name: '${resourceName}', ... }); // Analytics resource created here
|
|
549
550
|
|
|
550
|
-
Docs: https://
|
|
551
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/eventual-consistency.md
|
|
551
552
|
`.trim();
|
|
552
553
|
super(message, {
|
|
553
554
|
...rest,
|
|
@@ -557,8 +558,260 @@ Docs: https://docs.s3db.js.org/plugins/eventual-consistency#troubleshooting
|
|
|
557
558
|
configuredResources,
|
|
558
559
|
registeredResources,
|
|
559
560
|
pluginInitialized,
|
|
560
|
-
description
|
|
561
|
-
|
|
561
|
+
description
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
class PluginError extends S3dbError {
|
|
566
|
+
constructor(message, details = {}) {
|
|
567
|
+
const {
|
|
568
|
+
pluginName = "Unknown",
|
|
569
|
+
operation = "unknown",
|
|
570
|
+
...rest
|
|
571
|
+
} = details;
|
|
572
|
+
let description = details.description;
|
|
573
|
+
if (!description) {
|
|
574
|
+
description = `
|
|
575
|
+
Plugin Error
|
|
576
|
+
|
|
577
|
+
Plugin: ${pluginName}
|
|
578
|
+
Operation: ${operation}
|
|
579
|
+
|
|
580
|
+
Possible causes:
|
|
581
|
+
1. Plugin not properly initialized
|
|
582
|
+
2. Plugin configuration is invalid
|
|
583
|
+
3. Plugin dependencies not met
|
|
584
|
+
4. Plugin method called before installation
|
|
585
|
+
|
|
586
|
+
Solution:
|
|
587
|
+
Ensure plugin is added to database and connect() is called before usage.
|
|
588
|
+
|
|
589
|
+
Example:
|
|
590
|
+
const db = new Database({
|
|
591
|
+
bucket: 'my-bucket',
|
|
592
|
+
plugins: [new ${pluginName}({ /* config */ })]
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
await db.connect(); // Plugin installed here
|
|
596
|
+
// Now plugin methods are available
|
|
597
|
+
|
|
598
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md
|
|
599
|
+
`.trim();
|
|
600
|
+
}
|
|
601
|
+
super(message, {
|
|
602
|
+
...rest,
|
|
603
|
+
pluginName,
|
|
604
|
+
operation,
|
|
605
|
+
description
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
class PluginStorageError extends S3dbError {
|
|
610
|
+
constructor(message, details = {}) {
|
|
611
|
+
const {
|
|
612
|
+
pluginSlug = "unknown",
|
|
613
|
+
key = "",
|
|
614
|
+
operation = "unknown",
|
|
615
|
+
...rest
|
|
616
|
+
} = details;
|
|
617
|
+
let description = details.description;
|
|
618
|
+
if (!description) {
|
|
619
|
+
description = `
|
|
620
|
+
Plugin Storage Error
|
|
621
|
+
|
|
622
|
+
Plugin: ${pluginSlug}
|
|
623
|
+
Key: ${key}
|
|
624
|
+
Operation: ${operation}
|
|
625
|
+
|
|
626
|
+
Possible causes:
|
|
627
|
+
1. Storage not initialized (plugin not installed)
|
|
628
|
+
2. Invalid key format
|
|
629
|
+
3. S3 operation failed
|
|
630
|
+
4. Permissions issue
|
|
631
|
+
|
|
632
|
+
Solution:
|
|
633
|
+
Ensure plugin has access to storage and key is valid.
|
|
634
|
+
|
|
635
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md#plugin-storage
|
|
636
|
+
`.trim();
|
|
637
|
+
}
|
|
638
|
+
super(message, {
|
|
639
|
+
...rest,
|
|
640
|
+
pluginSlug,
|
|
641
|
+
key,
|
|
642
|
+
operation,
|
|
643
|
+
description
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
class PartitionDriverError extends S3dbError {
|
|
648
|
+
constructor(message, details = {}) {
|
|
649
|
+
const {
|
|
650
|
+
driver = "unknown",
|
|
651
|
+
operation = "unknown",
|
|
652
|
+
queueSize,
|
|
653
|
+
maxQueueSize,
|
|
654
|
+
...rest
|
|
655
|
+
} = details;
|
|
656
|
+
let description = details.description;
|
|
657
|
+
if (!description && queueSize !== void 0 && maxQueueSize !== void 0) {
|
|
658
|
+
description = `
|
|
659
|
+
Partition Driver Error
|
|
660
|
+
|
|
661
|
+
Driver: ${driver}
|
|
662
|
+
Operation: ${operation}
|
|
663
|
+
Queue Status: ${queueSize}/${maxQueueSize}
|
|
664
|
+
|
|
665
|
+
Possible causes:
|
|
666
|
+
1. Queue is full (backpressure)
|
|
667
|
+
2. Driver not properly configured
|
|
668
|
+
3. SQS permissions issue (if using SQS driver)
|
|
669
|
+
|
|
670
|
+
Solution:
|
|
671
|
+
${queueSize >= maxQueueSize ? "Wait for queue to drain or increase maxQueueSize" : "Check driver configuration and permissions"}
|
|
672
|
+
|
|
673
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
|
|
674
|
+
`.trim();
|
|
675
|
+
} else if (!description) {
|
|
676
|
+
description = `
|
|
677
|
+
Partition Driver Error
|
|
678
|
+
|
|
679
|
+
Driver: ${driver}
|
|
680
|
+
Operation: ${operation}
|
|
681
|
+
|
|
682
|
+
Check driver configuration and permissions.
|
|
683
|
+
|
|
684
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
|
|
685
|
+
`.trim();
|
|
686
|
+
}
|
|
687
|
+
super(message, {
|
|
688
|
+
...rest,
|
|
689
|
+
driver,
|
|
690
|
+
operation,
|
|
691
|
+
queueSize,
|
|
692
|
+
maxQueueSize,
|
|
693
|
+
description
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
class BehaviorError extends S3dbError {
|
|
698
|
+
constructor(message, details = {}) {
|
|
699
|
+
const {
|
|
700
|
+
behavior = "unknown",
|
|
701
|
+
availableBehaviors = [],
|
|
702
|
+
...rest
|
|
703
|
+
} = details;
|
|
704
|
+
let description = details.description;
|
|
705
|
+
if (!description) {
|
|
706
|
+
description = `
|
|
707
|
+
Behavior Error
|
|
708
|
+
|
|
709
|
+
Requested: ${behavior}
|
|
710
|
+
Available: ${availableBehaviors.join(", ") || "body-overflow, body-only, truncate-data, enforce-limits, user-managed"}
|
|
711
|
+
|
|
712
|
+
Possible causes:
|
|
713
|
+
1. Behavior name misspelled
|
|
714
|
+
2. Custom behavior not registered
|
|
715
|
+
|
|
716
|
+
Solution:
|
|
717
|
+
Use one of the available behaviors or register custom behavior.
|
|
718
|
+
|
|
719
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#behaviors
|
|
720
|
+
`.trim();
|
|
721
|
+
}
|
|
722
|
+
super(message, {
|
|
723
|
+
...rest,
|
|
724
|
+
behavior,
|
|
725
|
+
availableBehaviors,
|
|
726
|
+
description
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
class StreamError extends S3dbError {
|
|
731
|
+
constructor(message, details = {}) {
|
|
732
|
+
const {
|
|
733
|
+
operation = "unknown",
|
|
734
|
+
resource,
|
|
735
|
+
...rest
|
|
736
|
+
} = details;
|
|
737
|
+
let description = details.description;
|
|
738
|
+
if (!description) {
|
|
739
|
+
description = `
|
|
740
|
+
Stream Error
|
|
741
|
+
|
|
742
|
+
Operation: ${operation}
|
|
743
|
+
${resource ? `Resource: ${resource}` : ""}
|
|
744
|
+
|
|
745
|
+
Possible causes:
|
|
746
|
+
1. Stream not properly initialized
|
|
747
|
+
2. Resource not available
|
|
748
|
+
3. Network error during streaming
|
|
749
|
+
|
|
750
|
+
Solution:
|
|
751
|
+
Check stream configuration and resource availability.
|
|
752
|
+
|
|
753
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#streaming
|
|
754
|
+
`.trim();
|
|
755
|
+
}
|
|
756
|
+
super(message, {
|
|
757
|
+
...rest,
|
|
758
|
+
operation,
|
|
759
|
+
resource,
|
|
760
|
+
description
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
class MetadataLimitError extends S3dbError {
|
|
765
|
+
constructor(message, details = {}) {
|
|
766
|
+
const {
|
|
767
|
+
totalSize,
|
|
768
|
+
effectiveLimit,
|
|
769
|
+
absoluteLimit = 2047,
|
|
770
|
+
excess,
|
|
771
|
+
resourceName,
|
|
772
|
+
operation,
|
|
773
|
+
...rest
|
|
774
|
+
} = details;
|
|
775
|
+
let description = details.description;
|
|
776
|
+
if (!description && totalSize && effectiveLimit) {
|
|
777
|
+
description = `
|
|
778
|
+
S3 Metadata Size Limit Exceeded
|
|
779
|
+
|
|
780
|
+
Current Size: ${totalSize} bytes
|
|
781
|
+
Effective Limit: ${effectiveLimit} bytes
|
|
782
|
+
Absolute Limit: ${absoluteLimit} bytes
|
|
783
|
+
${excess ? `Excess: ${excess} bytes` : ""}
|
|
784
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
785
|
+
${operation ? `Operation: ${operation}` : ""}
|
|
786
|
+
|
|
787
|
+
S3 has a hard limit of 2KB (2047 bytes) for object metadata.
|
|
788
|
+
|
|
789
|
+
Solutions:
|
|
790
|
+
1. Use 'body-overflow' behavior to store excess in body
|
|
791
|
+
2. Use 'body-only' behavior to store everything in body
|
|
792
|
+
3. Reduce number of fields
|
|
793
|
+
4. Use shorter field values
|
|
794
|
+
5. Enable advanced metadata encoding
|
|
795
|
+
|
|
796
|
+
Example:
|
|
797
|
+
await db.createResource({
|
|
798
|
+
name: '${resourceName || "myResource"}',
|
|
799
|
+
behavior: 'body-overflow', // Automatically handles overflow
|
|
800
|
+
attributes: { ... }
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#metadata-size-limits
|
|
804
|
+
`.trim();
|
|
805
|
+
}
|
|
806
|
+
super(message, {
|
|
807
|
+
...rest,
|
|
808
|
+
totalSize,
|
|
809
|
+
effectiveLimit,
|
|
810
|
+
absoluteLimit,
|
|
811
|
+
excess,
|
|
812
|
+
resourceName,
|
|
813
|
+
operation,
|
|
814
|
+
description
|
|
562
815
|
});
|
|
563
816
|
}
|
|
564
817
|
}
|
|
@@ -902,10 +1155,17 @@ class PluginStorage {
|
|
|
902
1155
|
*/
|
|
903
1156
|
constructor(client, pluginSlug) {
|
|
904
1157
|
if (!client) {
|
|
905
|
-
throw new
|
|
1158
|
+
throw new PluginStorageError("PluginStorage requires a client instance", {
|
|
1159
|
+
operation: "constructor",
|
|
1160
|
+
pluginSlug,
|
|
1161
|
+
suggestion: "Pass a valid S3db Client instance when creating PluginStorage"
|
|
1162
|
+
});
|
|
906
1163
|
}
|
|
907
1164
|
if (!pluginSlug) {
|
|
908
|
-
throw new
|
|
1165
|
+
throw new PluginStorageError("PluginStorage requires a pluginSlug", {
|
|
1166
|
+
operation: "constructor",
|
|
1167
|
+
suggestion: 'Provide a plugin slug (e.g., "eventual-consistency", "cache", "audit")'
|
|
1168
|
+
});
|
|
909
1169
|
}
|
|
910
1170
|
this.client = client;
|
|
911
1171
|
this.pluginSlug = pluginSlug;
|
|
@@ -958,7 +1218,15 @@ class PluginStorage {
|
|
|
958
1218
|
}
|
|
959
1219
|
const [ok, err] = await tryFn(() => this.client.putObject(putParams));
|
|
960
1220
|
if (!ok) {
|
|
961
|
-
throw new
|
|
1221
|
+
throw new PluginStorageError(`Failed to save plugin data`, {
|
|
1222
|
+
pluginSlug: this.pluginSlug,
|
|
1223
|
+
key,
|
|
1224
|
+
operation: "set",
|
|
1225
|
+
behavior,
|
|
1226
|
+
ttl,
|
|
1227
|
+
original: err,
|
|
1228
|
+
suggestion: "Check S3 permissions and key format"
|
|
1229
|
+
});
|
|
962
1230
|
}
|
|
963
1231
|
}
|
|
964
1232
|
/**
|
|
@@ -980,7 +1248,13 @@ class PluginStorage {
|
|
|
980
1248
|
if (err.name === "NoSuchKey" || err.Code === "NoSuchKey") {
|
|
981
1249
|
return null;
|
|
982
1250
|
}
|
|
983
|
-
throw new
|
|
1251
|
+
throw new PluginStorageError(`Failed to retrieve plugin data`, {
|
|
1252
|
+
pluginSlug: this.pluginSlug,
|
|
1253
|
+
key,
|
|
1254
|
+
operation: "get",
|
|
1255
|
+
original: err,
|
|
1256
|
+
suggestion: "Check if the key exists and S3 permissions are correct"
|
|
1257
|
+
});
|
|
984
1258
|
}
|
|
985
1259
|
const metadata = response.Metadata || {};
|
|
986
1260
|
const parsedMetadata = this._parseMetadataValues(metadata);
|
|
@@ -993,7 +1267,13 @@ class PluginStorage {
|
|
|
993
1267
|
data = { ...parsedMetadata, ...body };
|
|
994
1268
|
}
|
|
995
1269
|
} catch (parseErr) {
|
|
996
|
-
throw new
|
|
1270
|
+
throw new PluginStorageError(`Failed to parse JSON body`, {
|
|
1271
|
+
pluginSlug: this.pluginSlug,
|
|
1272
|
+
key,
|
|
1273
|
+
operation: "get",
|
|
1274
|
+
original: parseErr,
|
|
1275
|
+
suggestion: "Body content may be corrupted. Check S3 object integrity"
|
|
1276
|
+
});
|
|
997
1277
|
}
|
|
998
1278
|
}
|
|
999
1279
|
const expiresAt = data._expiresat || data._expiresAt;
|
|
@@ -1054,7 +1334,15 @@ class PluginStorage {
|
|
|
1054
1334
|
() => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
|
|
1055
1335
|
);
|
|
1056
1336
|
if (!ok) {
|
|
1057
|
-
throw new
|
|
1337
|
+
throw new PluginStorageError(`Failed to list plugin data`, {
|
|
1338
|
+
pluginSlug: this.pluginSlug,
|
|
1339
|
+
operation: "list",
|
|
1340
|
+
prefix,
|
|
1341
|
+
fullPrefix,
|
|
1342
|
+
limit,
|
|
1343
|
+
original: err,
|
|
1344
|
+
suggestion: "Check S3 permissions and bucket configuration"
|
|
1345
|
+
});
|
|
1058
1346
|
}
|
|
1059
1347
|
const keys = result.Contents?.map((item) => item.Key) || [];
|
|
1060
1348
|
return this._removeKeyPrefix(keys);
|
|
@@ -1074,7 +1362,16 @@ class PluginStorage {
|
|
|
1074
1362
|
() => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
|
|
1075
1363
|
);
|
|
1076
1364
|
if (!ok) {
|
|
1077
|
-
throw new
|
|
1365
|
+
throw new PluginStorageError(`Failed to list resource data`, {
|
|
1366
|
+
pluginSlug: this.pluginSlug,
|
|
1367
|
+
operation: "listForResource",
|
|
1368
|
+
resourceName,
|
|
1369
|
+
subPrefix,
|
|
1370
|
+
fullPrefix,
|
|
1371
|
+
limit,
|
|
1372
|
+
original: err,
|
|
1373
|
+
suggestion: "Check resource name and S3 permissions"
|
|
1374
|
+
});
|
|
1078
1375
|
}
|
|
1079
1376
|
const keys = result.Contents?.map((item) => item.Key) || [];
|
|
1080
1377
|
return this._removeKeyPrefix(keys);
|
|
@@ -1214,7 +1511,13 @@ class PluginStorage {
|
|
|
1214
1511
|
async delete(key) {
|
|
1215
1512
|
const [ok, err] = await tryFn(() => this.client.deleteObject(key));
|
|
1216
1513
|
if (!ok) {
|
|
1217
|
-
throw new
|
|
1514
|
+
throw new PluginStorageError(`Failed to delete plugin data`, {
|
|
1515
|
+
pluginSlug: this.pluginSlug,
|
|
1516
|
+
key,
|
|
1517
|
+
operation: "delete",
|
|
1518
|
+
original: err,
|
|
1519
|
+
suggestion: "Check S3 delete permissions"
|
|
1520
|
+
});
|
|
1218
1521
|
}
|
|
1219
1522
|
}
|
|
1220
1523
|
/**
|
|
@@ -1401,16 +1704,28 @@ class PluginStorage {
|
|
|
1401
1704
|
const valueSize = calculateUTF8Bytes(encoded);
|
|
1402
1705
|
currentSize += keySize + valueSize;
|
|
1403
1706
|
if (currentSize > effectiveLimit) {
|
|
1404
|
-
throw new
|
|
1405
|
-
|
|
1406
|
-
|
|
1707
|
+
throw new MetadataLimitError(`Data exceeds metadata limit with enforce-limits behavior`, {
|
|
1708
|
+
totalSize: currentSize,
|
|
1709
|
+
effectiveLimit,
|
|
1710
|
+
absoluteLimit: S3_METADATA_LIMIT,
|
|
1711
|
+
excess: currentSize - effectiveLimit,
|
|
1712
|
+
operation: "PluginStorage.set",
|
|
1713
|
+
pluginSlug: this.pluginSlug,
|
|
1714
|
+
suggestion: "Use 'body-overflow' or 'body-only' behavior to handle large data"
|
|
1715
|
+
});
|
|
1407
1716
|
}
|
|
1408
1717
|
metadata[key] = jsonValue;
|
|
1409
1718
|
}
|
|
1410
1719
|
break;
|
|
1411
1720
|
}
|
|
1412
1721
|
default:
|
|
1413
|
-
throw new
|
|
1722
|
+
throw new BehaviorError(`Unknown behavior: ${behavior}`, {
|
|
1723
|
+
behavior,
|
|
1724
|
+
availableBehaviors: ["body-overflow", "body-only", "enforce-limits"],
|
|
1725
|
+
operation: "PluginStorage._applyBehavior",
|
|
1726
|
+
pluginSlug: this.pluginSlug,
|
|
1727
|
+
suggestion: "Use 'body-overflow', 'body-only', or 'enforce-limits'"
|
|
1728
|
+
});
|
|
1414
1729
|
}
|
|
1415
1730
|
return { metadata, body };
|
|
1416
1731
|
}
|
|
@@ -1975,6 +2290,35 @@ class AuditPlugin extends Plugin {
|
|
|
1975
2290
|
}
|
|
1976
2291
|
}
|
|
1977
2292
|
|
|
2293
|
+
class BackupError extends S3dbError {
|
|
2294
|
+
constructor(message, details = {}) {
|
|
2295
|
+
const { driver = "unknown", operation = "unknown", backupId, ...rest } = details;
|
|
2296
|
+
let description = details.description;
|
|
2297
|
+
if (!description) {
|
|
2298
|
+
description = `
|
|
2299
|
+
Backup Operation Error
|
|
2300
|
+
|
|
2301
|
+
Driver: ${driver}
|
|
2302
|
+
Operation: ${operation}
|
|
2303
|
+
${backupId ? `Backup ID: ${backupId}` : ""}
|
|
2304
|
+
|
|
2305
|
+
Common causes:
|
|
2306
|
+
1. Invalid backup driver configuration
|
|
2307
|
+
2. Destination storage not accessible
|
|
2308
|
+
3. Insufficient permissions
|
|
2309
|
+
4. Network connectivity issues
|
|
2310
|
+
5. Invalid backup file format
|
|
2311
|
+
|
|
2312
|
+
Solution:
|
|
2313
|
+
Check driver configuration and ensure destination storage is accessible.
|
|
2314
|
+
|
|
2315
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/backup.md
|
|
2316
|
+
`.trim();
|
|
2317
|
+
}
|
|
2318
|
+
super(message, { ...rest, driver, operation, backupId, description });
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
|
|
1978
2322
|
class BaseBackupDriver {
|
|
1979
2323
|
constructor(config = {}) {
|
|
1980
2324
|
this.config = {
|
|
@@ -2005,7 +2349,12 @@ class BaseBackupDriver {
|
|
|
2005
2349
|
* @returns {Object} Upload result with destination info
|
|
2006
2350
|
*/
|
|
2007
2351
|
async upload(filePath, backupId, manifest) {
|
|
2008
|
-
throw new
|
|
2352
|
+
throw new BackupError("upload() method must be implemented by subclass", {
|
|
2353
|
+
operation: "upload",
|
|
2354
|
+
driver: this.constructor.name,
|
|
2355
|
+
backupId,
|
|
2356
|
+
suggestion: "Extend BaseBackupDriver and implement the upload() method"
|
|
2357
|
+
});
|
|
2009
2358
|
}
|
|
2010
2359
|
/**
|
|
2011
2360
|
* Download a backup file from the destination
|
|
@@ -2015,7 +2364,12 @@ class BaseBackupDriver {
|
|
|
2015
2364
|
* @returns {string} Path to downloaded file
|
|
2016
2365
|
*/
|
|
2017
2366
|
async download(backupId, targetPath, metadata) {
|
|
2018
|
-
throw new
|
|
2367
|
+
throw new BackupError("download() method must be implemented by subclass", {
|
|
2368
|
+
operation: "download",
|
|
2369
|
+
driver: this.constructor.name,
|
|
2370
|
+
backupId,
|
|
2371
|
+
suggestion: "Extend BaseBackupDriver and implement the download() method"
|
|
2372
|
+
});
|
|
2019
2373
|
}
|
|
2020
2374
|
/**
|
|
2021
2375
|
* Delete a backup from the destination
|
|
@@ -2023,7 +2377,12 @@ class BaseBackupDriver {
|
|
|
2023
2377
|
* @param {Object} metadata - Backup metadata
|
|
2024
2378
|
*/
|
|
2025
2379
|
async delete(backupId, metadata) {
|
|
2026
|
-
throw new
|
|
2380
|
+
throw new BackupError("delete() method must be implemented by subclass", {
|
|
2381
|
+
operation: "delete",
|
|
2382
|
+
driver: this.constructor.name,
|
|
2383
|
+
backupId,
|
|
2384
|
+
suggestion: "Extend BaseBackupDriver and implement the delete() method"
|
|
2385
|
+
});
|
|
2027
2386
|
}
|
|
2028
2387
|
/**
|
|
2029
2388
|
* List backups available in the destination
|
|
@@ -2031,7 +2390,11 @@ class BaseBackupDriver {
|
|
|
2031
2390
|
* @returns {Array} List of backup metadata
|
|
2032
2391
|
*/
|
|
2033
2392
|
async list(options = {}) {
|
|
2034
|
-
throw new
|
|
2393
|
+
throw new BackupError("list() method must be implemented by subclass", {
|
|
2394
|
+
operation: "list",
|
|
2395
|
+
driver: this.constructor.name,
|
|
2396
|
+
suggestion: "Extend BaseBackupDriver and implement the list() method"
|
|
2397
|
+
});
|
|
2035
2398
|
}
|
|
2036
2399
|
/**
|
|
2037
2400
|
* Verify backup integrity
|
|
@@ -2041,14 +2404,23 @@ class BaseBackupDriver {
|
|
|
2041
2404
|
* @returns {boolean} True if backup is valid
|
|
2042
2405
|
*/
|
|
2043
2406
|
async verify(backupId, expectedChecksum, metadata) {
|
|
2044
|
-
throw new
|
|
2407
|
+
throw new BackupError("verify() method must be implemented by subclass", {
|
|
2408
|
+
operation: "verify",
|
|
2409
|
+
driver: this.constructor.name,
|
|
2410
|
+
backupId,
|
|
2411
|
+
suggestion: "Extend BaseBackupDriver and implement the verify() method"
|
|
2412
|
+
});
|
|
2045
2413
|
}
|
|
2046
2414
|
/**
|
|
2047
2415
|
* Get driver type identifier
|
|
2048
2416
|
* @returns {string} Driver type
|
|
2049
2417
|
*/
|
|
2050
2418
|
getType() {
|
|
2051
|
-
throw new
|
|
2419
|
+
throw new BackupError("getType() method must be implemented by subclass", {
|
|
2420
|
+
operation: "getType",
|
|
2421
|
+
driver: this.constructor.name,
|
|
2422
|
+
suggestion: "Extend BaseBackupDriver and implement the getType() method"
|
|
2423
|
+
});
|
|
2052
2424
|
}
|
|
2053
2425
|
/**
|
|
2054
2426
|
* Get driver-specific storage info
|
|
@@ -2090,7 +2462,11 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2090
2462
|
}
|
|
2091
2463
|
async onSetup() {
|
|
2092
2464
|
if (!this.config.path) {
|
|
2093
|
-
throw new
|
|
2465
|
+
throw new BackupError("FilesystemBackupDriver: path configuration is required", {
|
|
2466
|
+
operation: "onSetup",
|
|
2467
|
+
driver: "filesystem",
|
|
2468
|
+
suggestion: 'Provide a path in config: new FilesystemBackupDriver({ path: "/path/to/backups" })'
|
|
2469
|
+
});
|
|
2094
2470
|
}
|
|
2095
2471
|
this.log(`Initialized with path: ${this.config.path}`);
|
|
2096
2472
|
}
|
|
@@ -2114,11 +2490,26 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2114
2490
|
() => promises.mkdir(targetDir, { recursive: true, mode: this.config.directoryPermissions })
|
|
2115
2491
|
);
|
|
2116
2492
|
if (!createDirOk) {
|
|
2117
|
-
throw new
|
|
2493
|
+
throw new BackupError("Failed to create backup directory", {
|
|
2494
|
+
operation: "upload",
|
|
2495
|
+
driver: "filesystem",
|
|
2496
|
+
backupId,
|
|
2497
|
+
targetDir,
|
|
2498
|
+
original: createDirErr,
|
|
2499
|
+
suggestion: "Check directory permissions and disk space"
|
|
2500
|
+
});
|
|
2118
2501
|
}
|
|
2119
2502
|
const [copyOk, copyErr] = await tryFn(() => promises.copyFile(filePath, targetPath));
|
|
2120
2503
|
if (!copyOk) {
|
|
2121
|
-
throw new
|
|
2504
|
+
throw new BackupError("Failed to copy backup file", {
|
|
2505
|
+
operation: "upload",
|
|
2506
|
+
driver: "filesystem",
|
|
2507
|
+
backupId,
|
|
2508
|
+
filePath,
|
|
2509
|
+
targetPath,
|
|
2510
|
+
original: copyErr,
|
|
2511
|
+
suggestion: "Check file permissions and disk space"
|
|
2512
|
+
});
|
|
2122
2513
|
}
|
|
2123
2514
|
const [manifestOk, manifestErr] = await tryFn(
|
|
2124
2515
|
() => import('fs/promises').then((fs) => fs.writeFile(
|
|
@@ -2129,7 +2520,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2129
2520
|
);
|
|
2130
2521
|
if (!manifestOk) {
|
|
2131
2522
|
await tryFn(() => promises.unlink(targetPath));
|
|
2132
|
-
throw new
|
|
2523
|
+
throw new BackupError("Failed to write manifest file", {
|
|
2524
|
+
operation: "upload",
|
|
2525
|
+
driver: "filesystem",
|
|
2526
|
+
backupId,
|
|
2527
|
+
manifestPath,
|
|
2528
|
+
original: manifestErr,
|
|
2529
|
+
suggestion: "Check directory permissions and disk space"
|
|
2530
|
+
});
|
|
2133
2531
|
}
|
|
2134
2532
|
const [statOk, , stats] = await tryFn(() => promises.stat(targetPath));
|
|
2135
2533
|
const size = statOk ? stats.size : 0;
|
|
@@ -2148,13 +2546,27 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2148
2546
|
);
|
|
2149
2547
|
const [existsOk] = await tryFn(() => promises.access(sourcePath));
|
|
2150
2548
|
if (!existsOk) {
|
|
2151
|
-
throw new
|
|
2549
|
+
throw new BackupError("Backup file not found", {
|
|
2550
|
+
operation: "download",
|
|
2551
|
+
driver: "filesystem",
|
|
2552
|
+
backupId,
|
|
2553
|
+
sourcePath,
|
|
2554
|
+
suggestion: "Check if backup exists using list() method"
|
|
2555
|
+
});
|
|
2152
2556
|
}
|
|
2153
2557
|
const targetDir = path.dirname(targetPath);
|
|
2154
2558
|
await tryFn(() => promises.mkdir(targetDir, { recursive: true }));
|
|
2155
2559
|
const [copyOk, copyErr] = await tryFn(() => promises.copyFile(sourcePath, targetPath));
|
|
2156
2560
|
if (!copyOk) {
|
|
2157
|
-
throw new
|
|
2561
|
+
throw new BackupError("Failed to download backup", {
|
|
2562
|
+
operation: "download",
|
|
2563
|
+
driver: "filesystem",
|
|
2564
|
+
backupId,
|
|
2565
|
+
sourcePath,
|
|
2566
|
+
targetPath,
|
|
2567
|
+
original: copyErr,
|
|
2568
|
+
suggestion: "Check file permissions and disk space"
|
|
2569
|
+
});
|
|
2158
2570
|
}
|
|
2159
2571
|
this.log(`Downloaded backup ${backupId} from ${sourcePath} to ${targetPath}`);
|
|
2160
2572
|
return targetPath;
|
|
@@ -2171,7 +2583,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2171
2583
|
const [deleteBackupOk] = await tryFn(() => promises.unlink(backupPath));
|
|
2172
2584
|
const [deleteManifestOk] = await tryFn(() => promises.unlink(manifestPath));
|
|
2173
2585
|
if (!deleteBackupOk && !deleteManifestOk) {
|
|
2174
|
-
throw new
|
|
2586
|
+
throw new BackupError("Failed to delete backup files", {
|
|
2587
|
+
operation: "delete",
|
|
2588
|
+
driver: "filesystem",
|
|
2589
|
+
backupId,
|
|
2590
|
+
backupPath,
|
|
2591
|
+
manifestPath,
|
|
2592
|
+
suggestion: "Check file permissions"
|
|
2593
|
+
});
|
|
2175
2594
|
}
|
|
2176
2595
|
this.log(`Deleted backup ${backupId}`);
|
|
2177
2596
|
}
|
|
@@ -2276,10 +2695,18 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2276
2695
|
this.config.bucket = this.database.bucket;
|
|
2277
2696
|
}
|
|
2278
2697
|
if (!this.config.client) {
|
|
2279
|
-
throw new
|
|
2698
|
+
throw new BackupError("S3BackupDriver: client is required", {
|
|
2699
|
+
operation: "onSetup",
|
|
2700
|
+
driver: "s3",
|
|
2701
|
+
suggestion: "Provide a client in config or ensure database has a client configured"
|
|
2702
|
+
});
|
|
2280
2703
|
}
|
|
2281
2704
|
if (!this.config.bucket) {
|
|
2282
|
-
throw new
|
|
2705
|
+
throw new BackupError("S3BackupDriver: bucket is required", {
|
|
2706
|
+
operation: "onSetup",
|
|
2707
|
+
driver: "s3",
|
|
2708
|
+
suggestion: "Provide a bucket in config or ensure database has a bucket configured"
|
|
2709
|
+
});
|
|
2283
2710
|
}
|
|
2284
2711
|
this.log(`Initialized with bucket: ${this.config.bucket}, path: ${this.config.path}`);
|
|
2285
2712
|
}
|
|
@@ -2321,7 +2748,15 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2321
2748
|
});
|
|
2322
2749
|
});
|
|
2323
2750
|
if (!uploadOk) {
|
|
2324
|
-
throw new
|
|
2751
|
+
throw new BackupError("Failed to upload backup file to S3", {
|
|
2752
|
+
operation: "upload",
|
|
2753
|
+
driver: "s3",
|
|
2754
|
+
backupId,
|
|
2755
|
+
bucket: this.config.bucket,
|
|
2756
|
+
key: backupKey,
|
|
2757
|
+
original: uploadErr,
|
|
2758
|
+
suggestion: "Check S3 permissions and bucket configuration"
|
|
2759
|
+
});
|
|
2325
2760
|
}
|
|
2326
2761
|
const [manifestOk, manifestErr] = await tryFn(
|
|
2327
2762
|
() => this.config.client.uploadObject({
|
|
@@ -2342,7 +2777,15 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2342
2777
|
bucket: this.config.bucket,
|
|
2343
2778
|
key: backupKey
|
|
2344
2779
|
}));
|
|
2345
|
-
throw new
|
|
2780
|
+
throw new BackupError("Failed to upload manifest to S3", {
|
|
2781
|
+
operation: "upload",
|
|
2782
|
+
driver: "s3",
|
|
2783
|
+
backupId,
|
|
2784
|
+
bucket: this.config.bucket,
|
|
2785
|
+
manifestKey,
|
|
2786
|
+
original: manifestErr,
|
|
2787
|
+
suggestion: "Check S3 permissions and bucket configuration"
|
|
2788
|
+
});
|
|
2346
2789
|
}
|
|
2347
2790
|
this.log(`Uploaded backup ${backupId} to s3://${this.config.bucket}/${backupKey} (${fileSize} bytes)`);
|
|
2348
2791
|
return {
|
|
@@ -2365,7 +2808,16 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2365
2808
|
})
|
|
2366
2809
|
);
|
|
2367
2810
|
if (!downloadOk) {
|
|
2368
|
-
throw new
|
|
2811
|
+
throw new BackupError("Failed to download backup from S3", {
|
|
2812
|
+
operation: "download",
|
|
2813
|
+
driver: "s3",
|
|
2814
|
+
backupId,
|
|
2815
|
+
bucket: this.config.bucket,
|
|
2816
|
+
key: backupKey,
|
|
2817
|
+
targetPath,
|
|
2818
|
+
original: downloadErr,
|
|
2819
|
+
suggestion: "Check if backup exists and S3 permissions are correct"
|
|
2820
|
+
});
|
|
2369
2821
|
}
|
|
2370
2822
|
this.log(`Downloaded backup ${backupId} from s3://${this.config.bucket}/${backupKey} to ${targetPath}`);
|
|
2371
2823
|
return targetPath;
|
|
@@ -2386,7 +2838,15 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2386
2838
|
})
|
|
2387
2839
|
);
|
|
2388
2840
|
if (!deleteBackupOk && !deleteManifestOk) {
|
|
2389
|
-
throw new
|
|
2841
|
+
throw new BackupError("Failed to delete backup from S3", {
|
|
2842
|
+
operation: "delete",
|
|
2843
|
+
driver: "s3",
|
|
2844
|
+
backupId,
|
|
2845
|
+
bucket: this.config.bucket,
|
|
2846
|
+
backupKey,
|
|
2847
|
+
manifestKey,
|
|
2848
|
+
suggestion: "Check S3 delete permissions"
|
|
2849
|
+
});
|
|
2390
2850
|
}
|
|
2391
2851
|
this.log(`Deleted backup ${backupId} from S3`);
|
|
2392
2852
|
}
|
|
@@ -2499,11 +2959,22 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2499
2959
|
}
|
|
2500
2960
|
async onSetup() {
|
|
2501
2961
|
if (!Array.isArray(this.config.destinations) || this.config.destinations.length === 0) {
|
|
2502
|
-
throw new
|
|
2962
|
+
throw new BackupError("MultiBackupDriver requires non-empty destinations array", {
|
|
2963
|
+
operation: "onSetup",
|
|
2964
|
+
driver: "multi",
|
|
2965
|
+
destinationsProvided: this.config.destinations,
|
|
2966
|
+
suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }, { driver: "filesystem", config: {...} }] }'
|
|
2967
|
+
});
|
|
2503
2968
|
}
|
|
2504
2969
|
for (const [index, destConfig] of this.config.destinations.entries()) {
|
|
2505
2970
|
if (!destConfig.driver) {
|
|
2506
|
-
throw new
|
|
2971
|
+
throw new BackupError(`Destination ${index} missing driver type`, {
|
|
2972
|
+
operation: "onSetup",
|
|
2973
|
+
driver: "multi",
|
|
2974
|
+
destinationIndex: index,
|
|
2975
|
+
destination: destConfig,
|
|
2976
|
+
suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} } or { driver: "filesystem", config: {...} }'
|
|
2977
|
+
});
|
|
2507
2978
|
}
|
|
2508
2979
|
try {
|
|
2509
2980
|
const driver = createBackupDriver(destConfig.driver, destConfig.config || {});
|
|
@@ -2515,7 +2986,15 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2515
2986
|
});
|
|
2516
2987
|
this.log(`Setup destination ${index}: ${destConfig.driver}`);
|
|
2517
2988
|
} catch (error) {
|
|
2518
|
-
throw new
|
|
2989
|
+
throw new BackupError(`Failed to setup destination ${index}`, {
|
|
2990
|
+
operation: "onSetup",
|
|
2991
|
+
driver: "multi",
|
|
2992
|
+
destinationIndex: index,
|
|
2993
|
+
destinationDriver: destConfig.driver,
|
|
2994
|
+
destinationConfig: destConfig.config,
|
|
2995
|
+
original: error,
|
|
2996
|
+
suggestion: "Check destination driver configuration and ensure dependencies are available"
|
|
2997
|
+
});
|
|
2519
2998
|
}
|
|
2520
2999
|
}
|
|
2521
3000
|
if (this.config.requireAll === false) {
|
|
@@ -2544,7 +3023,15 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2544
3023
|
this.log(`Priority upload failed to destination ${index}: ${err.message}`);
|
|
2545
3024
|
}
|
|
2546
3025
|
}
|
|
2547
|
-
throw new
|
|
3026
|
+
throw new BackupError("All priority destinations failed", {
|
|
3027
|
+
operation: "upload",
|
|
3028
|
+
driver: "multi",
|
|
3029
|
+
strategy: "priority",
|
|
3030
|
+
backupId,
|
|
3031
|
+
totalDestinations: this.drivers.length,
|
|
3032
|
+
failures: errors,
|
|
3033
|
+
suggestion: "Check destination configurations and ensure at least one destination is accessible"
|
|
3034
|
+
});
|
|
2548
3035
|
}
|
|
2549
3036
|
const uploadPromises = this.drivers.map(async ({ driver, config, index }) => {
|
|
2550
3037
|
const [ok, err, result] = await tryFn(
|
|
@@ -2574,10 +3061,28 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2574
3061
|
const successResults = allResults.filter((r) => r.status === "success");
|
|
2575
3062
|
const failedResults = allResults.filter((r) => r.status === "failed");
|
|
2576
3063
|
if (strategy === "all" && failedResults.length > 0) {
|
|
2577
|
-
throw new
|
|
3064
|
+
throw new BackupError('Some destinations failed with strategy "all"', {
|
|
3065
|
+
operation: "upload",
|
|
3066
|
+
driver: "multi",
|
|
3067
|
+
strategy: "all",
|
|
3068
|
+
backupId,
|
|
3069
|
+
totalDestinations: this.drivers.length,
|
|
3070
|
+
successCount: successResults.length,
|
|
3071
|
+
failedCount: failedResults.length,
|
|
3072
|
+
failures: failedResults,
|
|
3073
|
+
suggestion: 'All destinations must succeed with "all" strategy. Use "any" strategy to tolerate failures, or fix failing destinations.'
|
|
3074
|
+
});
|
|
2578
3075
|
}
|
|
2579
3076
|
if (strategy === "any" && successResults.length === 0) {
|
|
2580
|
-
throw new
|
|
3077
|
+
throw new BackupError('All destinations failed with strategy "any"', {
|
|
3078
|
+
operation: "upload",
|
|
3079
|
+
driver: "multi",
|
|
3080
|
+
strategy: "any",
|
|
3081
|
+
backupId,
|
|
3082
|
+
totalDestinations: this.drivers.length,
|
|
3083
|
+
failures: failedResults,
|
|
3084
|
+
suggestion: 'At least one destination must succeed with "any" strategy. Check all destination configurations.'
|
|
3085
|
+
});
|
|
2581
3086
|
}
|
|
2582
3087
|
return allResults;
|
|
2583
3088
|
}
|
|
@@ -2597,7 +3102,14 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2597
3102
|
this.log(`Download failed from destination ${destMetadata.destination}: ${err.message}`);
|
|
2598
3103
|
}
|
|
2599
3104
|
}
|
|
2600
|
-
throw new
|
|
3105
|
+
throw new BackupError("Failed to download backup from any destination", {
|
|
3106
|
+
operation: "download",
|
|
3107
|
+
driver: "multi",
|
|
3108
|
+
backupId,
|
|
3109
|
+
targetPath,
|
|
3110
|
+
attemptedDestinations: destinations.length,
|
|
3111
|
+
suggestion: "Check if backup exists in at least one destination and destinations are accessible"
|
|
3112
|
+
});
|
|
2601
3113
|
}
|
|
2602
3114
|
async delete(backupId, metadata) {
|
|
2603
3115
|
const destinations = Array.isArray(metadata.destinations) ? metadata.destinations : [metadata];
|
|
@@ -2619,7 +3131,14 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2619
3131
|
}
|
|
2620
3132
|
}
|
|
2621
3133
|
if (successCount === 0 && errors.length > 0) {
|
|
2622
|
-
throw new
|
|
3134
|
+
throw new BackupError("Failed to delete from any destination", {
|
|
3135
|
+
operation: "delete",
|
|
3136
|
+
driver: "multi",
|
|
3137
|
+
backupId,
|
|
3138
|
+
attemptedDestinations: destinations.length,
|
|
3139
|
+
failures: errors,
|
|
3140
|
+
suggestion: "Check if backup exists in destinations and destinations are accessible with delete permissions"
|
|
3141
|
+
});
|
|
2623
3142
|
}
|
|
2624
3143
|
if (errors.length > 0) {
|
|
2625
3144
|
this.log(`Partial delete success, some errors: ${errors.join("; ")}`);
|
|
@@ -2719,32 +3238,62 @@ const BACKUP_DRIVERS = {
|
|
|
2719
3238
|
function createBackupDriver(driver, config = {}) {
|
|
2720
3239
|
const DriverClass = BACKUP_DRIVERS[driver];
|
|
2721
3240
|
if (!DriverClass) {
|
|
2722
|
-
throw new
|
|
3241
|
+
throw new BackupError(`Unknown backup driver: ${driver}`, {
|
|
3242
|
+
operation: "createBackupDriver",
|
|
3243
|
+
driver,
|
|
3244
|
+
availableDrivers: Object.keys(BACKUP_DRIVERS),
|
|
3245
|
+
suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
|
|
3246
|
+
});
|
|
2723
3247
|
}
|
|
2724
3248
|
return new DriverClass(config);
|
|
2725
3249
|
}
|
|
2726
3250
|
function validateBackupConfig(driver, config = {}) {
|
|
2727
3251
|
if (!driver || typeof driver !== "string") {
|
|
2728
|
-
throw new
|
|
3252
|
+
throw new BackupError("Driver type must be a non-empty string", {
|
|
3253
|
+
operation: "validateBackupConfig",
|
|
3254
|
+
driver,
|
|
3255
|
+
suggestion: "Provide a valid driver type string (filesystem, s3, or multi)"
|
|
3256
|
+
});
|
|
2729
3257
|
}
|
|
2730
3258
|
if (!BACKUP_DRIVERS[driver]) {
|
|
2731
|
-
throw new
|
|
3259
|
+
throw new BackupError(`Unknown backup driver: ${driver}`, {
|
|
3260
|
+
operation: "validateBackupConfig",
|
|
3261
|
+
driver,
|
|
3262
|
+
availableDrivers: Object.keys(BACKUP_DRIVERS),
|
|
3263
|
+
suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
|
|
3264
|
+
});
|
|
2732
3265
|
}
|
|
2733
3266
|
switch (driver) {
|
|
2734
3267
|
case "filesystem":
|
|
2735
3268
|
if (!config.path) {
|
|
2736
|
-
throw new
|
|
3269
|
+
throw new BackupError('FilesystemBackupDriver requires "path" configuration', {
|
|
3270
|
+
operation: "validateBackupConfig",
|
|
3271
|
+
driver: "filesystem",
|
|
3272
|
+
config,
|
|
3273
|
+
suggestion: 'Provide a "path" property in config: { path: "/path/to/backups" }'
|
|
3274
|
+
});
|
|
2737
3275
|
}
|
|
2738
3276
|
break;
|
|
2739
3277
|
case "s3":
|
|
2740
3278
|
break;
|
|
2741
3279
|
case "multi":
|
|
2742
3280
|
if (!Array.isArray(config.destinations) || config.destinations.length === 0) {
|
|
2743
|
-
throw new
|
|
3281
|
+
throw new BackupError('MultiBackupDriver requires non-empty "destinations" array', {
|
|
3282
|
+
operation: "validateBackupConfig",
|
|
3283
|
+
driver: "multi",
|
|
3284
|
+
config,
|
|
3285
|
+
suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }] }'
|
|
3286
|
+
});
|
|
2744
3287
|
}
|
|
2745
3288
|
config.destinations.forEach((dest, index) => {
|
|
2746
3289
|
if (!dest.driver) {
|
|
2747
|
-
throw new
|
|
3290
|
+
throw new BackupError(`Destination ${index} must have a "driver" property`, {
|
|
3291
|
+
operation: "validateBackupConfig",
|
|
3292
|
+
driver: "multi",
|
|
3293
|
+
destinationIndex: index,
|
|
3294
|
+
destination: dest,
|
|
3295
|
+
suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} }'
|
|
3296
|
+
});
|
|
2748
3297
|
}
|
|
2749
3298
|
if (dest.driver !== "multi") {
|
|
2750
3299
|
validateBackupConfig(dest.driver, dest.config || {});
|
|
@@ -3400,6 +3949,36 @@ class BackupPlugin extends Plugin {
|
|
|
3400
3949
|
}
|
|
3401
3950
|
}
|
|
3402
3951
|
|
|
3952
|
+
class CacheError extends S3dbError {
|
|
3953
|
+
constructor(message, details = {}) {
|
|
3954
|
+
const { driver = "unknown", operation = "unknown", resourceName, key, ...rest } = details;
|
|
3955
|
+
let description = details.description;
|
|
3956
|
+
if (!description) {
|
|
3957
|
+
description = `
|
|
3958
|
+
Cache Operation Error
|
|
3959
|
+
|
|
3960
|
+
Driver: ${driver}
|
|
3961
|
+
Operation: ${operation}
|
|
3962
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
3963
|
+
${key ? `Key: ${key}` : ""}
|
|
3964
|
+
|
|
3965
|
+
Common causes:
|
|
3966
|
+
1. Invalid cache key format
|
|
3967
|
+
2. Cache driver not properly initialized
|
|
3968
|
+
3. Resource not found or not cached
|
|
3969
|
+
4. Memory limits exceeded
|
|
3970
|
+
5. Filesystem permissions issues
|
|
3971
|
+
|
|
3972
|
+
Solution:
|
|
3973
|
+
Check cache configuration and ensure the cache driver is properly initialized.
|
|
3974
|
+
|
|
3975
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/cache.md
|
|
3976
|
+
`.trim();
|
|
3977
|
+
}
|
|
3978
|
+
super(message, { ...rest, driver, operation, resourceName, key, description });
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3403
3982
|
class Cache extends EventEmitter {
|
|
3404
3983
|
constructor(config = {}) {
|
|
3405
3984
|
super();
|
|
@@ -3416,7 +3995,13 @@ class Cache extends EventEmitter {
|
|
|
3416
3995
|
}
|
|
3417
3996
|
validateKey(key) {
|
|
3418
3997
|
if (key === null || key === void 0 || typeof key !== "string" || !key) {
|
|
3419
|
-
throw new
|
|
3998
|
+
throw new CacheError("Invalid cache key", {
|
|
3999
|
+
operation: "validateKey",
|
|
4000
|
+
driver: this.constructor.name,
|
|
4001
|
+
key,
|
|
4002
|
+
keyType: typeof key,
|
|
4003
|
+
suggestion: "Cache key must be a non-empty string"
|
|
4004
|
+
});
|
|
3420
4005
|
}
|
|
3421
4006
|
}
|
|
3422
4007
|
// generic class methods
|
|
@@ -3503,7 +4088,11 @@ class ResourceReader extends EventEmitter {
|
|
|
3503
4088
|
constructor({ resource, batchSize = 10, concurrency = 5 }) {
|
|
3504
4089
|
super();
|
|
3505
4090
|
if (!resource) {
|
|
3506
|
-
throw new
|
|
4091
|
+
throw new StreamError("Resource is required for ResourceReader", {
|
|
4092
|
+
operation: "constructor",
|
|
4093
|
+
resource: resource?.name,
|
|
4094
|
+
suggestion: "Pass a valid Resource instance when creating ResourceReader"
|
|
4095
|
+
});
|
|
3507
4096
|
}
|
|
3508
4097
|
this.resource = resource;
|
|
3509
4098
|
this.client = resource.client;
|
|
@@ -3627,7 +4216,10 @@ class ResourceWriter extends EventEmitter {
|
|
|
3627
4216
|
function streamToString(stream) {
|
|
3628
4217
|
return new Promise((resolve, reject) => {
|
|
3629
4218
|
if (!stream) {
|
|
3630
|
-
return reject(new
|
|
4219
|
+
return reject(new StreamError("Stream is undefined", {
|
|
4220
|
+
operation: "streamToString",
|
|
4221
|
+
suggestion: "Ensure a valid stream is passed to streamToString()"
|
|
4222
|
+
}));
|
|
3631
4223
|
}
|
|
3632
4224
|
const chunks = [];
|
|
3633
4225
|
stream.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -5121,7 +5713,13 @@ class CachePlugin extends Plugin {
|
|
|
5121
5713
|
async warmCache(resourceName, options = {}) {
|
|
5122
5714
|
const resource = this.database.resources[resourceName];
|
|
5123
5715
|
if (!resource) {
|
|
5124
|
-
throw new
|
|
5716
|
+
throw new CacheError("Resource not found for cache warming", {
|
|
5717
|
+
operation: "warmCache",
|
|
5718
|
+
driver: this.driver?.constructor.name,
|
|
5719
|
+
resourceName,
|
|
5720
|
+
availableResources: Object.keys(this.database.resources),
|
|
5721
|
+
suggestion: "Check resource name spelling or ensure resource has been created"
|
|
5722
|
+
});
|
|
5125
5723
|
}
|
|
5126
5724
|
const { includePartitions = true, sampleSize = 100 } = options;
|
|
5127
5725
|
if (this.driver instanceof PartitionAwareFilesystemCache && resource.warmPartitionCache) {
|
|
@@ -8238,6 +8836,35 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
8238
8836
|
}
|
|
8239
8837
|
}
|
|
8240
8838
|
|
|
8839
|
+
class FulltextError extends S3dbError {
|
|
8840
|
+
constructor(message, details = {}) {
|
|
8841
|
+
const { resourceName, query, operation = "unknown", ...rest } = details;
|
|
8842
|
+
let description = details.description;
|
|
8843
|
+
if (!description) {
|
|
8844
|
+
description = `
|
|
8845
|
+
Fulltext Search Operation Error
|
|
8846
|
+
|
|
8847
|
+
Operation: ${operation}
|
|
8848
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
8849
|
+
${query ? `Query: ${query}` : ""}
|
|
8850
|
+
|
|
8851
|
+
Common causes:
|
|
8852
|
+
1. Resource not indexed for fulltext search
|
|
8853
|
+
2. Invalid query syntax
|
|
8854
|
+
3. Index not built yet
|
|
8855
|
+
4. Search configuration missing
|
|
8856
|
+
5. Field not indexed
|
|
8857
|
+
|
|
8858
|
+
Solution:
|
|
8859
|
+
Ensure resource is configured for fulltext search and index is built.
|
|
8860
|
+
|
|
8861
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/fulltext.md
|
|
8862
|
+
`.trim();
|
|
8863
|
+
}
|
|
8864
|
+
super(message, { ...rest, resourceName, query, operation, description });
|
|
8865
|
+
}
|
|
8866
|
+
}
|
|
8867
|
+
|
|
8241
8868
|
class FullTextPlugin extends Plugin {
|
|
8242
8869
|
constructor(options = {}) {
|
|
8243
8870
|
super();
|
|
@@ -8544,7 +9171,13 @@ class FullTextPlugin extends Plugin {
|
|
|
8544
9171
|
}
|
|
8545
9172
|
const resource = this.database.resources[resourceName];
|
|
8546
9173
|
if (!resource) {
|
|
8547
|
-
throw new
|
|
9174
|
+
throw new FulltextError(`Resource '${resourceName}' not found`, {
|
|
9175
|
+
operation: "searchRecords",
|
|
9176
|
+
resourceName,
|
|
9177
|
+
query,
|
|
9178
|
+
availableResources: Object.keys(this.database.resources),
|
|
9179
|
+
suggestion: "Check resource name or ensure resource is created before searching"
|
|
9180
|
+
});
|
|
8548
9181
|
}
|
|
8549
9182
|
const recordIds = searchResults.map((result2) => result2.recordId);
|
|
8550
9183
|
const records = await resource.getMany(recordIds);
|
|
@@ -8561,7 +9194,12 @@ class FullTextPlugin extends Plugin {
|
|
|
8561
9194
|
async rebuildIndex(resourceName) {
|
|
8562
9195
|
const resource = this.database.resources[resourceName];
|
|
8563
9196
|
if (!resource) {
|
|
8564
|
-
throw new
|
|
9197
|
+
throw new FulltextError(`Resource '${resourceName}' not found`, {
|
|
9198
|
+
operation: "rebuildIndex",
|
|
9199
|
+
resourceName,
|
|
9200
|
+
availableResources: Object.keys(this.database.resources),
|
|
9201
|
+
suggestion: "Check resource name or ensure resource is created before rebuilding index"
|
|
9202
|
+
});
|
|
8565
9203
|
}
|
|
8566
9204
|
for (const [key] of this.indexes.entries()) {
|
|
8567
9205
|
if (key.startsWith(`${resourceName}:`)) {
|
|
@@ -9346,6 +9984,35 @@ function createConsumer(driver, config) {
|
|
|
9346
9984
|
return new ConsumerClass(config);
|
|
9347
9985
|
}
|
|
9348
9986
|
|
|
9987
|
+
class QueueError extends S3dbError {
|
|
9988
|
+
constructor(message, details = {}) {
|
|
9989
|
+
const { queueName, operation = "unknown", messageId, ...rest } = details;
|
|
9990
|
+
let description = details.description;
|
|
9991
|
+
if (!description) {
|
|
9992
|
+
description = `
|
|
9993
|
+
Queue Operation Error
|
|
9994
|
+
|
|
9995
|
+
Operation: ${operation}
|
|
9996
|
+
${queueName ? `Queue: ${queueName}` : ""}
|
|
9997
|
+
${messageId ? `Message ID: ${messageId}` : ""}
|
|
9998
|
+
|
|
9999
|
+
Common causes:
|
|
10000
|
+
1. Queue not properly configured
|
|
10001
|
+
2. Message handler not registered
|
|
10002
|
+
3. Queue resource not found
|
|
10003
|
+
4. SQS/RabbitMQ connection failed
|
|
10004
|
+
5. Message processing timeout
|
|
10005
|
+
|
|
10006
|
+
Solution:
|
|
10007
|
+
Check queue configuration and message handler registration.
|
|
10008
|
+
|
|
10009
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue.md
|
|
10010
|
+
`.trim();
|
|
10011
|
+
}
|
|
10012
|
+
super(message, { ...rest, queueName, operation, messageId, description });
|
|
10013
|
+
}
|
|
10014
|
+
}
|
|
10015
|
+
|
|
9349
10016
|
class QueueConsumerPlugin extends Plugin {
|
|
9350
10017
|
constructor(options = {}) {
|
|
9351
10018
|
super(options);
|
|
@@ -9406,13 +10073,32 @@ class QueueConsumerPlugin extends Plugin {
|
|
|
9406
10073
|
let action = body.action || msg.action;
|
|
9407
10074
|
let data = body.data || msg.data;
|
|
9408
10075
|
if (!resource) {
|
|
9409
|
-
throw new
|
|
10076
|
+
throw new QueueError("Resource not found in message", {
|
|
10077
|
+
operation: "handleMessage",
|
|
10078
|
+
queueName: configuredResource,
|
|
10079
|
+
messageBody: body,
|
|
10080
|
+
suggestion: 'Ensure message includes a "resource" field specifying the target resource name'
|
|
10081
|
+
});
|
|
9410
10082
|
}
|
|
9411
10083
|
if (!action) {
|
|
9412
|
-
throw new
|
|
10084
|
+
throw new QueueError("Action not found in message", {
|
|
10085
|
+
operation: "handleMessage",
|
|
10086
|
+
queueName: configuredResource,
|
|
10087
|
+
resource,
|
|
10088
|
+
messageBody: body,
|
|
10089
|
+
suggestion: 'Ensure message includes an "action" field (insert, update, or delete)'
|
|
10090
|
+
});
|
|
9413
10091
|
}
|
|
9414
10092
|
const resourceObj = this.database.resources[resource];
|
|
9415
|
-
if (!resourceObj)
|
|
10093
|
+
if (!resourceObj) {
|
|
10094
|
+
throw new QueueError(`Resource '${resource}' not found`, {
|
|
10095
|
+
operation: "handleMessage",
|
|
10096
|
+
queueName: configuredResource,
|
|
10097
|
+
resource,
|
|
10098
|
+
availableResources: Object.keys(this.database.resources),
|
|
10099
|
+
suggestion: "Check resource name or ensure resource is created before consuming messages"
|
|
10100
|
+
});
|
|
10101
|
+
}
|
|
9416
10102
|
let result;
|
|
9417
10103
|
const [ok, err, res] = await tryFn(async () => {
|
|
9418
10104
|
if (action === "insert") {
|
|
@@ -9423,7 +10109,14 @@ class QueueConsumerPlugin extends Plugin {
|
|
|
9423
10109
|
} else if (action === "delete") {
|
|
9424
10110
|
result = await resourceObj.delete(data.id);
|
|
9425
10111
|
} else {
|
|
9426
|
-
throw new
|
|
10112
|
+
throw new QueueError(`Unsupported action '${action}'`, {
|
|
10113
|
+
operation: "handleMessage",
|
|
10114
|
+
queueName: configuredResource,
|
|
10115
|
+
resource,
|
|
10116
|
+
action,
|
|
10117
|
+
supportedActions: ["insert", "update", "delete"],
|
|
10118
|
+
suggestion: "Use one of the supported actions: insert, update, or delete"
|
|
10119
|
+
});
|
|
9427
10120
|
}
|
|
9428
10121
|
return result;
|
|
9429
10122
|
});
|
|
@@ -9436,6 +10129,35 @@ class QueueConsumerPlugin extends Plugin {
|
|
|
9436
10129
|
}
|
|
9437
10130
|
}
|
|
9438
10131
|
|
|
10132
|
+
class ReplicationError extends S3dbError {
|
|
10133
|
+
constructor(message, details = {}) {
|
|
10134
|
+
const { replicatorClass = "unknown", operation = "unknown", resourceName, ...rest } = details;
|
|
10135
|
+
let description = details.description;
|
|
10136
|
+
if (!description) {
|
|
10137
|
+
description = `
|
|
10138
|
+
Replication Operation Error
|
|
10139
|
+
|
|
10140
|
+
Replicator: ${replicatorClass}
|
|
10141
|
+
Operation: ${operation}
|
|
10142
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
10143
|
+
|
|
10144
|
+
Common causes:
|
|
10145
|
+
1. Invalid replicator configuration
|
|
10146
|
+
2. Target system not accessible
|
|
10147
|
+
3. Resource not configured for replication
|
|
10148
|
+
4. Invalid operation type
|
|
10149
|
+
5. Transformation function errors
|
|
10150
|
+
|
|
10151
|
+
Solution:
|
|
10152
|
+
Check replicator configuration and ensure target system is accessible.
|
|
10153
|
+
|
|
10154
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md
|
|
10155
|
+
`.trim();
|
|
10156
|
+
}
|
|
10157
|
+
super(message, { ...rest, replicatorClass, operation, resourceName, description });
|
|
10158
|
+
}
|
|
10159
|
+
}
|
|
10160
|
+
|
|
9439
10161
|
class BaseReplicator extends EventEmitter {
|
|
9440
10162
|
constructor(config = {}) {
|
|
9441
10163
|
super();
|
|
@@ -9461,7 +10183,12 @@ class BaseReplicator extends EventEmitter {
|
|
|
9461
10183
|
* @returns {Promise<Object>} replicator result
|
|
9462
10184
|
*/
|
|
9463
10185
|
async replicate(resourceName, operation, data, id) {
|
|
9464
|
-
throw new
|
|
10186
|
+
throw new ReplicationError("replicate() method must be implemented by subclass", {
|
|
10187
|
+
operation: "replicate",
|
|
10188
|
+
replicatorClass: this.name,
|
|
10189
|
+
resourceName,
|
|
10190
|
+
suggestion: "Extend BaseReplicator and implement the replicate() method"
|
|
10191
|
+
});
|
|
9465
10192
|
}
|
|
9466
10193
|
/**
|
|
9467
10194
|
* Replicate multiple records in batch
|
|
@@ -9470,14 +10197,24 @@ class BaseReplicator extends EventEmitter {
|
|
|
9470
10197
|
* @returns {Promise<Object>} Batch replicator result
|
|
9471
10198
|
*/
|
|
9472
10199
|
async replicateBatch(resourceName, records) {
|
|
9473
|
-
throw new
|
|
10200
|
+
throw new ReplicationError("replicateBatch() method must be implemented by subclass", {
|
|
10201
|
+
operation: "replicateBatch",
|
|
10202
|
+
replicatorClass: this.name,
|
|
10203
|
+
resourceName,
|
|
10204
|
+
batchSize: records?.length,
|
|
10205
|
+
suggestion: "Extend BaseReplicator and implement the replicateBatch() method"
|
|
10206
|
+
});
|
|
9474
10207
|
}
|
|
9475
10208
|
/**
|
|
9476
10209
|
* Test the connection to the target
|
|
9477
10210
|
* @returns {Promise<boolean>} True if connection is successful
|
|
9478
10211
|
*/
|
|
9479
10212
|
async testConnection() {
|
|
9480
|
-
throw new
|
|
10213
|
+
throw new ReplicationError("testConnection() method must be implemented by subclass", {
|
|
10214
|
+
operation: "testConnection",
|
|
10215
|
+
replicatorClass: this.name,
|
|
10216
|
+
suggestion: "Extend BaseReplicator and implement the testConnection() method"
|
|
10217
|
+
});
|
|
9481
10218
|
}
|
|
9482
10219
|
/**
|
|
9483
10220
|
* Get replicator status and statistics
|
|
@@ -10649,7 +11386,17 @@ class Client extends EventEmitter {
|
|
|
10649
11386
|
});
|
|
10650
11387
|
this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
10651
11388
|
if (errors.length > 0) {
|
|
10652
|
-
throw new
|
|
11389
|
+
throw new UnknownError("Some objects could not be moved", {
|
|
11390
|
+
bucket: this.config.bucket,
|
|
11391
|
+
operation: "moveAllObjects",
|
|
11392
|
+
prefixFrom,
|
|
11393
|
+
prefixTo,
|
|
11394
|
+
totalKeys: keys.length,
|
|
11395
|
+
failedCount: errors.length,
|
|
11396
|
+
successCount: results.length,
|
|
11397
|
+
errors: errors.map((e) => ({ message: e.message, raw: e.raw })),
|
|
11398
|
+
suggestion: "Check S3 permissions and retry failed objects individually"
|
|
11399
|
+
});
|
|
10653
11400
|
}
|
|
10654
11401
|
return results;
|
|
10655
11402
|
}
|
|
@@ -11363,7 +12110,14 @@ async function handleInsert$4({ resource, data, mappedData, originalData }) {
|
|
|
11363
12110
|
}
|
|
11364
12111
|
});
|
|
11365
12112
|
if (totalSize > effectiveLimit) {
|
|
11366
|
-
throw new
|
|
12113
|
+
throw new MetadataLimitError("Metadata size exceeds 2KB limit on insert", {
|
|
12114
|
+
totalSize,
|
|
12115
|
+
effectiveLimit,
|
|
12116
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
12117
|
+
excess: totalSize - effectiveLimit,
|
|
12118
|
+
resourceName: resource.name,
|
|
12119
|
+
operation: "insert"
|
|
12120
|
+
});
|
|
11367
12121
|
}
|
|
11368
12122
|
return { mappedData, body: "" };
|
|
11369
12123
|
}
|
|
@@ -11378,7 +12132,15 @@ async function handleUpdate$4({ resource, id, data, mappedData, originalData })
|
|
|
11378
12132
|
}
|
|
11379
12133
|
});
|
|
11380
12134
|
if (totalSize > effectiveLimit) {
|
|
11381
|
-
throw new
|
|
12135
|
+
throw new MetadataLimitError("Metadata size exceeds 2KB limit on update", {
|
|
12136
|
+
totalSize,
|
|
12137
|
+
effectiveLimit,
|
|
12138
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
12139
|
+
excess: totalSize - effectiveLimit,
|
|
12140
|
+
resourceName: resource.name,
|
|
12141
|
+
operation: "update",
|
|
12142
|
+
id
|
|
12143
|
+
});
|
|
11382
12144
|
}
|
|
11383
12145
|
return { mappedData, body: JSON.stringify(mappedData) };
|
|
11384
12146
|
}
|
|
@@ -11393,7 +12155,15 @@ async function handleUpsert$4({ resource, id, data, mappedData }) {
|
|
|
11393
12155
|
}
|
|
11394
12156
|
});
|
|
11395
12157
|
if (totalSize > effectiveLimit) {
|
|
11396
|
-
throw new
|
|
12158
|
+
throw new MetadataLimitError("Metadata size exceeds 2KB limit on upsert", {
|
|
12159
|
+
totalSize,
|
|
12160
|
+
effectiveLimit,
|
|
12161
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
12162
|
+
excess: totalSize - effectiveLimit,
|
|
12163
|
+
resourceName: resource.name,
|
|
12164
|
+
operation: "upsert",
|
|
12165
|
+
id
|
|
12166
|
+
});
|
|
11397
12167
|
}
|
|
11398
12168
|
return { mappedData, body: "" };
|
|
11399
12169
|
}
|
|
@@ -11735,7 +12505,11 @@ const behaviors = {
|
|
|
11735
12505
|
function getBehavior(behaviorName) {
|
|
11736
12506
|
const behavior = behaviors[behaviorName];
|
|
11737
12507
|
if (!behavior) {
|
|
11738
|
-
throw new
|
|
12508
|
+
throw new BehaviorError(`Unknown behavior: ${behaviorName}`, {
|
|
12509
|
+
behavior: behaviorName,
|
|
12510
|
+
availableBehaviors: Object.keys(behaviors),
|
|
12511
|
+
operation: "getBehavior"
|
|
12512
|
+
});
|
|
11739
12513
|
}
|
|
11740
12514
|
return behavior;
|
|
11741
12515
|
}
|
|
@@ -14259,7 +15033,7 @@ class Database extends EventEmitter {
|
|
|
14259
15033
|
this.id = idGenerator(7);
|
|
14260
15034
|
this.version = "1";
|
|
14261
15035
|
this.s3dbVersion = (() => {
|
|
14262
|
-
const [ok, err, version] = tryFn(() => true ? "11.2.
|
|
15036
|
+
const [ok, err, version] = tryFn(() => true ? "11.2.4" : "latest");
|
|
14263
15037
|
return ok ? version : "latest";
|
|
14264
15038
|
})();
|
|
14265
15039
|
this.resources = {};
|
|
@@ -14604,7 +15378,12 @@ class Database extends EventEmitter {
|
|
|
14604
15378
|
const pluginName = name.toLowerCase().replace("plugin", "");
|
|
14605
15379
|
const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
|
|
14606
15380
|
if (!plugin) {
|
|
14607
|
-
throw new
|
|
15381
|
+
throw new DatabaseError(`Plugin '${name}' not found`, {
|
|
15382
|
+
operation: "uninstallPlugin",
|
|
15383
|
+
pluginName: name,
|
|
15384
|
+
availablePlugins: Object.keys(this.pluginRegistry),
|
|
15385
|
+
suggestion: "Check plugin name or list available plugins using Object.keys(db.pluginRegistry)"
|
|
15386
|
+
});
|
|
14608
15387
|
}
|
|
14609
15388
|
if (plugin.stop) {
|
|
14610
15389
|
await plugin.stop();
|
|
@@ -15237,10 +16016,20 @@ class Database extends EventEmitter {
|
|
|
15237
16016
|
addHook(event, fn) {
|
|
15238
16017
|
if (!this._hooks) this._initHooks();
|
|
15239
16018
|
if (!this._hooks.has(event)) {
|
|
15240
|
-
throw new
|
|
16019
|
+
throw new DatabaseError(`Unknown hook event: ${event}`, {
|
|
16020
|
+
operation: "addHook",
|
|
16021
|
+
invalidEvent: event,
|
|
16022
|
+
availableEvents: this._hookEvents,
|
|
16023
|
+
suggestion: `Use one of the available hook events: ${this._hookEvents.join(", ")}`
|
|
16024
|
+
});
|
|
15241
16025
|
}
|
|
15242
16026
|
if (typeof fn !== "function") {
|
|
15243
|
-
throw new
|
|
16027
|
+
throw new DatabaseError("Hook function must be a function", {
|
|
16028
|
+
operation: "addHook",
|
|
16029
|
+
event,
|
|
16030
|
+
receivedType: typeof fn,
|
|
16031
|
+
suggestion: "Provide a function that will be called when the hook event occurs"
|
|
16032
|
+
});
|
|
15244
16033
|
}
|
|
15245
16034
|
this._hooks.get(event).push(fn);
|
|
15246
16035
|
}
|
|
@@ -15378,7 +16167,11 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
15378
16167
|
this.targetDatabase = new S3db(targetConfig);
|
|
15379
16168
|
await this.targetDatabase.connect();
|
|
15380
16169
|
} else {
|
|
15381
|
-
throw new
|
|
16170
|
+
throw new ReplicationError("S3dbReplicator requires client or connectionString", {
|
|
16171
|
+
operation: "initialize",
|
|
16172
|
+
replicatorClass: "S3dbReplicator",
|
|
16173
|
+
suggestion: 'Provide either a client instance or connectionString in config: { client: db } or { connectionString: "s3://..." }'
|
|
16174
|
+
});
|
|
15382
16175
|
}
|
|
15383
16176
|
this.emit("connected", {
|
|
15384
16177
|
replicator: this.name,
|
|
@@ -15409,7 +16202,13 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
15409
16202
|
const normResource = normalizeResourceName$1(resource);
|
|
15410
16203
|
const entry = this.resourcesMap[normResource];
|
|
15411
16204
|
if (!entry) {
|
|
15412
|
-
throw new
|
|
16205
|
+
throw new ReplicationError("Resource not configured for replication", {
|
|
16206
|
+
operation: "replicate",
|
|
16207
|
+
replicatorClass: "S3dbReplicator",
|
|
16208
|
+
resourceName: resource,
|
|
16209
|
+
configuredResources: Object.keys(this.resourcesMap),
|
|
16210
|
+
suggestion: 'Add resource to replicator resources map: { resources: { [resourceName]: "destination" } }'
|
|
16211
|
+
});
|
|
15413
16212
|
}
|
|
15414
16213
|
if (Array.isArray(entry)) {
|
|
15415
16214
|
const results = [];
|
|
@@ -15477,7 +16276,14 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
15477
16276
|
} else if (operation === "delete") {
|
|
15478
16277
|
result = await destResourceObj.delete(recordId);
|
|
15479
16278
|
} else {
|
|
15480
|
-
throw new
|
|
16279
|
+
throw new ReplicationError(`Invalid replication operation: ${operation}`, {
|
|
16280
|
+
operation: "replicate",
|
|
16281
|
+
replicatorClass: "S3dbReplicator",
|
|
16282
|
+
invalidOperation: operation,
|
|
16283
|
+
supportedOperations: ["insert", "update", "delete"],
|
|
16284
|
+
resourceName: sourceResource,
|
|
16285
|
+
suggestion: "Use one of the supported operations: insert, update, delete"
|
|
16286
|
+
});
|
|
15481
16287
|
}
|
|
15482
16288
|
return result;
|
|
15483
16289
|
}
|
|
@@ -15545,7 +16351,13 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
15545
16351
|
const norm = normalizeResourceName$1(resource);
|
|
15546
16352
|
const found = available.find((r) => normalizeResourceName$1(r) === norm);
|
|
15547
16353
|
if (!found) {
|
|
15548
|
-
throw new
|
|
16354
|
+
throw new ReplicationError("Destination resource not found in target database", {
|
|
16355
|
+
operation: "_getDestResourceObj",
|
|
16356
|
+
replicatorClass: "S3dbReplicator",
|
|
16357
|
+
destinationResource: resource,
|
|
16358
|
+
availableResources: available,
|
|
16359
|
+
suggestion: "Create the resource in target database or check resource name spelling"
|
|
16360
|
+
});
|
|
15549
16361
|
}
|
|
15550
16362
|
return db.resources[found];
|
|
15551
16363
|
}
|
|
@@ -15594,7 +16406,13 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
15594
16406
|
}
|
|
15595
16407
|
async testConnection() {
|
|
15596
16408
|
const [ok, err] = await tryFn(async () => {
|
|
15597
|
-
if (!this.targetDatabase)
|
|
16409
|
+
if (!this.targetDatabase) {
|
|
16410
|
+
throw new ReplicationError("No target database configured for connection test", {
|
|
16411
|
+
operation: "testConnection",
|
|
16412
|
+
replicatorClass: "S3dbReplicator",
|
|
16413
|
+
suggestion: "Initialize replicator with client or connectionString before testing connection"
|
|
16414
|
+
});
|
|
16415
|
+
}
|
|
15598
16416
|
if (typeof this.targetDatabase.connect === "function") {
|
|
15599
16417
|
await this.targetDatabase.connect();
|
|
15600
16418
|
}
|
|
@@ -15981,7 +16799,12 @@ const REPLICATOR_DRIVERS = {
|
|
|
15981
16799
|
function createReplicator(driver, config = {}, resources = [], client = null) {
|
|
15982
16800
|
const ReplicatorClass = REPLICATOR_DRIVERS[driver];
|
|
15983
16801
|
if (!ReplicatorClass) {
|
|
15984
|
-
throw new
|
|
16802
|
+
throw new ReplicationError(`Unknown replicator driver: ${driver}`, {
|
|
16803
|
+
operation: "createReplicator",
|
|
16804
|
+
driver,
|
|
16805
|
+
availableDrivers: Object.keys(REPLICATOR_DRIVERS),
|
|
16806
|
+
suggestion: `Use one of the available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`
|
|
16807
|
+
});
|
|
15985
16808
|
}
|
|
15986
16809
|
return new ReplicatorClass(config, resources, client);
|
|
15987
16810
|
}
|
|
@@ -15993,12 +16816,40 @@ class ReplicatorPlugin extends Plugin {
|
|
|
15993
16816
|
constructor(options = {}) {
|
|
15994
16817
|
super();
|
|
15995
16818
|
if (!options.replicators || !Array.isArray(options.replicators)) {
|
|
15996
|
-
throw new
|
|
16819
|
+
throw new ReplicationError("ReplicatorPlugin requires replicators array", {
|
|
16820
|
+
operation: "constructor",
|
|
16821
|
+
pluginName: "ReplicatorPlugin",
|
|
16822
|
+
providedOptions: Object.keys(options),
|
|
16823
|
+
suggestion: 'Provide replicators array: new ReplicatorPlugin({ replicators: [{ driver: "s3db", resources: [...] }] })'
|
|
16824
|
+
});
|
|
15997
16825
|
}
|
|
15998
16826
|
for (const rep of options.replicators) {
|
|
15999
|
-
if (!rep.driver)
|
|
16000
|
-
|
|
16001
|
-
|
|
16827
|
+
if (!rep.driver) {
|
|
16828
|
+
throw new ReplicationError("Each replicator must have a driver", {
|
|
16829
|
+
operation: "constructor",
|
|
16830
|
+
pluginName: "ReplicatorPlugin",
|
|
16831
|
+
replicatorConfig: rep,
|
|
16832
|
+
suggestion: 'Each replicator entry must specify a driver: { driver: "s3db", resources: {...} }'
|
|
16833
|
+
});
|
|
16834
|
+
}
|
|
16835
|
+
if (!rep.resources || typeof rep.resources !== "object") {
|
|
16836
|
+
throw new ReplicationError("Each replicator must have resources config", {
|
|
16837
|
+
operation: "constructor",
|
|
16838
|
+
pluginName: "ReplicatorPlugin",
|
|
16839
|
+
driver: rep.driver,
|
|
16840
|
+
replicatorConfig: rep,
|
|
16841
|
+
suggestion: 'Provide resources as object or array: { driver: "s3db", resources: ["users"] } or { resources: { users: "people" } }'
|
|
16842
|
+
});
|
|
16843
|
+
}
|
|
16844
|
+
if (Object.keys(rep.resources).length === 0) {
|
|
16845
|
+
throw new ReplicationError("Each replicator must have at least one resource configured", {
|
|
16846
|
+
operation: "constructor",
|
|
16847
|
+
pluginName: "ReplicatorPlugin",
|
|
16848
|
+
driver: rep.driver,
|
|
16849
|
+
replicatorConfig: rep,
|
|
16850
|
+
suggestion: 'Add at least one resource to replicate: { driver: "s3db", resources: ["users"] }'
|
|
16851
|
+
});
|
|
16852
|
+
}
|
|
16002
16853
|
}
|
|
16003
16854
|
this.config = {
|
|
16004
16855
|
replicators: options.replicators || [],
|
|
@@ -16424,7 +17275,13 @@ class ReplicatorPlugin extends Plugin {
|
|
|
16424
17275
|
async syncAllData(replicatorId) {
|
|
16425
17276
|
const replicator = this.replicators.find((r) => r.id === replicatorId);
|
|
16426
17277
|
if (!replicator) {
|
|
16427
|
-
throw new
|
|
17278
|
+
throw new ReplicationError("Replicator not found", {
|
|
17279
|
+
operation: "syncAllData",
|
|
17280
|
+
pluginName: "ReplicatorPlugin",
|
|
17281
|
+
replicatorId,
|
|
17282
|
+
availableReplicators: this.replicators.map((r) => r.id),
|
|
17283
|
+
suggestion: "Check replicator ID or use getReplicatorStats() to list available replicators"
|
|
17284
|
+
});
|
|
16428
17285
|
}
|
|
16429
17286
|
this.stats.lastSync = (/* @__PURE__ */ new Date()).toISOString();
|
|
16430
17287
|
for (const resourceName in this.database.resources) {
|
|
@@ -16954,6 +17811,35 @@ class S3QueuePlugin extends Plugin {
|
|
|
16954
17811
|
}
|
|
16955
17812
|
}
|
|
16956
17813
|
|
|
17814
|
+
class SchedulerError extends S3dbError {
|
|
17815
|
+
constructor(message, details = {}) {
|
|
17816
|
+
const { taskId, operation = "unknown", cronExpression, ...rest } = details;
|
|
17817
|
+
let description = details.description;
|
|
17818
|
+
if (!description) {
|
|
17819
|
+
description = `
|
|
17820
|
+
Scheduler Operation Error
|
|
17821
|
+
|
|
17822
|
+
Operation: ${operation}
|
|
17823
|
+
${taskId ? `Task ID: ${taskId}` : ""}
|
|
17824
|
+
${cronExpression ? `Cron: ${cronExpression}` : ""}
|
|
17825
|
+
|
|
17826
|
+
Common causes:
|
|
17827
|
+
1. Invalid cron expression format
|
|
17828
|
+
2. Task not found or already exists
|
|
17829
|
+
3. Scheduler not properly initialized
|
|
17830
|
+
4. Job execution failure
|
|
17831
|
+
5. Resource conflicts
|
|
17832
|
+
|
|
17833
|
+
Solution:
|
|
17834
|
+
Check task configuration and ensure scheduler is properly initialized.
|
|
17835
|
+
|
|
17836
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/scheduler.md
|
|
17837
|
+
`.trim();
|
|
17838
|
+
}
|
|
17839
|
+
super(message, { ...rest, taskId, operation, cronExpression, description });
|
|
17840
|
+
}
|
|
17841
|
+
}
|
|
17842
|
+
|
|
16957
17843
|
class SchedulerPlugin extends Plugin {
|
|
16958
17844
|
constructor(options = {}) {
|
|
16959
17845
|
super();
|
|
@@ -16987,17 +17873,36 @@ class SchedulerPlugin extends Plugin {
|
|
|
16987
17873
|
}
|
|
16988
17874
|
_validateConfiguration() {
|
|
16989
17875
|
if (Object.keys(this.config.jobs).length === 0) {
|
|
16990
|
-
throw new
|
|
17876
|
+
throw new SchedulerError("At least one job must be defined", {
|
|
17877
|
+
operation: "validateConfiguration",
|
|
17878
|
+
jobCount: 0,
|
|
17879
|
+
suggestion: 'Provide at least one job in the jobs configuration: { jobs: { myJob: { schedule: "* * * * *", action: async () => {...} } } }'
|
|
17880
|
+
});
|
|
16991
17881
|
}
|
|
16992
17882
|
for (const [jobName, job] of Object.entries(this.config.jobs)) {
|
|
16993
17883
|
if (!job.schedule) {
|
|
16994
|
-
throw new
|
|
17884
|
+
throw new SchedulerError(`Job '${jobName}' must have a schedule`, {
|
|
17885
|
+
operation: "validateConfiguration",
|
|
17886
|
+
taskId: jobName,
|
|
17887
|
+
providedConfig: Object.keys(job),
|
|
17888
|
+
suggestion: 'Add a schedule property with a valid cron expression: { schedule: "0 * * * *", action: async () => {...} }'
|
|
17889
|
+
});
|
|
16995
17890
|
}
|
|
16996
17891
|
if (!job.action || typeof job.action !== "function") {
|
|
16997
|
-
throw new
|
|
17892
|
+
throw new SchedulerError(`Job '${jobName}' must have an action function`, {
|
|
17893
|
+
operation: "validateConfiguration",
|
|
17894
|
+
taskId: jobName,
|
|
17895
|
+
actionType: typeof job.action,
|
|
17896
|
+
suggestion: 'Provide an action function: { schedule: "...", action: async (db, ctx) => {...} }'
|
|
17897
|
+
});
|
|
16998
17898
|
}
|
|
16999
17899
|
if (!this._isValidCronExpression(job.schedule)) {
|
|
17000
|
-
throw new
|
|
17900
|
+
throw new SchedulerError(`Job '${jobName}' has invalid cron expression`, {
|
|
17901
|
+
operation: "validateConfiguration",
|
|
17902
|
+
taskId: jobName,
|
|
17903
|
+
cronExpression: job.schedule,
|
|
17904
|
+
suggestion: "Use valid cron format (5 fields: minute hour day month weekday) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
|
|
17905
|
+
});
|
|
17001
17906
|
}
|
|
17002
17907
|
}
|
|
17003
17908
|
}
|
|
@@ -17295,10 +18200,20 @@ class SchedulerPlugin extends Plugin {
|
|
|
17295
18200
|
async runJob(jobName, context = {}) {
|
|
17296
18201
|
const job = this.jobs.get(jobName);
|
|
17297
18202
|
if (!job) {
|
|
17298
|
-
throw new
|
|
18203
|
+
throw new SchedulerError(`Job '${jobName}' not found`, {
|
|
18204
|
+
operation: "runJob",
|
|
18205
|
+
taskId: jobName,
|
|
18206
|
+
availableJobs: Array.from(this.jobs.keys()),
|
|
18207
|
+
suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
|
|
18208
|
+
});
|
|
17299
18209
|
}
|
|
17300
18210
|
if (this.activeJobs.has(jobName)) {
|
|
17301
|
-
throw new
|
|
18211
|
+
throw new SchedulerError(`Job '${jobName}' is already running`, {
|
|
18212
|
+
operation: "runJob",
|
|
18213
|
+
taskId: jobName,
|
|
18214
|
+
executionId: this.activeJobs.get(jobName),
|
|
18215
|
+
suggestion: "Wait for current execution to complete or check job status with getJobStatus()"
|
|
18216
|
+
});
|
|
17302
18217
|
}
|
|
17303
18218
|
await this._executeJob(jobName);
|
|
17304
18219
|
}
|
|
@@ -17308,7 +18223,12 @@ class SchedulerPlugin extends Plugin {
|
|
|
17308
18223
|
enableJob(jobName) {
|
|
17309
18224
|
const job = this.jobs.get(jobName);
|
|
17310
18225
|
if (!job) {
|
|
17311
|
-
throw new
|
|
18226
|
+
throw new SchedulerError(`Job '${jobName}' not found`, {
|
|
18227
|
+
operation: "enableJob",
|
|
18228
|
+
taskId: jobName,
|
|
18229
|
+
availableJobs: Array.from(this.jobs.keys()),
|
|
18230
|
+
suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
|
|
18231
|
+
});
|
|
17312
18232
|
}
|
|
17313
18233
|
job.enabled = true;
|
|
17314
18234
|
this._scheduleNextExecution(jobName);
|
|
@@ -17320,7 +18240,12 @@ class SchedulerPlugin extends Plugin {
|
|
|
17320
18240
|
disableJob(jobName) {
|
|
17321
18241
|
const job = this.jobs.get(jobName);
|
|
17322
18242
|
if (!job) {
|
|
17323
|
-
throw new
|
|
18243
|
+
throw new SchedulerError(`Job '${jobName}' not found`, {
|
|
18244
|
+
operation: "disableJob",
|
|
18245
|
+
taskId: jobName,
|
|
18246
|
+
availableJobs: Array.from(this.jobs.keys()),
|
|
18247
|
+
suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
|
|
18248
|
+
});
|
|
17324
18249
|
}
|
|
17325
18250
|
job.enabled = false;
|
|
17326
18251
|
const timer = this.timers.get(jobName);
|
|
@@ -17419,13 +18344,28 @@ class SchedulerPlugin extends Plugin {
|
|
|
17419
18344
|
*/
|
|
17420
18345
|
addJob(jobName, jobConfig) {
|
|
17421
18346
|
if (this.jobs.has(jobName)) {
|
|
17422
|
-
throw new
|
|
18347
|
+
throw new SchedulerError(`Job '${jobName}' already exists`, {
|
|
18348
|
+
operation: "addJob",
|
|
18349
|
+
taskId: jobName,
|
|
18350
|
+
existingJobs: Array.from(this.jobs.keys()),
|
|
18351
|
+
suggestion: "Use a different job name or remove the existing job first with removeJob()"
|
|
18352
|
+
});
|
|
17423
18353
|
}
|
|
17424
18354
|
if (!jobConfig.schedule || !jobConfig.action) {
|
|
17425
|
-
throw new
|
|
18355
|
+
throw new SchedulerError("Job must have schedule and action", {
|
|
18356
|
+
operation: "addJob",
|
|
18357
|
+
taskId: jobName,
|
|
18358
|
+
providedConfig: Object.keys(jobConfig),
|
|
18359
|
+
suggestion: 'Provide both schedule and action: { schedule: "0 * * * *", action: async (db, ctx) => {...} }'
|
|
18360
|
+
});
|
|
17426
18361
|
}
|
|
17427
18362
|
if (!this._isValidCronExpression(jobConfig.schedule)) {
|
|
17428
|
-
throw new
|
|
18363
|
+
throw new SchedulerError("Invalid cron expression", {
|
|
18364
|
+
operation: "addJob",
|
|
18365
|
+
taskId: jobName,
|
|
18366
|
+
cronExpression: jobConfig.schedule,
|
|
18367
|
+
suggestion: "Use valid cron format (5 fields) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
|
|
18368
|
+
});
|
|
17429
18369
|
}
|
|
17430
18370
|
const job = {
|
|
17431
18371
|
...jobConfig,
|
|
@@ -17459,7 +18399,12 @@ class SchedulerPlugin extends Plugin {
|
|
|
17459
18399
|
removeJob(jobName) {
|
|
17460
18400
|
const job = this.jobs.get(jobName);
|
|
17461
18401
|
if (!job) {
|
|
17462
|
-
throw new
|
|
18402
|
+
throw new SchedulerError(`Job '${jobName}' not found`, {
|
|
18403
|
+
operation: "removeJob",
|
|
18404
|
+
taskId: jobName,
|
|
18405
|
+
availableJobs: Array.from(this.jobs.keys()),
|
|
18406
|
+
suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
|
|
18407
|
+
});
|
|
17463
18408
|
}
|
|
17464
18409
|
const timer = this.timers.get(jobName);
|
|
17465
18410
|
if (timer) {
|
|
@@ -17513,6 +18458,36 @@ class SchedulerPlugin extends Plugin {
|
|
|
17513
18458
|
}
|
|
17514
18459
|
}
|
|
17515
18460
|
|
|
18461
|
+
class StateMachineError extends S3dbError {
|
|
18462
|
+
constructor(message, details = {}) {
|
|
18463
|
+
const { currentState, targetState, resourceName, operation = "unknown", ...rest } = details;
|
|
18464
|
+
let description = details.description;
|
|
18465
|
+
if (!description) {
|
|
18466
|
+
description = `
|
|
18467
|
+
State Machine Operation Error
|
|
18468
|
+
|
|
18469
|
+
Operation: ${operation}
|
|
18470
|
+
${currentState ? `Current State: ${currentState}` : ""}
|
|
18471
|
+
${targetState ? `Target State: ${targetState}` : ""}
|
|
18472
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
18473
|
+
|
|
18474
|
+
Common causes:
|
|
18475
|
+
1. Invalid state transition
|
|
18476
|
+
2. State machine not configured
|
|
18477
|
+
3. Transition conditions not met
|
|
18478
|
+
4. State not defined in configuration
|
|
18479
|
+
5. Missing transition handler
|
|
18480
|
+
|
|
18481
|
+
Solution:
|
|
18482
|
+
Check state machine configuration and valid transitions.
|
|
18483
|
+
|
|
18484
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-machine.md
|
|
18485
|
+
`.trim();
|
|
18486
|
+
}
|
|
18487
|
+
super(message, { ...rest, currentState, targetState, resourceName, operation, description });
|
|
18488
|
+
}
|
|
18489
|
+
}
|
|
18490
|
+
|
|
17516
18491
|
class StateMachinePlugin extends Plugin {
|
|
17517
18492
|
constructor(options = {}) {
|
|
17518
18493
|
super();
|
|
@@ -17533,17 +18508,36 @@ class StateMachinePlugin extends Plugin {
|
|
|
17533
18508
|
}
|
|
17534
18509
|
_validateConfiguration() {
|
|
17535
18510
|
if (!this.config.stateMachines || Object.keys(this.config.stateMachines).length === 0) {
|
|
17536
|
-
throw new
|
|
18511
|
+
throw new StateMachineError("At least one state machine must be defined", {
|
|
18512
|
+
operation: "validateConfiguration",
|
|
18513
|
+
machineCount: 0,
|
|
18514
|
+
suggestion: "Provide at least one state machine in the stateMachines configuration"
|
|
18515
|
+
});
|
|
17537
18516
|
}
|
|
17538
18517
|
for (const [machineName, machine] of Object.entries(this.config.stateMachines)) {
|
|
17539
18518
|
if (!machine.states || Object.keys(machine.states).length === 0) {
|
|
17540
|
-
throw new
|
|
18519
|
+
throw new StateMachineError(`Machine '${machineName}' must have states defined`, {
|
|
18520
|
+
operation: "validateConfiguration",
|
|
18521
|
+
machineId: machineName,
|
|
18522
|
+
suggestion: "Define at least one state in the states configuration"
|
|
18523
|
+
});
|
|
17541
18524
|
}
|
|
17542
18525
|
if (!machine.initialState) {
|
|
17543
|
-
throw new
|
|
18526
|
+
throw new StateMachineError(`Machine '${machineName}' must have an initialState`, {
|
|
18527
|
+
operation: "validateConfiguration",
|
|
18528
|
+
machineId: machineName,
|
|
18529
|
+
availableStates: Object.keys(machine.states),
|
|
18530
|
+
suggestion: "Specify an initialState property matching one of the defined states"
|
|
18531
|
+
});
|
|
17544
18532
|
}
|
|
17545
18533
|
if (!machine.states[machine.initialState]) {
|
|
17546
|
-
throw new
|
|
18534
|
+
throw new StateMachineError(`Initial state '${machine.initialState}' not found in machine '${machineName}'`, {
|
|
18535
|
+
operation: "validateConfiguration",
|
|
18536
|
+
machineId: machineName,
|
|
18537
|
+
initialState: machine.initialState,
|
|
18538
|
+
availableStates: Object.keys(machine.states),
|
|
18539
|
+
suggestion: "Set initialState to one of the defined states"
|
|
18540
|
+
});
|
|
17547
18541
|
}
|
|
17548
18542
|
}
|
|
17549
18543
|
}
|
|
@@ -17600,12 +18594,25 @@ class StateMachinePlugin extends Plugin {
|
|
|
17600
18594
|
async send(machineId, entityId, event, context = {}) {
|
|
17601
18595
|
const machine = this.machines.get(machineId);
|
|
17602
18596
|
if (!machine) {
|
|
17603
|
-
throw new
|
|
18597
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18598
|
+
operation: "send",
|
|
18599
|
+
machineId,
|
|
18600
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18601
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18602
|
+
});
|
|
17604
18603
|
}
|
|
17605
18604
|
const currentState = await this.getState(machineId, entityId);
|
|
17606
18605
|
const stateConfig = machine.config.states[currentState];
|
|
17607
18606
|
if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
|
|
17608
|
-
throw new
|
|
18607
|
+
throw new StateMachineError(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`, {
|
|
18608
|
+
operation: "send",
|
|
18609
|
+
machineId,
|
|
18610
|
+
entityId,
|
|
18611
|
+
event,
|
|
18612
|
+
currentState,
|
|
18613
|
+
validEvents: stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [],
|
|
18614
|
+
suggestion: "Use getValidEvents() to check which events are valid for the current state"
|
|
18615
|
+
});
|
|
17609
18616
|
}
|
|
17610
18617
|
const targetState = stateConfig.on[event];
|
|
17611
18618
|
if (stateConfig.guards && stateConfig.guards[event]) {
|
|
@@ -17616,7 +18623,16 @@ class StateMachinePlugin extends Plugin {
|
|
|
17616
18623
|
() => guard(context, event, { database: this.database, machineId, entityId })
|
|
17617
18624
|
);
|
|
17618
18625
|
if (!guardOk || !guardResult) {
|
|
17619
|
-
throw new
|
|
18626
|
+
throw new StateMachineError(`Transition blocked by guard '${guardName}'`, {
|
|
18627
|
+
operation: "send",
|
|
18628
|
+
machineId,
|
|
18629
|
+
entityId,
|
|
18630
|
+
event,
|
|
18631
|
+
currentState,
|
|
18632
|
+
guardName,
|
|
18633
|
+
guardError: guardErr?.message || "Guard returned false",
|
|
18634
|
+
suggestion: "Check guard conditions or modify the context to satisfy guard requirements"
|
|
18635
|
+
});
|
|
17620
18636
|
}
|
|
17621
18637
|
}
|
|
17622
18638
|
}
|
|
@@ -17726,7 +18742,12 @@ class StateMachinePlugin extends Plugin {
|
|
|
17726
18742
|
async getState(machineId, entityId) {
|
|
17727
18743
|
const machine = this.machines.get(machineId);
|
|
17728
18744
|
if (!machine) {
|
|
17729
|
-
throw new
|
|
18745
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18746
|
+
operation: "getState",
|
|
18747
|
+
machineId,
|
|
18748
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18749
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18750
|
+
});
|
|
17730
18751
|
}
|
|
17731
18752
|
if (machine.currentStates.has(entityId)) {
|
|
17732
18753
|
return machine.currentStates.get(entityId);
|
|
@@ -17752,7 +18773,12 @@ class StateMachinePlugin extends Plugin {
|
|
|
17752
18773
|
async getValidEvents(machineId, stateOrEntityId) {
|
|
17753
18774
|
const machine = this.machines.get(machineId);
|
|
17754
18775
|
if (!machine) {
|
|
17755
|
-
throw new
|
|
18776
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18777
|
+
operation: "getValidEvents",
|
|
18778
|
+
machineId,
|
|
18779
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18780
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18781
|
+
});
|
|
17756
18782
|
}
|
|
17757
18783
|
let state;
|
|
17758
18784
|
if (machine.config.states[stateOrEntityId]) {
|
|
@@ -17801,7 +18827,12 @@ class StateMachinePlugin extends Plugin {
|
|
|
17801
18827
|
async initializeEntity(machineId, entityId, context = {}) {
|
|
17802
18828
|
const machine = this.machines.get(machineId);
|
|
17803
18829
|
if (!machine) {
|
|
17804
|
-
throw new
|
|
18830
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18831
|
+
operation: "initializeEntity",
|
|
18832
|
+
machineId,
|
|
18833
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18834
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18835
|
+
});
|
|
17805
18836
|
}
|
|
17806
18837
|
const initialState = machine.config.initialState;
|
|
17807
18838
|
machine.currentStates.set(entityId, initialState);
|
|
@@ -17820,7 +18851,14 @@ class StateMachinePlugin extends Plugin {
|
|
|
17820
18851
|
})
|
|
17821
18852
|
);
|
|
17822
18853
|
if (!ok && err && !err.message?.includes("already exists")) {
|
|
17823
|
-
throw new
|
|
18854
|
+
throw new StateMachineError("Failed to initialize entity state", {
|
|
18855
|
+
operation: "initializeEntity",
|
|
18856
|
+
machineId,
|
|
18857
|
+
entityId,
|
|
18858
|
+
initialState,
|
|
18859
|
+
original: err,
|
|
18860
|
+
suggestion: "Check state resource configuration and database permissions"
|
|
18861
|
+
});
|
|
17824
18862
|
}
|
|
17825
18863
|
}
|
|
17826
18864
|
const initialStateConfig = machine.config.states[initialState];
|
|
@@ -17849,7 +18887,12 @@ class StateMachinePlugin extends Plugin {
|
|
|
17849
18887
|
visualize(machineId) {
|
|
17850
18888
|
const machine = this.machines.get(machineId);
|
|
17851
18889
|
if (!machine) {
|
|
17852
|
-
throw new
|
|
18890
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18891
|
+
operation: "visualize",
|
|
18892
|
+
machineId,
|
|
18893
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18894
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18895
|
+
});
|
|
17853
18896
|
}
|
|
17854
18897
|
let dot = `digraph ${machineId} {
|
|
17855
18898
|
`;
|
|
@@ -17899,6 +18942,7 @@ exports.AuditPlugin = AuditPlugin;
|
|
|
17899
18942
|
exports.AuthenticationError = AuthenticationError;
|
|
17900
18943
|
exports.BackupPlugin = BackupPlugin;
|
|
17901
18944
|
exports.BaseError = BaseError;
|
|
18945
|
+
exports.BehaviorError = BehaviorError;
|
|
17902
18946
|
exports.CachePlugin = CachePlugin;
|
|
17903
18947
|
exports.Client = Client;
|
|
17904
18948
|
exports.ConnectionString = ConnectionString;
|
|
@@ -17913,15 +18957,19 @@ exports.ErrorMap = ErrorMap;
|
|
|
17913
18957
|
exports.EventualConsistencyPlugin = EventualConsistencyPlugin;
|
|
17914
18958
|
exports.FullTextPlugin = FullTextPlugin;
|
|
17915
18959
|
exports.InvalidResourceItem = InvalidResourceItem;
|
|
18960
|
+
exports.MetadataLimitError = MetadataLimitError;
|
|
17916
18961
|
exports.MetricsPlugin = MetricsPlugin;
|
|
17917
18962
|
exports.MissingMetadata = MissingMetadata;
|
|
17918
18963
|
exports.NoSuchBucket = NoSuchBucket;
|
|
17919
18964
|
exports.NoSuchKey = NoSuchKey;
|
|
17920
18965
|
exports.NotFound = NotFound;
|
|
18966
|
+
exports.PartitionDriverError = PartitionDriverError;
|
|
17921
18967
|
exports.PartitionError = PartitionError;
|
|
17922
18968
|
exports.PermissionError = PermissionError;
|
|
17923
18969
|
exports.Plugin = Plugin;
|
|
18970
|
+
exports.PluginError = PluginError;
|
|
17924
18971
|
exports.PluginObject = PluginObject;
|
|
18972
|
+
exports.PluginStorageError = PluginStorageError;
|
|
17925
18973
|
exports.QueueConsumerPlugin = QueueConsumerPlugin;
|
|
17926
18974
|
exports.ReplicatorPlugin = ReplicatorPlugin;
|
|
17927
18975
|
exports.Resource = Resource;
|
|
@@ -17938,6 +18986,7 @@ exports.SchedulerPlugin = SchedulerPlugin;
|
|
|
17938
18986
|
exports.Schema = Schema;
|
|
17939
18987
|
exports.SchemaError = SchemaError;
|
|
17940
18988
|
exports.StateMachinePlugin = StateMachinePlugin;
|
|
18989
|
+
exports.StreamError = StreamError;
|
|
17941
18990
|
exports.UnknownError = UnknownError;
|
|
17942
18991
|
exports.ValidationError = ValidationError;
|
|
17943
18992
|
exports.Validator = Validator;
|