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