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.es.js
CHANGED
|
@@ -11,6 +11,7 @@ import jsonStableStringify from 'json-stable-stringify';
|
|
|
11
11
|
import { Transform, Writable } from 'stream';
|
|
12
12
|
import { PromisePool } from '@supercharge/promise-pool';
|
|
13
13
|
import { ReadableStream } from 'node:stream/web';
|
|
14
|
+
import os$1 from 'node:os';
|
|
14
15
|
import { chunk, merge, isString, isEmpty, invert, uniq, cloneDeep, get, set, isObject, isFunction } from 'lodash-es';
|
|
15
16
|
import { Agent } from 'http';
|
|
16
17
|
import { Agent as Agent$1 } from 'https';
|
|
@@ -217,7 +218,7 @@ function calculateEffectiveLimit(config = {}) {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
class BaseError extends Error {
|
|
220
|
-
constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata,
|
|
221
|
+
constructor({ verbose, bucket, key, message, code, statusCode, requestId, awsMessage, original, commandName, commandInput, metadata, description, ...rest }) {
|
|
221
222
|
if (verbose) message = message + `
|
|
222
223
|
|
|
223
224
|
Verbose:
|
|
@@ -242,7 +243,7 @@ ${JSON.stringify(rest, null, 2)}`;
|
|
|
242
243
|
this.commandName = commandName;
|
|
243
244
|
this.commandInput = commandInput;
|
|
244
245
|
this.metadata = metadata;
|
|
245
|
-
this.
|
|
246
|
+
this.description = description;
|
|
246
247
|
this.data = { bucket, key, ...rest, verbose, message };
|
|
247
248
|
}
|
|
248
249
|
toJson() {
|
|
@@ -259,7 +260,7 @@ ${JSON.stringify(rest, null, 2)}`;
|
|
|
259
260
|
commandName: this.commandName,
|
|
260
261
|
commandInput: this.commandInput,
|
|
261
262
|
metadata: this.metadata,
|
|
262
|
-
|
|
263
|
+
description: this.description,
|
|
263
264
|
data: this.data,
|
|
264
265
|
original: this.original,
|
|
265
266
|
stack: this.stack
|
|
@@ -399,26 +400,26 @@ function mapAwsError(err, context = {}) {
|
|
|
399
400
|
const metadata = err.$metadata ? { ...err.$metadata } : void 0;
|
|
400
401
|
const commandName = context.commandName;
|
|
401
402
|
const commandInput = context.commandInput;
|
|
402
|
-
let
|
|
403
|
+
let description;
|
|
403
404
|
if (code === "NoSuchKey" || code === "NotFound") {
|
|
404
|
-
|
|
405
|
-
return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput,
|
|
405
|
+
description = "The specified key does not exist in the bucket. Check if the key exists and if your credentials have permission to access it.";
|
|
406
|
+
return new NoSuchKey({ ...context, original: err, metadata, commandName, commandInput, description });
|
|
406
407
|
}
|
|
407
408
|
if (code === "NoSuchBucket") {
|
|
408
|
-
|
|
409
|
-
return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput,
|
|
409
|
+
description = "The specified bucket does not exist. Check if the bucket name is correct and if your credentials have permission to access it.";
|
|
410
|
+
return new NoSuchBucket({ ...context, original: err, metadata, commandName, commandInput, description });
|
|
410
411
|
}
|
|
411
412
|
if (code === "AccessDenied" || err.statusCode === 403 || code === "Forbidden") {
|
|
412
|
-
|
|
413
|
-
return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput,
|
|
413
|
+
description = "Access denied. Check your AWS credentials, IAM permissions, and bucket policy.";
|
|
414
|
+
return new PermissionError("Access denied", { ...context, original: err, metadata, commandName, commandInput, description });
|
|
414
415
|
}
|
|
415
416
|
if (code === "ValidationError" || err.statusCode === 400) {
|
|
416
|
-
|
|
417
|
-
return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput,
|
|
417
|
+
description = "Validation error. Check the request parameters and payload format.";
|
|
418
|
+
return new ValidationError("Validation error", { ...context, original: err, metadata, commandName, commandInput, description });
|
|
418
419
|
}
|
|
419
420
|
if (code === "MissingMetadata") {
|
|
420
|
-
|
|
421
|
-
return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput,
|
|
421
|
+
description = "Object metadata is missing or invalid. Check if the object was uploaded correctly.";
|
|
422
|
+
return new MissingMetadata({ ...context, original: err, metadata, commandName, commandInput, description });
|
|
422
423
|
}
|
|
423
424
|
const errorDetails = [
|
|
424
425
|
`Unknown error: ${err.message || err.toString()}`,
|
|
@@ -426,33 +427,388 @@ function mapAwsError(err, context = {}) {
|
|
|
426
427
|
err.statusCode && `Status: ${err.statusCode}`,
|
|
427
428
|
err.stack && `Stack: ${err.stack.split("\n")[0]}`
|
|
428
429
|
].filter(Boolean).join(" | ");
|
|
429
|
-
|
|
430
|
-
return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput,
|
|
430
|
+
description = `Check the error details and AWS documentation. Original error: ${err.message || err.toString()}`;
|
|
431
|
+
return new UnknownError(errorDetails, { ...context, original: err, metadata, commandName, commandInput, description });
|
|
431
432
|
}
|
|
432
433
|
class ConnectionStringError extends S3dbError {
|
|
433
434
|
constructor(message, details = {}) {
|
|
434
|
-
|
|
435
|
+
const description = details.description || "Invalid connection string format. Check the connection string syntax and credentials.";
|
|
436
|
+
super(message, { ...details, description });
|
|
435
437
|
}
|
|
436
438
|
}
|
|
437
439
|
class CryptoError extends S3dbError {
|
|
438
440
|
constructor(message, details = {}) {
|
|
439
|
-
|
|
441
|
+
const description = details.description || "Cryptography operation failed. Check if the crypto library is available and input is valid.";
|
|
442
|
+
super(message, { ...details, description });
|
|
440
443
|
}
|
|
441
444
|
}
|
|
442
445
|
class SchemaError extends S3dbError {
|
|
443
446
|
constructor(message, details = {}) {
|
|
444
|
-
|
|
447
|
+
const description = details.description || "Schema validation failed. Check schema definition and input data format.";
|
|
448
|
+
super(message, { ...details, description });
|
|
445
449
|
}
|
|
446
450
|
}
|
|
447
451
|
class ResourceError extends S3dbError {
|
|
448
452
|
constructor(message, details = {}) {
|
|
449
|
-
|
|
453
|
+
const description = details.description || "Resource operation failed. Check resource configuration, attributes, and operation context.";
|
|
454
|
+
super(message, { ...details, description });
|
|
450
455
|
Object.assign(this, details);
|
|
451
456
|
}
|
|
452
457
|
}
|
|
453
458
|
class PartitionError extends S3dbError {
|
|
454
459
|
constructor(message, details = {}) {
|
|
455
|
-
|
|
460
|
+
let description = details.description;
|
|
461
|
+
if (!description && details.resourceName && details.partitionName && details.fieldName) {
|
|
462
|
+
const { resourceName, partitionName, fieldName, availableFields = [] } = details;
|
|
463
|
+
description = `
|
|
464
|
+
Partition Field Validation Error
|
|
465
|
+
|
|
466
|
+
Resource: ${resourceName}
|
|
467
|
+
Partition: ${partitionName}
|
|
468
|
+
Missing Field: ${fieldName}
|
|
469
|
+
|
|
470
|
+
Available fields in schema:
|
|
471
|
+
${availableFields.map((f) => ` \u2022 ${f}`).join("\n") || " (no fields defined)"}
|
|
472
|
+
|
|
473
|
+
Possible causes:
|
|
474
|
+
1. Field was removed from schema but partition still references it
|
|
475
|
+
2. Typo in partition field name
|
|
476
|
+
3. Nested field path is incorrect (use dot notation like 'utm.source')
|
|
477
|
+
|
|
478
|
+
Solution:
|
|
479
|
+
${details.strictValidation === false ? " \u2022 Update partition definition to use existing fields" : ` \u2022 Add missing field to schema, OR
|
|
480
|
+
\u2022 Update partition definition to use existing fields, OR
|
|
481
|
+
\u2022 Use strictValidation: false to skip this check during testing`}
|
|
482
|
+
|
|
483
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partitions
|
|
484
|
+
`.trim();
|
|
485
|
+
}
|
|
486
|
+
super(message, {
|
|
487
|
+
...details,
|
|
488
|
+
description
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
class AnalyticsNotEnabledError extends S3dbError {
|
|
493
|
+
constructor(details = {}) {
|
|
494
|
+
const {
|
|
495
|
+
pluginName = "EventualConsistency",
|
|
496
|
+
resourceName = "unknown",
|
|
497
|
+
field = "unknown",
|
|
498
|
+
configuredResources = [],
|
|
499
|
+
registeredResources = [],
|
|
500
|
+
pluginInitialized = false,
|
|
501
|
+
...rest
|
|
502
|
+
} = details;
|
|
503
|
+
const message = `Analytics not enabled for ${resourceName}.${field}`;
|
|
504
|
+
const description = `
|
|
505
|
+
Analytics Not Enabled
|
|
506
|
+
|
|
507
|
+
Plugin: ${pluginName}
|
|
508
|
+
Resource: ${resourceName}
|
|
509
|
+
Field: ${field}
|
|
510
|
+
|
|
511
|
+
Diagnostics:
|
|
512
|
+
\u2022 Plugin initialized: ${pluginInitialized ? "\u2713 Yes" : "\u2717 No"}
|
|
513
|
+
\u2022 Analytics resources created: ${registeredResources.length}/${configuredResources.length}
|
|
514
|
+
${configuredResources.map((r) => {
|
|
515
|
+
const exists = registeredResources.includes(r);
|
|
516
|
+
return ` ${exists ? "\u2713" : "\u2717"} ${r}${!exists ? " (missing)" : ""}`;
|
|
517
|
+
}).join("\n")}
|
|
518
|
+
|
|
519
|
+
Possible causes:
|
|
520
|
+
1. Resource not created yet - Analytics resources are created when db.createResource() is called
|
|
521
|
+
2. Resource created before plugin initialization - Plugin must be initialized before resources
|
|
522
|
+
3. Field not configured in analytics.resources config
|
|
523
|
+
|
|
524
|
+
Correct initialization order:
|
|
525
|
+
1. Create database: const db = new Database({ ... })
|
|
526
|
+
2. Install plugins: await db.connect() (triggers plugin.install())
|
|
527
|
+
3. Create resources: await db.createResource({ name: '${resourceName}', ... })
|
|
528
|
+
4. Analytics resources are auto-created by plugin
|
|
529
|
+
|
|
530
|
+
Example fix:
|
|
531
|
+
const db = new Database({
|
|
532
|
+
bucket: 'my-bucket',
|
|
533
|
+
plugins: [new EventualConsistencyPlugin({
|
|
534
|
+
resources: {
|
|
535
|
+
'${resourceName}': {
|
|
536
|
+
fields: {
|
|
537
|
+
'${field}': { type: 'counter', analytics: true }
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
})]
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
await db.connect(); // Plugin initialized here
|
|
545
|
+
await db.createResource({ name: '${resourceName}', ... }); // Analytics resource created here
|
|
546
|
+
|
|
547
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/eventual-consistency.md
|
|
548
|
+
`.trim();
|
|
549
|
+
super(message, {
|
|
550
|
+
...rest,
|
|
551
|
+
pluginName,
|
|
552
|
+
resourceName,
|
|
553
|
+
field,
|
|
554
|
+
configuredResources,
|
|
555
|
+
registeredResources,
|
|
556
|
+
pluginInitialized,
|
|
557
|
+
description
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
class PluginError extends S3dbError {
|
|
562
|
+
constructor(message, details = {}) {
|
|
563
|
+
const {
|
|
564
|
+
pluginName = "Unknown",
|
|
565
|
+
operation = "unknown",
|
|
566
|
+
...rest
|
|
567
|
+
} = details;
|
|
568
|
+
let description = details.description;
|
|
569
|
+
if (!description) {
|
|
570
|
+
description = `
|
|
571
|
+
Plugin Error
|
|
572
|
+
|
|
573
|
+
Plugin: ${pluginName}
|
|
574
|
+
Operation: ${operation}
|
|
575
|
+
|
|
576
|
+
Possible causes:
|
|
577
|
+
1. Plugin not properly initialized
|
|
578
|
+
2. Plugin configuration is invalid
|
|
579
|
+
3. Plugin dependencies not met
|
|
580
|
+
4. Plugin method called before installation
|
|
581
|
+
|
|
582
|
+
Solution:
|
|
583
|
+
Ensure plugin is added to database and connect() is called before usage.
|
|
584
|
+
|
|
585
|
+
Example:
|
|
586
|
+
const db = new Database({
|
|
587
|
+
bucket: 'my-bucket',
|
|
588
|
+
plugins: [new ${pluginName}({ /* config */ })]
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
await db.connect(); // Plugin installed here
|
|
592
|
+
// Now plugin methods are available
|
|
593
|
+
|
|
594
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md
|
|
595
|
+
`.trim();
|
|
596
|
+
}
|
|
597
|
+
super(message, {
|
|
598
|
+
...rest,
|
|
599
|
+
pluginName,
|
|
600
|
+
operation,
|
|
601
|
+
description
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
class PluginStorageError extends S3dbError {
|
|
606
|
+
constructor(message, details = {}) {
|
|
607
|
+
const {
|
|
608
|
+
pluginSlug = "unknown",
|
|
609
|
+
key = "",
|
|
610
|
+
operation = "unknown",
|
|
611
|
+
...rest
|
|
612
|
+
} = details;
|
|
613
|
+
let description = details.description;
|
|
614
|
+
if (!description) {
|
|
615
|
+
description = `
|
|
616
|
+
Plugin Storage Error
|
|
617
|
+
|
|
618
|
+
Plugin: ${pluginSlug}
|
|
619
|
+
Key: ${key}
|
|
620
|
+
Operation: ${operation}
|
|
621
|
+
|
|
622
|
+
Possible causes:
|
|
623
|
+
1. Storage not initialized (plugin not installed)
|
|
624
|
+
2. Invalid key format
|
|
625
|
+
3. S3 operation failed
|
|
626
|
+
4. Permissions issue
|
|
627
|
+
|
|
628
|
+
Solution:
|
|
629
|
+
Ensure plugin has access to storage and key is valid.
|
|
630
|
+
|
|
631
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/README.md#plugin-storage
|
|
632
|
+
`.trim();
|
|
633
|
+
}
|
|
634
|
+
super(message, {
|
|
635
|
+
...rest,
|
|
636
|
+
pluginSlug,
|
|
637
|
+
key,
|
|
638
|
+
operation,
|
|
639
|
+
description
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
class PartitionDriverError extends S3dbError {
|
|
644
|
+
constructor(message, details = {}) {
|
|
645
|
+
const {
|
|
646
|
+
driver = "unknown",
|
|
647
|
+
operation = "unknown",
|
|
648
|
+
queueSize,
|
|
649
|
+
maxQueueSize,
|
|
650
|
+
...rest
|
|
651
|
+
} = details;
|
|
652
|
+
let description = details.description;
|
|
653
|
+
if (!description && queueSize !== void 0 && maxQueueSize !== void 0) {
|
|
654
|
+
description = `
|
|
655
|
+
Partition Driver Error
|
|
656
|
+
|
|
657
|
+
Driver: ${driver}
|
|
658
|
+
Operation: ${operation}
|
|
659
|
+
Queue Status: ${queueSize}/${maxQueueSize}
|
|
660
|
+
|
|
661
|
+
Possible causes:
|
|
662
|
+
1. Queue is full (backpressure)
|
|
663
|
+
2. Driver not properly configured
|
|
664
|
+
3. SQS permissions issue (if using SQS driver)
|
|
665
|
+
|
|
666
|
+
Solution:
|
|
667
|
+
${queueSize >= maxQueueSize ? "Wait for queue to drain or increase maxQueueSize" : "Check driver configuration and permissions"}
|
|
668
|
+
|
|
669
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
|
|
670
|
+
`.trim();
|
|
671
|
+
} else if (!description) {
|
|
672
|
+
description = `
|
|
673
|
+
Partition Driver Error
|
|
674
|
+
|
|
675
|
+
Driver: ${driver}
|
|
676
|
+
Operation: ${operation}
|
|
677
|
+
|
|
678
|
+
Check driver configuration and permissions.
|
|
679
|
+
|
|
680
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#partition-drivers
|
|
681
|
+
`.trim();
|
|
682
|
+
}
|
|
683
|
+
super(message, {
|
|
684
|
+
...rest,
|
|
685
|
+
driver,
|
|
686
|
+
operation,
|
|
687
|
+
queueSize,
|
|
688
|
+
maxQueueSize,
|
|
689
|
+
description
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
class BehaviorError extends S3dbError {
|
|
694
|
+
constructor(message, details = {}) {
|
|
695
|
+
const {
|
|
696
|
+
behavior = "unknown",
|
|
697
|
+
availableBehaviors = [],
|
|
698
|
+
...rest
|
|
699
|
+
} = details;
|
|
700
|
+
let description = details.description;
|
|
701
|
+
if (!description) {
|
|
702
|
+
description = `
|
|
703
|
+
Behavior Error
|
|
704
|
+
|
|
705
|
+
Requested: ${behavior}
|
|
706
|
+
Available: ${availableBehaviors.join(", ") || "body-overflow, body-only, truncate-data, enforce-limits, user-managed"}
|
|
707
|
+
|
|
708
|
+
Possible causes:
|
|
709
|
+
1. Behavior name misspelled
|
|
710
|
+
2. Custom behavior not registered
|
|
711
|
+
|
|
712
|
+
Solution:
|
|
713
|
+
Use one of the available behaviors or register custom behavior.
|
|
714
|
+
|
|
715
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#behaviors
|
|
716
|
+
`.trim();
|
|
717
|
+
}
|
|
718
|
+
super(message, {
|
|
719
|
+
...rest,
|
|
720
|
+
behavior,
|
|
721
|
+
availableBehaviors,
|
|
722
|
+
description
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
class StreamError extends S3dbError {
|
|
727
|
+
constructor(message, details = {}) {
|
|
728
|
+
const {
|
|
729
|
+
operation = "unknown",
|
|
730
|
+
resource,
|
|
731
|
+
...rest
|
|
732
|
+
} = details;
|
|
733
|
+
let description = details.description;
|
|
734
|
+
if (!description) {
|
|
735
|
+
description = `
|
|
736
|
+
Stream Error
|
|
737
|
+
|
|
738
|
+
Operation: ${operation}
|
|
739
|
+
${resource ? `Resource: ${resource}` : ""}
|
|
740
|
+
|
|
741
|
+
Possible causes:
|
|
742
|
+
1. Stream not properly initialized
|
|
743
|
+
2. Resource not available
|
|
744
|
+
3. Network error during streaming
|
|
745
|
+
|
|
746
|
+
Solution:
|
|
747
|
+
Check stream configuration and resource availability.
|
|
748
|
+
|
|
749
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#streaming
|
|
750
|
+
`.trim();
|
|
751
|
+
}
|
|
752
|
+
super(message, {
|
|
753
|
+
...rest,
|
|
754
|
+
operation,
|
|
755
|
+
resource,
|
|
756
|
+
description
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
class MetadataLimitError extends S3dbError {
|
|
761
|
+
constructor(message, details = {}) {
|
|
762
|
+
const {
|
|
763
|
+
totalSize,
|
|
764
|
+
effectiveLimit,
|
|
765
|
+
absoluteLimit = 2047,
|
|
766
|
+
excess,
|
|
767
|
+
resourceName,
|
|
768
|
+
operation,
|
|
769
|
+
...rest
|
|
770
|
+
} = details;
|
|
771
|
+
let description = details.description;
|
|
772
|
+
if (!description && totalSize && effectiveLimit) {
|
|
773
|
+
description = `
|
|
774
|
+
S3 Metadata Size Limit Exceeded
|
|
775
|
+
|
|
776
|
+
Current Size: ${totalSize} bytes
|
|
777
|
+
Effective Limit: ${effectiveLimit} bytes
|
|
778
|
+
Absolute Limit: ${absoluteLimit} bytes
|
|
779
|
+
${excess ? `Excess: ${excess} bytes` : ""}
|
|
780
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
781
|
+
${operation ? `Operation: ${operation}` : ""}
|
|
782
|
+
|
|
783
|
+
S3 has a hard limit of 2KB (2047 bytes) for object metadata.
|
|
784
|
+
|
|
785
|
+
Solutions:
|
|
786
|
+
1. Use 'body-overflow' behavior to store excess in body
|
|
787
|
+
2. Use 'body-only' behavior to store everything in body
|
|
788
|
+
3. Reduce number of fields
|
|
789
|
+
4. Use shorter field values
|
|
790
|
+
5. Enable advanced metadata encoding
|
|
791
|
+
|
|
792
|
+
Example:
|
|
793
|
+
await db.createResource({
|
|
794
|
+
name: '${resourceName || "myResource"}',
|
|
795
|
+
behavior: 'body-overflow', // Automatically handles overflow
|
|
796
|
+
attributes: { ... }
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/README.md#metadata-size-limits
|
|
800
|
+
`.trim();
|
|
801
|
+
}
|
|
802
|
+
super(message, {
|
|
803
|
+
...rest,
|
|
804
|
+
totalSize,
|
|
805
|
+
effectiveLimit,
|
|
806
|
+
absoluteLimit,
|
|
807
|
+
excess,
|
|
808
|
+
resourceName,
|
|
809
|
+
operation,
|
|
810
|
+
description
|
|
811
|
+
});
|
|
456
812
|
}
|
|
457
813
|
}
|
|
458
814
|
|
|
@@ -795,10 +1151,17 @@ class PluginStorage {
|
|
|
795
1151
|
*/
|
|
796
1152
|
constructor(client, pluginSlug) {
|
|
797
1153
|
if (!client) {
|
|
798
|
-
throw new
|
|
1154
|
+
throw new PluginStorageError("PluginStorage requires a client instance", {
|
|
1155
|
+
operation: "constructor",
|
|
1156
|
+
pluginSlug,
|
|
1157
|
+
suggestion: "Pass a valid S3db Client instance when creating PluginStorage"
|
|
1158
|
+
});
|
|
799
1159
|
}
|
|
800
1160
|
if (!pluginSlug) {
|
|
801
|
-
throw new
|
|
1161
|
+
throw new PluginStorageError("PluginStorage requires a pluginSlug", {
|
|
1162
|
+
operation: "constructor",
|
|
1163
|
+
suggestion: 'Provide a plugin slug (e.g., "eventual-consistency", "cache", "audit")'
|
|
1164
|
+
});
|
|
802
1165
|
}
|
|
803
1166
|
this.client = client;
|
|
804
1167
|
this.pluginSlug = pluginSlug;
|
|
@@ -851,7 +1214,15 @@ class PluginStorage {
|
|
|
851
1214
|
}
|
|
852
1215
|
const [ok, err] = await tryFn(() => this.client.putObject(putParams));
|
|
853
1216
|
if (!ok) {
|
|
854
|
-
throw new
|
|
1217
|
+
throw new PluginStorageError(`Failed to save plugin data`, {
|
|
1218
|
+
pluginSlug: this.pluginSlug,
|
|
1219
|
+
key,
|
|
1220
|
+
operation: "set",
|
|
1221
|
+
behavior,
|
|
1222
|
+
ttl,
|
|
1223
|
+
original: err,
|
|
1224
|
+
suggestion: "Check S3 permissions and key format"
|
|
1225
|
+
});
|
|
855
1226
|
}
|
|
856
1227
|
}
|
|
857
1228
|
/**
|
|
@@ -873,7 +1244,13 @@ class PluginStorage {
|
|
|
873
1244
|
if (err.name === "NoSuchKey" || err.Code === "NoSuchKey") {
|
|
874
1245
|
return null;
|
|
875
1246
|
}
|
|
876
|
-
throw new
|
|
1247
|
+
throw new PluginStorageError(`Failed to retrieve plugin data`, {
|
|
1248
|
+
pluginSlug: this.pluginSlug,
|
|
1249
|
+
key,
|
|
1250
|
+
operation: "get",
|
|
1251
|
+
original: err,
|
|
1252
|
+
suggestion: "Check if the key exists and S3 permissions are correct"
|
|
1253
|
+
});
|
|
877
1254
|
}
|
|
878
1255
|
const metadata = response.Metadata || {};
|
|
879
1256
|
const parsedMetadata = this._parseMetadataValues(metadata);
|
|
@@ -886,7 +1263,13 @@ class PluginStorage {
|
|
|
886
1263
|
data = { ...parsedMetadata, ...body };
|
|
887
1264
|
}
|
|
888
1265
|
} catch (parseErr) {
|
|
889
|
-
throw new
|
|
1266
|
+
throw new PluginStorageError(`Failed to parse JSON body`, {
|
|
1267
|
+
pluginSlug: this.pluginSlug,
|
|
1268
|
+
key,
|
|
1269
|
+
operation: "get",
|
|
1270
|
+
original: parseErr,
|
|
1271
|
+
suggestion: "Body content may be corrupted. Check S3 object integrity"
|
|
1272
|
+
});
|
|
890
1273
|
}
|
|
891
1274
|
}
|
|
892
1275
|
const expiresAt = data._expiresat || data._expiresAt;
|
|
@@ -947,7 +1330,15 @@ class PluginStorage {
|
|
|
947
1330
|
() => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
|
|
948
1331
|
);
|
|
949
1332
|
if (!ok) {
|
|
950
|
-
throw new
|
|
1333
|
+
throw new PluginStorageError(`Failed to list plugin data`, {
|
|
1334
|
+
pluginSlug: this.pluginSlug,
|
|
1335
|
+
operation: "list",
|
|
1336
|
+
prefix,
|
|
1337
|
+
fullPrefix,
|
|
1338
|
+
limit,
|
|
1339
|
+
original: err,
|
|
1340
|
+
suggestion: "Check S3 permissions and bucket configuration"
|
|
1341
|
+
});
|
|
951
1342
|
}
|
|
952
1343
|
const keys = result.Contents?.map((item) => item.Key) || [];
|
|
953
1344
|
return this._removeKeyPrefix(keys);
|
|
@@ -967,7 +1358,16 @@ class PluginStorage {
|
|
|
967
1358
|
() => this.client.listObjects({ prefix: fullPrefix, maxKeys: limit })
|
|
968
1359
|
);
|
|
969
1360
|
if (!ok) {
|
|
970
|
-
throw new
|
|
1361
|
+
throw new PluginStorageError(`Failed to list resource data`, {
|
|
1362
|
+
pluginSlug: this.pluginSlug,
|
|
1363
|
+
operation: "listForResource",
|
|
1364
|
+
resourceName,
|
|
1365
|
+
subPrefix,
|
|
1366
|
+
fullPrefix,
|
|
1367
|
+
limit,
|
|
1368
|
+
original: err,
|
|
1369
|
+
suggestion: "Check resource name and S3 permissions"
|
|
1370
|
+
});
|
|
971
1371
|
}
|
|
972
1372
|
const keys = result.Contents?.map((item) => item.Key) || [];
|
|
973
1373
|
return this._removeKeyPrefix(keys);
|
|
@@ -1107,7 +1507,13 @@ class PluginStorage {
|
|
|
1107
1507
|
async delete(key) {
|
|
1108
1508
|
const [ok, err] = await tryFn(() => this.client.deleteObject(key));
|
|
1109
1509
|
if (!ok) {
|
|
1110
|
-
throw new
|
|
1510
|
+
throw new PluginStorageError(`Failed to delete plugin data`, {
|
|
1511
|
+
pluginSlug: this.pluginSlug,
|
|
1512
|
+
key,
|
|
1513
|
+
operation: "delete",
|
|
1514
|
+
original: err,
|
|
1515
|
+
suggestion: "Check S3 delete permissions"
|
|
1516
|
+
});
|
|
1111
1517
|
}
|
|
1112
1518
|
}
|
|
1113
1519
|
/**
|
|
@@ -1294,16 +1700,28 @@ class PluginStorage {
|
|
|
1294
1700
|
const valueSize = calculateUTF8Bytes(encoded);
|
|
1295
1701
|
currentSize += keySize + valueSize;
|
|
1296
1702
|
if (currentSize > effectiveLimit) {
|
|
1297
|
-
throw new
|
|
1298
|
-
|
|
1299
|
-
|
|
1703
|
+
throw new MetadataLimitError(`Data exceeds metadata limit with enforce-limits behavior`, {
|
|
1704
|
+
totalSize: currentSize,
|
|
1705
|
+
effectiveLimit,
|
|
1706
|
+
absoluteLimit: S3_METADATA_LIMIT,
|
|
1707
|
+
excess: currentSize - effectiveLimit,
|
|
1708
|
+
operation: "PluginStorage.set",
|
|
1709
|
+
pluginSlug: this.pluginSlug,
|
|
1710
|
+
suggestion: "Use 'body-overflow' or 'body-only' behavior to handle large data"
|
|
1711
|
+
});
|
|
1300
1712
|
}
|
|
1301
1713
|
metadata[key] = jsonValue;
|
|
1302
1714
|
}
|
|
1303
1715
|
break;
|
|
1304
1716
|
}
|
|
1305
1717
|
default:
|
|
1306
|
-
throw new
|
|
1718
|
+
throw new BehaviorError(`Unknown behavior: ${behavior}`, {
|
|
1719
|
+
behavior,
|
|
1720
|
+
availableBehaviors: ["body-overflow", "body-only", "enforce-limits"],
|
|
1721
|
+
operation: "PluginStorage._applyBehavior",
|
|
1722
|
+
pluginSlug: this.pluginSlug,
|
|
1723
|
+
suggestion: "Use 'body-overflow', 'body-only', or 'enforce-limits'"
|
|
1724
|
+
});
|
|
1307
1725
|
}
|
|
1308
1726
|
return { metadata, body };
|
|
1309
1727
|
}
|
|
@@ -1868,6 +2286,35 @@ class AuditPlugin extends Plugin {
|
|
|
1868
2286
|
}
|
|
1869
2287
|
}
|
|
1870
2288
|
|
|
2289
|
+
class BackupError extends S3dbError {
|
|
2290
|
+
constructor(message, details = {}) {
|
|
2291
|
+
const { driver = "unknown", operation = "unknown", backupId, ...rest } = details;
|
|
2292
|
+
let description = details.description;
|
|
2293
|
+
if (!description) {
|
|
2294
|
+
description = `
|
|
2295
|
+
Backup Operation Error
|
|
2296
|
+
|
|
2297
|
+
Driver: ${driver}
|
|
2298
|
+
Operation: ${operation}
|
|
2299
|
+
${backupId ? `Backup ID: ${backupId}` : ""}
|
|
2300
|
+
|
|
2301
|
+
Common causes:
|
|
2302
|
+
1. Invalid backup driver configuration
|
|
2303
|
+
2. Destination storage not accessible
|
|
2304
|
+
3. Insufficient permissions
|
|
2305
|
+
4. Network connectivity issues
|
|
2306
|
+
5. Invalid backup file format
|
|
2307
|
+
|
|
2308
|
+
Solution:
|
|
2309
|
+
Check driver configuration and ensure destination storage is accessible.
|
|
2310
|
+
|
|
2311
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/backup.md
|
|
2312
|
+
`.trim();
|
|
2313
|
+
}
|
|
2314
|
+
super(message, { ...rest, driver, operation, backupId, description });
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
|
|
1871
2318
|
class BaseBackupDriver {
|
|
1872
2319
|
constructor(config = {}) {
|
|
1873
2320
|
this.config = {
|
|
@@ -1898,7 +2345,12 @@ class BaseBackupDriver {
|
|
|
1898
2345
|
* @returns {Object} Upload result with destination info
|
|
1899
2346
|
*/
|
|
1900
2347
|
async upload(filePath, backupId, manifest) {
|
|
1901
|
-
throw new
|
|
2348
|
+
throw new BackupError("upload() method must be implemented by subclass", {
|
|
2349
|
+
operation: "upload",
|
|
2350
|
+
driver: this.constructor.name,
|
|
2351
|
+
backupId,
|
|
2352
|
+
suggestion: "Extend BaseBackupDriver and implement the upload() method"
|
|
2353
|
+
});
|
|
1902
2354
|
}
|
|
1903
2355
|
/**
|
|
1904
2356
|
* Download a backup file from the destination
|
|
@@ -1908,7 +2360,12 @@ class BaseBackupDriver {
|
|
|
1908
2360
|
* @returns {string} Path to downloaded file
|
|
1909
2361
|
*/
|
|
1910
2362
|
async download(backupId, targetPath, metadata) {
|
|
1911
|
-
throw new
|
|
2363
|
+
throw new BackupError("download() method must be implemented by subclass", {
|
|
2364
|
+
operation: "download",
|
|
2365
|
+
driver: this.constructor.name,
|
|
2366
|
+
backupId,
|
|
2367
|
+
suggestion: "Extend BaseBackupDriver and implement the download() method"
|
|
2368
|
+
});
|
|
1912
2369
|
}
|
|
1913
2370
|
/**
|
|
1914
2371
|
* Delete a backup from the destination
|
|
@@ -1916,7 +2373,12 @@ class BaseBackupDriver {
|
|
|
1916
2373
|
* @param {Object} metadata - Backup metadata
|
|
1917
2374
|
*/
|
|
1918
2375
|
async delete(backupId, metadata) {
|
|
1919
|
-
throw new
|
|
2376
|
+
throw new BackupError("delete() method must be implemented by subclass", {
|
|
2377
|
+
operation: "delete",
|
|
2378
|
+
driver: this.constructor.name,
|
|
2379
|
+
backupId,
|
|
2380
|
+
suggestion: "Extend BaseBackupDriver and implement the delete() method"
|
|
2381
|
+
});
|
|
1920
2382
|
}
|
|
1921
2383
|
/**
|
|
1922
2384
|
* List backups available in the destination
|
|
@@ -1924,7 +2386,11 @@ class BaseBackupDriver {
|
|
|
1924
2386
|
* @returns {Array} List of backup metadata
|
|
1925
2387
|
*/
|
|
1926
2388
|
async list(options = {}) {
|
|
1927
|
-
throw new
|
|
2389
|
+
throw new BackupError("list() method must be implemented by subclass", {
|
|
2390
|
+
operation: "list",
|
|
2391
|
+
driver: this.constructor.name,
|
|
2392
|
+
suggestion: "Extend BaseBackupDriver and implement the list() method"
|
|
2393
|
+
});
|
|
1928
2394
|
}
|
|
1929
2395
|
/**
|
|
1930
2396
|
* Verify backup integrity
|
|
@@ -1934,14 +2400,23 @@ class BaseBackupDriver {
|
|
|
1934
2400
|
* @returns {boolean} True if backup is valid
|
|
1935
2401
|
*/
|
|
1936
2402
|
async verify(backupId, expectedChecksum, metadata) {
|
|
1937
|
-
throw new
|
|
2403
|
+
throw new BackupError("verify() method must be implemented by subclass", {
|
|
2404
|
+
operation: "verify",
|
|
2405
|
+
driver: this.constructor.name,
|
|
2406
|
+
backupId,
|
|
2407
|
+
suggestion: "Extend BaseBackupDriver and implement the verify() method"
|
|
2408
|
+
});
|
|
1938
2409
|
}
|
|
1939
2410
|
/**
|
|
1940
2411
|
* Get driver type identifier
|
|
1941
2412
|
* @returns {string} Driver type
|
|
1942
2413
|
*/
|
|
1943
2414
|
getType() {
|
|
1944
|
-
throw new
|
|
2415
|
+
throw new BackupError("getType() method must be implemented by subclass", {
|
|
2416
|
+
operation: "getType",
|
|
2417
|
+
driver: this.constructor.name,
|
|
2418
|
+
suggestion: "Extend BaseBackupDriver and implement the getType() method"
|
|
2419
|
+
});
|
|
1945
2420
|
}
|
|
1946
2421
|
/**
|
|
1947
2422
|
* Get driver-specific storage info
|
|
@@ -1983,7 +2458,11 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
1983
2458
|
}
|
|
1984
2459
|
async onSetup() {
|
|
1985
2460
|
if (!this.config.path) {
|
|
1986
|
-
throw new
|
|
2461
|
+
throw new BackupError("FilesystemBackupDriver: path configuration is required", {
|
|
2462
|
+
operation: "onSetup",
|
|
2463
|
+
driver: "filesystem",
|
|
2464
|
+
suggestion: 'Provide a path in config: new FilesystemBackupDriver({ path: "/path/to/backups" })'
|
|
2465
|
+
});
|
|
1987
2466
|
}
|
|
1988
2467
|
this.log(`Initialized with path: ${this.config.path}`);
|
|
1989
2468
|
}
|
|
@@ -2007,11 +2486,26 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2007
2486
|
() => mkdir(targetDir, { recursive: true, mode: this.config.directoryPermissions })
|
|
2008
2487
|
);
|
|
2009
2488
|
if (!createDirOk) {
|
|
2010
|
-
throw new
|
|
2489
|
+
throw new BackupError("Failed to create backup directory", {
|
|
2490
|
+
operation: "upload",
|
|
2491
|
+
driver: "filesystem",
|
|
2492
|
+
backupId,
|
|
2493
|
+
targetDir,
|
|
2494
|
+
original: createDirErr,
|
|
2495
|
+
suggestion: "Check directory permissions and disk space"
|
|
2496
|
+
});
|
|
2011
2497
|
}
|
|
2012
2498
|
const [copyOk, copyErr] = await tryFn(() => copyFile(filePath, targetPath));
|
|
2013
2499
|
if (!copyOk) {
|
|
2014
|
-
throw new
|
|
2500
|
+
throw new BackupError("Failed to copy backup file", {
|
|
2501
|
+
operation: "upload",
|
|
2502
|
+
driver: "filesystem",
|
|
2503
|
+
backupId,
|
|
2504
|
+
filePath,
|
|
2505
|
+
targetPath,
|
|
2506
|
+
original: copyErr,
|
|
2507
|
+
suggestion: "Check file permissions and disk space"
|
|
2508
|
+
});
|
|
2015
2509
|
}
|
|
2016
2510
|
const [manifestOk, manifestErr] = await tryFn(
|
|
2017
2511
|
() => import('fs/promises').then((fs) => fs.writeFile(
|
|
@@ -2022,7 +2516,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2022
2516
|
);
|
|
2023
2517
|
if (!manifestOk) {
|
|
2024
2518
|
await tryFn(() => unlink(targetPath));
|
|
2025
|
-
throw new
|
|
2519
|
+
throw new BackupError("Failed to write manifest file", {
|
|
2520
|
+
operation: "upload",
|
|
2521
|
+
driver: "filesystem",
|
|
2522
|
+
backupId,
|
|
2523
|
+
manifestPath,
|
|
2524
|
+
original: manifestErr,
|
|
2525
|
+
suggestion: "Check directory permissions and disk space"
|
|
2526
|
+
});
|
|
2026
2527
|
}
|
|
2027
2528
|
const [statOk, , stats] = await tryFn(() => stat(targetPath));
|
|
2028
2529
|
const size = statOk ? stats.size : 0;
|
|
@@ -2041,13 +2542,27 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2041
2542
|
);
|
|
2042
2543
|
const [existsOk] = await tryFn(() => access(sourcePath));
|
|
2043
2544
|
if (!existsOk) {
|
|
2044
|
-
throw new
|
|
2545
|
+
throw new BackupError("Backup file not found", {
|
|
2546
|
+
operation: "download",
|
|
2547
|
+
driver: "filesystem",
|
|
2548
|
+
backupId,
|
|
2549
|
+
sourcePath,
|
|
2550
|
+
suggestion: "Check if backup exists using list() method"
|
|
2551
|
+
});
|
|
2045
2552
|
}
|
|
2046
2553
|
const targetDir = path.dirname(targetPath);
|
|
2047
2554
|
await tryFn(() => mkdir(targetDir, { recursive: true }));
|
|
2048
2555
|
const [copyOk, copyErr] = await tryFn(() => copyFile(sourcePath, targetPath));
|
|
2049
2556
|
if (!copyOk) {
|
|
2050
|
-
throw new
|
|
2557
|
+
throw new BackupError("Failed to download backup", {
|
|
2558
|
+
operation: "download",
|
|
2559
|
+
driver: "filesystem",
|
|
2560
|
+
backupId,
|
|
2561
|
+
sourcePath,
|
|
2562
|
+
targetPath,
|
|
2563
|
+
original: copyErr,
|
|
2564
|
+
suggestion: "Check file permissions and disk space"
|
|
2565
|
+
});
|
|
2051
2566
|
}
|
|
2052
2567
|
this.log(`Downloaded backup ${backupId} from ${sourcePath} to ${targetPath}`);
|
|
2053
2568
|
return targetPath;
|
|
@@ -2064,7 +2579,14 @@ class FilesystemBackupDriver extends BaseBackupDriver {
|
|
|
2064
2579
|
const [deleteBackupOk] = await tryFn(() => unlink(backupPath));
|
|
2065
2580
|
const [deleteManifestOk] = await tryFn(() => unlink(manifestPath));
|
|
2066
2581
|
if (!deleteBackupOk && !deleteManifestOk) {
|
|
2067
|
-
throw new
|
|
2582
|
+
throw new BackupError("Failed to delete backup files", {
|
|
2583
|
+
operation: "delete",
|
|
2584
|
+
driver: "filesystem",
|
|
2585
|
+
backupId,
|
|
2586
|
+
backupPath,
|
|
2587
|
+
manifestPath,
|
|
2588
|
+
suggestion: "Check file permissions"
|
|
2589
|
+
});
|
|
2068
2590
|
}
|
|
2069
2591
|
this.log(`Deleted backup ${backupId}`);
|
|
2070
2592
|
}
|
|
@@ -2169,10 +2691,18 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2169
2691
|
this.config.bucket = this.database.bucket;
|
|
2170
2692
|
}
|
|
2171
2693
|
if (!this.config.client) {
|
|
2172
|
-
throw new
|
|
2694
|
+
throw new BackupError("S3BackupDriver: client is required", {
|
|
2695
|
+
operation: "onSetup",
|
|
2696
|
+
driver: "s3",
|
|
2697
|
+
suggestion: "Provide a client in config or ensure database has a client configured"
|
|
2698
|
+
});
|
|
2173
2699
|
}
|
|
2174
2700
|
if (!this.config.bucket) {
|
|
2175
|
-
throw new
|
|
2701
|
+
throw new BackupError("S3BackupDriver: bucket is required", {
|
|
2702
|
+
operation: "onSetup",
|
|
2703
|
+
driver: "s3",
|
|
2704
|
+
suggestion: "Provide a bucket in config or ensure database has a bucket configured"
|
|
2705
|
+
});
|
|
2176
2706
|
}
|
|
2177
2707
|
this.log(`Initialized with bucket: ${this.config.bucket}, path: ${this.config.path}`);
|
|
2178
2708
|
}
|
|
@@ -2214,7 +2744,15 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2214
2744
|
});
|
|
2215
2745
|
});
|
|
2216
2746
|
if (!uploadOk) {
|
|
2217
|
-
throw new
|
|
2747
|
+
throw new BackupError("Failed to upload backup file to S3", {
|
|
2748
|
+
operation: "upload",
|
|
2749
|
+
driver: "s3",
|
|
2750
|
+
backupId,
|
|
2751
|
+
bucket: this.config.bucket,
|
|
2752
|
+
key: backupKey,
|
|
2753
|
+
original: uploadErr,
|
|
2754
|
+
suggestion: "Check S3 permissions and bucket configuration"
|
|
2755
|
+
});
|
|
2218
2756
|
}
|
|
2219
2757
|
const [manifestOk, manifestErr] = await tryFn(
|
|
2220
2758
|
() => this.config.client.uploadObject({
|
|
@@ -2235,7 +2773,15 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2235
2773
|
bucket: this.config.bucket,
|
|
2236
2774
|
key: backupKey
|
|
2237
2775
|
}));
|
|
2238
|
-
throw new
|
|
2776
|
+
throw new BackupError("Failed to upload manifest to S3", {
|
|
2777
|
+
operation: "upload",
|
|
2778
|
+
driver: "s3",
|
|
2779
|
+
backupId,
|
|
2780
|
+
bucket: this.config.bucket,
|
|
2781
|
+
manifestKey,
|
|
2782
|
+
original: manifestErr,
|
|
2783
|
+
suggestion: "Check S3 permissions and bucket configuration"
|
|
2784
|
+
});
|
|
2239
2785
|
}
|
|
2240
2786
|
this.log(`Uploaded backup ${backupId} to s3://${this.config.bucket}/${backupKey} (${fileSize} bytes)`);
|
|
2241
2787
|
return {
|
|
@@ -2258,7 +2804,16 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2258
2804
|
})
|
|
2259
2805
|
);
|
|
2260
2806
|
if (!downloadOk) {
|
|
2261
|
-
throw new
|
|
2807
|
+
throw new BackupError("Failed to download backup from S3", {
|
|
2808
|
+
operation: "download",
|
|
2809
|
+
driver: "s3",
|
|
2810
|
+
backupId,
|
|
2811
|
+
bucket: this.config.bucket,
|
|
2812
|
+
key: backupKey,
|
|
2813
|
+
targetPath,
|
|
2814
|
+
original: downloadErr,
|
|
2815
|
+
suggestion: "Check if backup exists and S3 permissions are correct"
|
|
2816
|
+
});
|
|
2262
2817
|
}
|
|
2263
2818
|
this.log(`Downloaded backup ${backupId} from s3://${this.config.bucket}/${backupKey} to ${targetPath}`);
|
|
2264
2819
|
return targetPath;
|
|
@@ -2279,7 +2834,15 @@ class S3BackupDriver extends BaseBackupDriver {
|
|
|
2279
2834
|
})
|
|
2280
2835
|
);
|
|
2281
2836
|
if (!deleteBackupOk && !deleteManifestOk) {
|
|
2282
|
-
throw new
|
|
2837
|
+
throw new BackupError("Failed to delete backup from S3", {
|
|
2838
|
+
operation: "delete",
|
|
2839
|
+
driver: "s3",
|
|
2840
|
+
backupId,
|
|
2841
|
+
bucket: this.config.bucket,
|
|
2842
|
+
backupKey,
|
|
2843
|
+
manifestKey,
|
|
2844
|
+
suggestion: "Check S3 delete permissions"
|
|
2845
|
+
});
|
|
2283
2846
|
}
|
|
2284
2847
|
this.log(`Deleted backup ${backupId} from S3`);
|
|
2285
2848
|
}
|
|
@@ -2392,11 +2955,22 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2392
2955
|
}
|
|
2393
2956
|
async onSetup() {
|
|
2394
2957
|
if (!Array.isArray(this.config.destinations) || this.config.destinations.length === 0) {
|
|
2395
|
-
throw new
|
|
2958
|
+
throw new BackupError("MultiBackupDriver requires non-empty destinations array", {
|
|
2959
|
+
operation: "onSetup",
|
|
2960
|
+
driver: "multi",
|
|
2961
|
+
destinationsProvided: this.config.destinations,
|
|
2962
|
+
suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }, { driver: "filesystem", config: {...} }] }'
|
|
2963
|
+
});
|
|
2396
2964
|
}
|
|
2397
2965
|
for (const [index, destConfig] of this.config.destinations.entries()) {
|
|
2398
2966
|
if (!destConfig.driver) {
|
|
2399
|
-
throw new
|
|
2967
|
+
throw new BackupError(`Destination ${index} missing driver type`, {
|
|
2968
|
+
operation: "onSetup",
|
|
2969
|
+
driver: "multi",
|
|
2970
|
+
destinationIndex: index,
|
|
2971
|
+
destination: destConfig,
|
|
2972
|
+
suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} } or { driver: "filesystem", config: {...} }'
|
|
2973
|
+
});
|
|
2400
2974
|
}
|
|
2401
2975
|
try {
|
|
2402
2976
|
const driver = createBackupDriver(destConfig.driver, destConfig.config || {});
|
|
@@ -2408,7 +2982,15 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2408
2982
|
});
|
|
2409
2983
|
this.log(`Setup destination ${index}: ${destConfig.driver}`);
|
|
2410
2984
|
} catch (error) {
|
|
2411
|
-
throw new
|
|
2985
|
+
throw new BackupError(`Failed to setup destination ${index}`, {
|
|
2986
|
+
operation: "onSetup",
|
|
2987
|
+
driver: "multi",
|
|
2988
|
+
destinationIndex: index,
|
|
2989
|
+
destinationDriver: destConfig.driver,
|
|
2990
|
+
destinationConfig: destConfig.config,
|
|
2991
|
+
original: error,
|
|
2992
|
+
suggestion: "Check destination driver configuration and ensure dependencies are available"
|
|
2993
|
+
});
|
|
2412
2994
|
}
|
|
2413
2995
|
}
|
|
2414
2996
|
if (this.config.requireAll === false) {
|
|
@@ -2437,7 +3019,15 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2437
3019
|
this.log(`Priority upload failed to destination ${index}: ${err.message}`);
|
|
2438
3020
|
}
|
|
2439
3021
|
}
|
|
2440
|
-
throw new
|
|
3022
|
+
throw new BackupError("All priority destinations failed", {
|
|
3023
|
+
operation: "upload",
|
|
3024
|
+
driver: "multi",
|
|
3025
|
+
strategy: "priority",
|
|
3026
|
+
backupId,
|
|
3027
|
+
totalDestinations: this.drivers.length,
|
|
3028
|
+
failures: errors,
|
|
3029
|
+
suggestion: "Check destination configurations and ensure at least one destination is accessible"
|
|
3030
|
+
});
|
|
2441
3031
|
}
|
|
2442
3032
|
const uploadPromises = this.drivers.map(async ({ driver, config, index }) => {
|
|
2443
3033
|
const [ok, err, result] = await tryFn(
|
|
@@ -2467,10 +3057,28 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2467
3057
|
const successResults = allResults.filter((r) => r.status === "success");
|
|
2468
3058
|
const failedResults = allResults.filter((r) => r.status === "failed");
|
|
2469
3059
|
if (strategy === "all" && failedResults.length > 0) {
|
|
2470
|
-
throw new
|
|
3060
|
+
throw new BackupError('Some destinations failed with strategy "all"', {
|
|
3061
|
+
operation: "upload",
|
|
3062
|
+
driver: "multi",
|
|
3063
|
+
strategy: "all",
|
|
3064
|
+
backupId,
|
|
3065
|
+
totalDestinations: this.drivers.length,
|
|
3066
|
+
successCount: successResults.length,
|
|
3067
|
+
failedCount: failedResults.length,
|
|
3068
|
+
failures: failedResults,
|
|
3069
|
+
suggestion: 'All destinations must succeed with "all" strategy. Use "any" strategy to tolerate failures, or fix failing destinations.'
|
|
3070
|
+
});
|
|
2471
3071
|
}
|
|
2472
3072
|
if (strategy === "any" && successResults.length === 0) {
|
|
2473
|
-
throw new
|
|
3073
|
+
throw new BackupError('All destinations failed with strategy "any"', {
|
|
3074
|
+
operation: "upload",
|
|
3075
|
+
driver: "multi",
|
|
3076
|
+
strategy: "any",
|
|
3077
|
+
backupId,
|
|
3078
|
+
totalDestinations: this.drivers.length,
|
|
3079
|
+
failures: failedResults,
|
|
3080
|
+
suggestion: 'At least one destination must succeed with "any" strategy. Check all destination configurations.'
|
|
3081
|
+
});
|
|
2474
3082
|
}
|
|
2475
3083
|
return allResults;
|
|
2476
3084
|
}
|
|
@@ -2490,7 +3098,14 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2490
3098
|
this.log(`Download failed from destination ${destMetadata.destination}: ${err.message}`);
|
|
2491
3099
|
}
|
|
2492
3100
|
}
|
|
2493
|
-
throw new
|
|
3101
|
+
throw new BackupError("Failed to download backup from any destination", {
|
|
3102
|
+
operation: "download",
|
|
3103
|
+
driver: "multi",
|
|
3104
|
+
backupId,
|
|
3105
|
+
targetPath,
|
|
3106
|
+
attemptedDestinations: destinations.length,
|
|
3107
|
+
suggestion: "Check if backup exists in at least one destination and destinations are accessible"
|
|
3108
|
+
});
|
|
2494
3109
|
}
|
|
2495
3110
|
async delete(backupId, metadata) {
|
|
2496
3111
|
const destinations = Array.isArray(metadata.destinations) ? metadata.destinations : [metadata];
|
|
@@ -2512,7 +3127,14 @@ class MultiBackupDriver extends BaseBackupDriver {
|
|
|
2512
3127
|
}
|
|
2513
3128
|
}
|
|
2514
3129
|
if (successCount === 0 && errors.length > 0) {
|
|
2515
|
-
throw new
|
|
3130
|
+
throw new BackupError("Failed to delete from any destination", {
|
|
3131
|
+
operation: "delete",
|
|
3132
|
+
driver: "multi",
|
|
3133
|
+
backupId,
|
|
3134
|
+
attemptedDestinations: destinations.length,
|
|
3135
|
+
failures: errors,
|
|
3136
|
+
suggestion: "Check if backup exists in destinations and destinations are accessible with delete permissions"
|
|
3137
|
+
});
|
|
2516
3138
|
}
|
|
2517
3139
|
if (errors.length > 0) {
|
|
2518
3140
|
this.log(`Partial delete success, some errors: ${errors.join("; ")}`);
|
|
@@ -2612,32 +3234,62 @@ const BACKUP_DRIVERS = {
|
|
|
2612
3234
|
function createBackupDriver(driver, config = {}) {
|
|
2613
3235
|
const DriverClass = BACKUP_DRIVERS[driver];
|
|
2614
3236
|
if (!DriverClass) {
|
|
2615
|
-
throw new
|
|
3237
|
+
throw new BackupError(`Unknown backup driver: ${driver}`, {
|
|
3238
|
+
operation: "createBackupDriver",
|
|
3239
|
+
driver,
|
|
3240
|
+
availableDrivers: Object.keys(BACKUP_DRIVERS),
|
|
3241
|
+
suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
|
|
3242
|
+
});
|
|
2616
3243
|
}
|
|
2617
3244
|
return new DriverClass(config);
|
|
2618
3245
|
}
|
|
2619
3246
|
function validateBackupConfig(driver, config = {}) {
|
|
2620
3247
|
if (!driver || typeof driver !== "string") {
|
|
2621
|
-
throw new
|
|
3248
|
+
throw new BackupError("Driver type must be a non-empty string", {
|
|
3249
|
+
operation: "validateBackupConfig",
|
|
3250
|
+
driver,
|
|
3251
|
+
suggestion: "Provide a valid driver type string (filesystem, s3, or multi)"
|
|
3252
|
+
});
|
|
2622
3253
|
}
|
|
2623
3254
|
if (!BACKUP_DRIVERS[driver]) {
|
|
2624
|
-
throw new
|
|
3255
|
+
throw new BackupError(`Unknown backup driver: ${driver}`, {
|
|
3256
|
+
operation: "validateBackupConfig",
|
|
3257
|
+
driver,
|
|
3258
|
+
availableDrivers: Object.keys(BACKUP_DRIVERS),
|
|
3259
|
+
suggestion: `Use one of the available drivers: ${Object.keys(BACKUP_DRIVERS).join(", ")}`
|
|
3260
|
+
});
|
|
2625
3261
|
}
|
|
2626
3262
|
switch (driver) {
|
|
2627
3263
|
case "filesystem":
|
|
2628
3264
|
if (!config.path) {
|
|
2629
|
-
throw new
|
|
3265
|
+
throw new BackupError('FilesystemBackupDriver requires "path" configuration', {
|
|
3266
|
+
operation: "validateBackupConfig",
|
|
3267
|
+
driver: "filesystem",
|
|
3268
|
+
config,
|
|
3269
|
+
suggestion: 'Provide a "path" property in config: { path: "/path/to/backups" }'
|
|
3270
|
+
});
|
|
2630
3271
|
}
|
|
2631
3272
|
break;
|
|
2632
3273
|
case "s3":
|
|
2633
3274
|
break;
|
|
2634
3275
|
case "multi":
|
|
2635
3276
|
if (!Array.isArray(config.destinations) || config.destinations.length === 0) {
|
|
2636
|
-
throw new
|
|
3277
|
+
throw new BackupError('MultiBackupDriver requires non-empty "destinations" array', {
|
|
3278
|
+
operation: "validateBackupConfig",
|
|
3279
|
+
driver: "multi",
|
|
3280
|
+
config,
|
|
3281
|
+
suggestion: 'Provide destinations array: { destinations: [{ driver: "s3", config: {...} }] }'
|
|
3282
|
+
});
|
|
2637
3283
|
}
|
|
2638
3284
|
config.destinations.forEach((dest, index) => {
|
|
2639
3285
|
if (!dest.driver) {
|
|
2640
|
-
throw new
|
|
3286
|
+
throw new BackupError(`Destination ${index} must have a "driver" property`, {
|
|
3287
|
+
operation: "validateBackupConfig",
|
|
3288
|
+
driver: "multi",
|
|
3289
|
+
destinationIndex: index,
|
|
3290
|
+
destination: dest,
|
|
3291
|
+
suggestion: 'Each destination must have a driver property: { driver: "s3", config: {...} }'
|
|
3292
|
+
});
|
|
2641
3293
|
}
|
|
2642
3294
|
if (dest.driver !== "multi") {
|
|
2643
3295
|
validateBackupConfig(dest.driver, dest.config || {});
|
|
@@ -3293,6 +3945,36 @@ class BackupPlugin extends Plugin {
|
|
|
3293
3945
|
}
|
|
3294
3946
|
}
|
|
3295
3947
|
|
|
3948
|
+
class CacheError extends S3dbError {
|
|
3949
|
+
constructor(message, details = {}) {
|
|
3950
|
+
const { driver = "unknown", operation = "unknown", resourceName, key, ...rest } = details;
|
|
3951
|
+
let description = details.description;
|
|
3952
|
+
if (!description) {
|
|
3953
|
+
description = `
|
|
3954
|
+
Cache Operation Error
|
|
3955
|
+
|
|
3956
|
+
Driver: ${driver}
|
|
3957
|
+
Operation: ${operation}
|
|
3958
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
3959
|
+
${key ? `Key: ${key}` : ""}
|
|
3960
|
+
|
|
3961
|
+
Common causes:
|
|
3962
|
+
1. Invalid cache key format
|
|
3963
|
+
2. Cache driver not properly initialized
|
|
3964
|
+
3. Resource not found or not cached
|
|
3965
|
+
4. Memory limits exceeded
|
|
3966
|
+
5. Filesystem permissions issues
|
|
3967
|
+
|
|
3968
|
+
Solution:
|
|
3969
|
+
Check cache configuration and ensure the cache driver is properly initialized.
|
|
3970
|
+
|
|
3971
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/cache.md
|
|
3972
|
+
`.trim();
|
|
3973
|
+
}
|
|
3974
|
+
super(message, { ...rest, driver, operation, resourceName, key, description });
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
|
|
3296
3978
|
class Cache extends EventEmitter {
|
|
3297
3979
|
constructor(config = {}) {
|
|
3298
3980
|
super();
|
|
@@ -3309,7 +3991,13 @@ class Cache extends EventEmitter {
|
|
|
3309
3991
|
}
|
|
3310
3992
|
validateKey(key) {
|
|
3311
3993
|
if (key === null || key === void 0 || typeof key !== "string" || !key) {
|
|
3312
|
-
throw new
|
|
3994
|
+
throw new CacheError("Invalid cache key", {
|
|
3995
|
+
operation: "validateKey",
|
|
3996
|
+
driver: this.constructor.name,
|
|
3997
|
+
key,
|
|
3998
|
+
keyType: typeof key,
|
|
3999
|
+
suggestion: "Cache key must be a non-empty string"
|
|
4000
|
+
});
|
|
3313
4001
|
}
|
|
3314
4002
|
}
|
|
3315
4003
|
// generic class methods
|
|
@@ -3396,7 +4084,11 @@ class ResourceReader extends EventEmitter {
|
|
|
3396
4084
|
constructor({ resource, batchSize = 10, concurrency = 5 }) {
|
|
3397
4085
|
super();
|
|
3398
4086
|
if (!resource) {
|
|
3399
|
-
throw new
|
|
4087
|
+
throw new StreamError("Resource is required for ResourceReader", {
|
|
4088
|
+
operation: "constructor",
|
|
4089
|
+
resource: resource?.name,
|
|
4090
|
+
suggestion: "Pass a valid Resource instance when creating ResourceReader"
|
|
4091
|
+
});
|
|
3400
4092
|
}
|
|
3401
4093
|
this.resource = resource;
|
|
3402
4094
|
this.client = resource.client;
|
|
@@ -3520,7 +4212,10 @@ class ResourceWriter extends EventEmitter {
|
|
|
3520
4212
|
function streamToString(stream) {
|
|
3521
4213
|
return new Promise((resolve, reject) => {
|
|
3522
4214
|
if (!stream) {
|
|
3523
|
-
return reject(new
|
|
4215
|
+
return reject(new StreamError("Stream is undefined", {
|
|
4216
|
+
operation: "streamToString",
|
|
4217
|
+
suggestion: "Ensure a valid stream is passed to streamToString()"
|
|
4218
|
+
}));
|
|
3524
4219
|
}
|
|
3525
4220
|
const chunks = [];
|
|
3526
4221
|
stream.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -3601,6 +4296,24 @@ class MemoryCache extends Cache {
|
|
|
3601
4296
|
this.cache = {};
|
|
3602
4297
|
this.meta = {};
|
|
3603
4298
|
this.maxSize = config.maxSize !== void 0 ? config.maxSize : 1e3;
|
|
4299
|
+
if (config.maxMemoryBytes && config.maxMemoryBytes > 0 && config.maxMemoryPercent && config.maxMemoryPercent > 0) {
|
|
4300
|
+
throw new Error(
|
|
4301
|
+
"[MemoryCache] Cannot use both maxMemoryBytes and maxMemoryPercent. Choose one: maxMemoryBytes (absolute) or maxMemoryPercent (0...1 fraction)."
|
|
4302
|
+
);
|
|
4303
|
+
}
|
|
4304
|
+
if (config.maxMemoryPercent && config.maxMemoryPercent > 0) {
|
|
4305
|
+
if (config.maxMemoryPercent > 1) {
|
|
4306
|
+
throw new Error(
|
|
4307
|
+
`[MemoryCache] maxMemoryPercent must be between 0 and 1 (e.g., 0.1 for 10%). Received: ${config.maxMemoryPercent}`
|
|
4308
|
+
);
|
|
4309
|
+
}
|
|
4310
|
+
const totalMemory = os$1.totalmem();
|
|
4311
|
+
this.maxMemoryBytes = Math.floor(totalMemory * config.maxMemoryPercent);
|
|
4312
|
+
this.maxMemoryPercent = config.maxMemoryPercent;
|
|
4313
|
+
} else {
|
|
4314
|
+
this.maxMemoryBytes = config.maxMemoryBytes !== void 0 ? config.maxMemoryBytes : 0;
|
|
4315
|
+
this.maxMemoryPercent = 0;
|
|
4316
|
+
}
|
|
3604
4317
|
this.ttl = config.ttl !== void 0 ? config.ttl : 3e5;
|
|
3605
4318
|
this.enableCompression = config.enableCompression !== void 0 ? config.enableCompression : false;
|
|
3606
4319
|
this.compressionThreshold = config.compressionThreshold !== void 0 ? config.compressionThreshold : 1024;
|
|
@@ -3610,23 +4323,18 @@ class MemoryCache extends Cache {
|
|
|
3610
4323
|
totalCompressedSize: 0,
|
|
3611
4324
|
compressionRatio: 0
|
|
3612
4325
|
};
|
|
4326
|
+
this.currentMemoryBytes = 0;
|
|
4327
|
+
this.evictedDueToMemory = 0;
|
|
3613
4328
|
}
|
|
3614
4329
|
async _set(key, data) {
|
|
3615
|
-
if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
|
|
3616
|
-
const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
|
|
3617
|
-
if (oldestKey) {
|
|
3618
|
-
delete this.cache[oldestKey];
|
|
3619
|
-
delete this.meta[oldestKey];
|
|
3620
|
-
}
|
|
3621
|
-
}
|
|
3622
4330
|
let finalData = data;
|
|
3623
4331
|
let compressed = false;
|
|
3624
4332
|
let originalSize = 0;
|
|
3625
4333
|
let compressedSize = 0;
|
|
4334
|
+
const serialized = JSON.stringify(data);
|
|
4335
|
+
originalSize = Buffer.byteLength(serialized, "utf8");
|
|
3626
4336
|
if (this.enableCompression) {
|
|
3627
4337
|
try {
|
|
3628
|
-
const serialized = JSON.stringify(data);
|
|
3629
|
-
originalSize = Buffer.byteLength(serialized, "utf8");
|
|
3630
4338
|
if (originalSize >= this.compressionThreshold) {
|
|
3631
4339
|
const compressedBuffer = zlib.gzipSync(Buffer.from(serialized, "utf8"));
|
|
3632
4340
|
finalData = {
|
|
@@ -3645,13 +4353,42 @@ class MemoryCache extends Cache {
|
|
|
3645
4353
|
console.warn(`[MemoryCache] Compression failed for key '${key}':`, error.message);
|
|
3646
4354
|
}
|
|
3647
4355
|
}
|
|
4356
|
+
const itemSize = compressed ? compressedSize : originalSize;
|
|
4357
|
+
if (Object.prototype.hasOwnProperty.call(this.cache, key)) {
|
|
4358
|
+
const oldSize = this.meta[key]?.compressedSize || 0;
|
|
4359
|
+
this.currentMemoryBytes -= oldSize;
|
|
4360
|
+
}
|
|
4361
|
+
if (this.maxMemoryBytes > 0) {
|
|
4362
|
+
while (this.currentMemoryBytes + itemSize > this.maxMemoryBytes && Object.keys(this.cache).length > 0) {
|
|
4363
|
+
const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
|
|
4364
|
+
if (oldestKey) {
|
|
4365
|
+
const evictedSize = this.meta[oldestKey]?.compressedSize || 0;
|
|
4366
|
+
delete this.cache[oldestKey];
|
|
4367
|
+
delete this.meta[oldestKey];
|
|
4368
|
+
this.currentMemoryBytes -= evictedSize;
|
|
4369
|
+
this.evictedDueToMemory++;
|
|
4370
|
+
} else {
|
|
4371
|
+
break;
|
|
4372
|
+
}
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
if (this.maxSize > 0 && Object.keys(this.cache).length >= this.maxSize) {
|
|
4376
|
+
const oldestKey = Object.entries(this.meta).sort((a, b) => a[1].ts - b[1].ts)[0]?.[0];
|
|
4377
|
+
if (oldestKey) {
|
|
4378
|
+
const evictedSize = this.meta[oldestKey]?.compressedSize || 0;
|
|
4379
|
+
delete this.cache[oldestKey];
|
|
4380
|
+
delete this.meta[oldestKey];
|
|
4381
|
+
this.currentMemoryBytes -= evictedSize;
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
3648
4384
|
this.cache[key] = finalData;
|
|
3649
4385
|
this.meta[key] = {
|
|
3650
4386
|
ts: Date.now(),
|
|
3651
4387
|
compressed,
|
|
3652
4388
|
originalSize,
|
|
3653
|
-
compressedSize:
|
|
4389
|
+
compressedSize: itemSize
|
|
3654
4390
|
};
|
|
4391
|
+
this.currentMemoryBytes += itemSize;
|
|
3655
4392
|
return data;
|
|
3656
4393
|
}
|
|
3657
4394
|
async _get(key) {
|
|
@@ -3659,7 +4396,9 @@ class MemoryCache extends Cache {
|
|
|
3659
4396
|
if (this.ttl > 0) {
|
|
3660
4397
|
const now = Date.now();
|
|
3661
4398
|
const meta = this.meta[key];
|
|
3662
|
-
if (meta && now - meta.ts > this.ttl
|
|
4399
|
+
if (meta && now - meta.ts > this.ttl) {
|
|
4400
|
+
const itemSize = meta.compressedSize || 0;
|
|
4401
|
+
this.currentMemoryBytes -= itemSize;
|
|
3663
4402
|
delete this.cache[key];
|
|
3664
4403
|
delete this.meta[key];
|
|
3665
4404
|
return null;
|
|
@@ -3681,6 +4420,10 @@ class MemoryCache extends Cache {
|
|
|
3681
4420
|
return rawData;
|
|
3682
4421
|
}
|
|
3683
4422
|
async _del(key) {
|
|
4423
|
+
if (Object.prototype.hasOwnProperty.call(this.cache, key)) {
|
|
4424
|
+
const itemSize = this.meta[key]?.compressedSize || 0;
|
|
4425
|
+
this.currentMemoryBytes -= itemSize;
|
|
4426
|
+
}
|
|
3684
4427
|
delete this.cache[key];
|
|
3685
4428
|
delete this.meta[key];
|
|
3686
4429
|
return true;
|
|
@@ -3689,10 +4432,13 @@ class MemoryCache extends Cache {
|
|
|
3689
4432
|
if (!prefix) {
|
|
3690
4433
|
this.cache = {};
|
|
3691
4434
|
this.meta = {};
|
|
4435
|
+
this.currentMemoryBytes = 0;
|
|
3692
4436
|
return true;
|
|
3693
4437
|
}
|
|
3694
4438
|
for (const key of Object.keys(this.cache)) {
|
|
3695
4439
|
if (key.startsWith(prefix)) {
|
|
4440
|
+
const itemSize = this.meta[key]?.compressedSize || 0;
|
|
4441
|
+
this.currentMemoryBytes -= itemSize;
|
|
3696
4442
|
delete this.cache[key];
|
|
3697
4443
|
delete this.meta[key];
|
|
3698
4444
|
}
|
|
@@ -3730,6 +4476,53 @@ class MemoryCache extends Cache {
|
|
|
3730
4476
|
}
|
|
3731
4477
|
};
|
|
3732
4478
|
}
|
|
4479
|
+
/**
|
|
4480
|
+
* Get memory usage statistics
|
|
4481
|
+
* @returns {Object} Memory stats including current usage, limits, and eviction counts
|
|
4482
|
+
*/
|
|
4483
|
+
getMemoryStats() {
|
|
4484
|
+
const totalItems = Object.keys(this.cache).length;
|
|
4485
|
+
const memoryUsagePercent = this.maxMemoryBytes > 0 ? (this.currentMemoryBytes / this.maxMemoryBytes * 100).toFixed(2) : 0;
|
|
4486
|
+
const systemMemory = {
|
|
4487
|
+
total: os$1.totalmem(),
|
|
4488
|
+
free: os$1.freemem(),
|
|
4489
|
+
used: os$1.totalmem() - os$1.freemem()
|
|
4490
|
+
};
|
|
4491
|
+
const cachePercentOfTotal = systemMemory.total > 0 ? (this.currentMemoryBytes / systemMemory.total * 100).toFixed(2) : 0;
|
|
4492
|
+
return {
|
|
4493
|
+
currentMemoryBytes: this.currentMemoryBytes,
|
|
4494
|
+
maxMemoryBytes: this.maxMemoryBytes,
|
|
4495
|
+
maxMemoryPercent: this.maxMemoryPercent,
|
|
4496
|
+
memoryUsagePercent: parseFloat(memoryUsagePercent),
|
|
4497
|
+
cachePercentOfSystemMemory: parseFloat(cachePercentOfTotal),
|
|
4498
|
+
totalItems,
|
|
4499
|
+
maxSize: this.maxSize,
|
|
4500
|
+
evictedDueToMemory: this.evictedDueToMemory,
|
|
4501
|
+
averageItemSize: totalItems > 0 ? Math.round(this.currentMemoryBytes / totalItems) : 0,
|
|
4502
|
+
memoryUsage: {
|
|
4503
|
+
current: this._formatBytes(this.currentMemoryBytes),
|
|
4504
|
+
max: this.maxMemoryBytes > 0 ? this._formatBytes(this.maxMemoryBytes) : "unlimited",
|
|
4505
|
+
available: this.maxMemoryBytes > 0 ? this._formatBytes(this.maxMemoryBytes - this.currentMemoryBytes) : "unlimited"
|
|
4506
|
+
},
|
|
4507
|
+
systemMemory: {
|
|
4508
|
+
total: this._formatBytes(systemMemory.total),
|
|
4509
|
+
free: this._formatBytes(systemMemory.free),
|
|
4510
|
+
used: this._formatBytes(systemMemory.used),
|
|
4511
|
+
cachePercent: `${cachePercentOfTotal}%`
|
|
4512
|
+
}
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
/**
|
|
4516
|
+
* Format bytes to human-readable format
|
|
4517
|
+
* @private
|
|
4518
|
+
*/
|
|
4519
|
+
_formatBytes(bytes) {
|
|
4520
|
+
if (bytes === 0) return "0 B";
|
|
4521
|
+
const k = 1024;
|
|
4522
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
4523
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
4524
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
4525
|
+
}
|
|
3733
4526
|
}
|
|
3734
4527
|
|
|
3735
4528
|
class FilesystemCache extends Cache {
|
|
@@ -4559,8 +5352,10 @@ class CachePlugin extends Plugin {
|
|
|
4559
5352
|
config: {
|
|
4560
5353
|
ttl: options.ttl,
|
|
4561
5354
|
maxSize: options.maxSize,
|
|
5355
|
+
maxMemoryBytes: options.maxMemoryBytes,
|
|
5356
|
+
maxMemoryPercent: options.maxMemoryPercent,
|
|
4562
5357
|
...options.config
|
|
4563
|
-
// Driver-specific config (can override ttl/maxSize)
|
|
5358
|
+
// Driver-specific config (can override ttl/maxSize/maxMemoryBytes/maxMemoryPercent)
|
|
4564
5359
|
},
|
|
4565
5360
|
// Resource filtering
|
|
4566
5361
|
include: options.include || null,
|
|
@@ -4914,7 +5709,13 @@ class CachePlugin extends Plugin {
|
|
|
4914
5709
|
async warmCache(resourceName, options = {}) {
|
|
4915
5710
|
const resource = this.database.resources[resourceName];
|
|
4916
5711
|
if (!resource) {
|
|
4917
|
-
throw new
|
|
5712
|
+
throw new CacheError("Resource not found for cache warming", {
|
|
5713
|
+
operation: "warmCache",
|
|
5714
|
+
driver: this.driver?.constructor.name,
|
|
5715
|
+
resourceName,
|
|
5716
|
+
availableResources: Object.keys(this.database.resources),
|
|
5717
|
+
suggestion: "Check resource name spelling or ensure resource has been created"
|
|
5718
|
+
});
|
|
4918
5719
|
}
|
|
4919
5720
|
const { includePartitions = true, sampleSize = 100 } = options;
|
|
4920
5721
|
if (this.driver instanceof PartitionAwareFilesystemCache && resource.warmPartitionCache) {
|
|
@@ -7019,6 +7820,80 @@ async function getLastNMonths(resourceName, field, months = 12, options, fieldHa
|
|
|
7019
7820
|
}
|
|
7020
7821
|
return data;
|
|
7021
7822
|
}
|
|
7823
|
+
async function getRawEvents(resourceName, field, options, fieldHandlers) {
|
|
7824
|
+
const resourceHandlers = fieldHandlers.get(resourceName);
|
|
7825
|
+
if (!resourceHandlers) {
|
|
7826
|
+
throw new Error(`No eventual consistency configured for resource: ${resourceName}`);
|
|
7827
|
+
}
|
|
7828
|
+
const handler = resourceHandlers.get(field);
|
|
7829
|
+
if (!handler) {
|
|
7830
|
+
throw new Error(`No eventual consistency configured for field: ${resourceName}.${field}`);
|
|
7831
|
+
}
|
|
7832
|
+
if (!handler.transactionResource) {
|
|
7833
|
+
throw new Error("Transaction resource not initialized");
|
|
7834
|
+
}
|
|
7835
|
+
const {
|
|
7836
|
+
recordId,
|
|
7837
|
+
startDate,
|
|
7838
|
+
endDate,
|
|
7839
|
+
cohortDate,
|
|
7840
|
+
cohortHour,
|
|
7841
|
+
cohortMonth,
|
|
7842
|
+
applied,
|
|
7843
|
+
operation,
|
|
7844
|
+
limit
|
|
7845
|
+
} = options;
|
|
7846
|
+
const query = {};
|
|
7847
|
+
if (recordId !== void 0) {
|
|
7848
|
+
query.originalId = recordId;
|
|
7849
|
+
}
|
|
7850
|
+
if (applied !== void 0) {
|
|
7851
|
+
query.applied = applied;
|
|
7852
|
+
}
|
|
7853
|
+
const [ok, err, allTransactions] = await tryFn(
|
|
7854
|
+
() => handler.transactionResource.query(query)
|
|
7855
|
+
);
|
|
7856
|
+
if (!ok || !allTransactions) {
|
|
7857
|
+
return [];
|
|
7858
|
+
}
|
|
7859
|
+
let filtered = allTransactions;
|
|
7860
|
+
if (operation !== void 0) {
|
|
7861
|
+
filtered = filtered.filter((t) => t.operation === operation);
|
|
7862
|
+
}
|
|
7863
|
+
if (cohortDate) {
|
|
7864
|
+
filtered = filtered.filter((t) => t.cohortDate === cohortDate);
|
|
7865
|
+
}
|
|
7866
|
+
if (cohortHour) {
|
|
7867
|
+
filtered = filtered.filter((t) => t.cohortHour === cohortHour);
|
|
7868
|
+
}
|
|
7869
|
+
if (cohortMonth) {
|
|
7870
|
+
filtered = filtered.filter((t) => t.cohortMonth === cohortMonth);
|
|
7871
|
+
}
|
|
7872
|
+
if (startDate && endDate) {
|
|
7873
|
+
const isHourly = startDate.length > 10;
|
|
7874
|
+
const cohortField = isHourly ? "cohortHour" : "cohortDate";
|
|
7875
|
+
filtered = filtered.filter(
|
|
7876
|
+
(t) => t[cohortField] && t[cohortField] >= startDate && t[cohortField] <= endDate
|
|
7877
|
+
);
|
|
7878
|
+
} else if (startDate) {
|
|
7879
|
+
const isHourly = startDate.length > 10;
|
|
7880
|
+
const cohortField = isHourly ? "cohortHour" : "cohortDate";
|
|
7881
|
+
filtered = filtered.filter((t) => t[cohortField] && t[cohortField] >= startDate);
|
|
7882
|
+
} else if (endDate) {
|
|
7883
|
+
const isHourly = endDate.length > 10;
|
|
7884
|
+
const cohortField = isHourly ? "cohortHour" : "cohortDate";
|
|
7885
|
+
filtered = filtered.filter((t) => t[cohortField] && t[cohortField] <= endDate);
|
|
7886
|
+
}
|
|
7887
|
+
filtered.sort((a, b) => {
|
|
7888
|
+
const aTime = new Date(a.timestamp || a.createdAt).getTime();
|
|
7889
|
+
const bTime = new Date(b.timestamp || b.createdAt).getTime();
|
|
7890
|
+
return bTime - aTime;
|
|
7891
|
+
});
|
|
7892
|
+
if (limit && limit > 0) {
|
|
7893
|
+
filtered = filtered.slice(0, limit);
|
|
7894
|
+
}
|
|
7895
|
+
return filtered;
|
|
7896
|
+
}
|
|
7022
7897
|
|
|
7023
7898
|
function addHelperMethods(resource, plugin, config) {
|
|
7024
7899
|
resource.set = async (id, field, value) => {
|
|
@@ -7776,6 +8651,214 @@ class EventualConsistencyPlugin extends Plugin {
|
|
|
7776
8651
|
async getLastNMonths(resourceName, field, months = 12, options = {}) {
|
|
7777
8652
|
return await getLastNMonths(resourceName, field, months, options, this.fieldHandlers);
|
|
7778
8653
|
}
|
|
8654
|
+
/**
|
|
8655
|
+
* Get raw transaction events for custom aggregation
|
|
8656
|
+
*
|
|
8657
|
+
* This method provides direct access to the underlying transaction events,
|
|
8658
|
+
* allowing developers to perform custom aggregations beyond the pre-built analytics.
|
|
8659
|
+
* Useful for complex queries, custom metrics, or when you need the raw event data.
|
|
8660
|
+
*
|
|
8661
|
+
* @param {string} resourceName - Resource name
|
|
8662
|
+
* @param {string} field - Field name
|
|
8663
|
+
* @param {Object} options - Query options
|
|
8664
|
+
* @param {string} options.recordId - Filter by specific record ID
|
|
8665
|
+
* @param {string} options.startDate - Start date filter (YYYY-MM-DD or YYYY-MM-DDTHH)
|
|
8666
|
+
* @param {string} options.endDate - End date filter (YYYY-MM-DD or YYYY-MM-DDTHH)
|
|
8667
|
+
* @param {string} options.cohortDate - Filter by cohort date (YYYY-MM-DD)
|
|
8668
|
+
* @param {string} options.cohortHour - Filter by cohort hour (YYYY-MM-DDTHH)
|
|
8669
|
+
* @param {string} options.cohortMonth - Filter by cohort month (YYYY-MM)
|
|
8670
|
+
* @param {boolean} options.applied - Filter by applied status (true/false/undefined for both)
|
|
8671
|
+
* @param {string} options.operation - Filter by operation type ('add', 'sub', 'set')
|
|
8672
|
+
* @param {number} options.limit - Maximum number of events to return
|
|
8673
|
+
* @returns {Promise<Array>} Raw transaction events
|
|
8674
|
+
*
|
|
8675
|
+
* @example
|
|
8676
|
+
* // Get all events for a specific record
|
|
8677
|
+
* const events = await plugin.getRawEvents('wallets', 'balance', {
|
|
8678
|
+
* recordId: 'wallet1'
|
|
8679
|
+
* });
|
|
8680
|
+
*
|
|
8681
|
+
* @example
|
|
8682
|
+
* // Get events for a specific time range
|
|
8683
|
+
* const events = await plugin.getRawEvents('wallets', 'balance', {
|
|
8684
|
+
* startDate: '2025-10-01',
|
|
8685
|
+
* endDate: '2025-10-31'
|
|
8686
|
+
* });
|
|
8687
|
+
*
|
|
8688
|
+
* @example
|
|
8689
|
+
* // Get only pending (unapplied) transactions
|
|
8690
|
+
* const pending = await plugin.getRawEvents('wallets', 'balance', {
|
|
8691
|
+
* applied: false
|
|
8692
|
+
* });
|
|
8693
|
+
*/
|
|
8694
|
+
async getRawEvents(resourceName, field, options = {}) {
|
|
8695
|
+
return await getRawEvents(resourceName, field, options, this.fieldHandlers);
|
|
8696
|
+
}
|
|
8697
|
+
/**
|
|
8698
|
+
* Get diagnostics information about the plugin state
|
|
8699
|
+
*
|
|
8700
|
+
* This method provides comprehensive diagnostic information about the EventualConsistencyPlugin,
|
|
8701
|
+
* including configured resources, field handlers, timers, and overall health status.
|
|
8702
|
+
* Useful for debugging initialization issues, configuration problems, or runtime errors.
|
|
8703
|
+
*
|
|
8704
|
+
* @param {Object} options - Diagnostic options
|
|
8705
|
+
* @param {string} options.resourceName - Optional: limit diagnostics to specific resource
|
|
8706
|
+
* @param {string} options.field - Optional: limit diagnostics to specific field
|
|
8707
|
+
* @param {boolean} options.includeStats - Include transaction statistics (default: false)
|
|
8708
|
+
* @returns {Promise<Object>} Diagnostic information
|
|
8709
|
+
*
|
|
8710
|
+
* @example
|
|
8711
|
+
* // Get overall plugin diagnostics
|
|
8712
|
+
* const diagnostics = await plugin.getDiagnostics();
|
|
8713
|
+
* console.log(diagnostics);
|
|
8714
|
+
*
|
|
8715
|
+
* @example
|
|
8716
|
+
* // Get diagnostics for specific resource/field with stats
|
|
8717
|
+
* const diagnostics = await plugin.getDiagnostics({
|
|
8718
|
+
* resourceName: 'wallets',
|
|
8719
|
+
* field: 'balance',
|
|
8720
|
+
* includeStats: true
|
|
8721
|
+
* });
|
|
8722
|
+
*/
|
|
8723
|
+
async getDiagnostics(options = {}) {
|
|
8724
|
+
const { resourceName, field, includeStats = false } = options;
|
|
8725
|
+
const diagnostics = {
|
|
8726
|
+
plugin: {
|
|
8727
|
+
name: "EventualConsistencyPlugin",
|
|
8728
|
+
initialized: this.database !== null && this.database !== void 0,
|
|
8729
|
+
verbose: this.config.verbose || false,
|
|
8730
|
+
timezone: this.config.cohort?.timezone || "UTC",
|
|
8731
|
+
consolidation: {
|
|
8732
|
+
mode: this.config.consolidation?.mode || "timer",
|
|
8733
|
+
interval: this.config.consolidation?.interval || 6e4,
|
|
8734
|
+
batchSize: this.config.consolidation?.batchSize || 100
|
|
8735
|
+
},
|
|
8736
|
+
garbageCollection: {
|
|
8737
|
+
enabled: this.config.garbageCollection?.enabled !== false,
|
|
8738
|
+
retentionDays: this.config.garbageCollection?.retentionDays || 30,
|
|
8739
|
+
interval: this.config.garbageCollection?.interval || 36e5
|
|
8740
|
+
}
|
|
8741
|
+
},
|
|
8742
|
+
resources: [],
|
|
8743
|
+
errors: [],
|
|
8744
|
+
warnings: []
|
|
8745
|
+
};
|
|
8746
|
+
for (const [resName, resourceHandlers] of this.fieldHandlers.entries()) {
|
|
8747
|
+
if (resourceName && resName !== resourceName) {
|
|
8748
|
+
continue;
|
|
8749
|
+
}
|
|
8750
|
+
const resourceDiag = {
|
|
8751
|
+
name: resName,
|
|
8752
|
+
fields: []
|
|
8753
|
+
};
|
|
8754
|
+
for (const [fieldName, handler] of resourceHandlers.entries()) {
|
|
8755
|
+
if (field && fieldName !== field) {
|
|
8756
|
+
continue;
|
|
8757
|
+
}
|
|
8758
|
+
const fieldDiag = {
|
|
8759
|
+
name: fieldName,
|
|
8760
|
+
type: handler.type || "counter",
|
|
8761
|
+
analyticsEnabled: handler.analyticsResource !== null && handler.analyticsResource !== void 0,
|
|
8762
|
+
resources: {
|
|
8763
|
+
transaction: handler.transactionResource?.name || null,
|
|
8764
|
+
target: handler.targetResource?.name || null,
|
|
8765
|
+
analytics: handler.analyticsResource?.name || null
|
|
8766
|
+
},
|
|
8767
|
+
timers: {
|
|
8768
|
+
consolidation: handler.consolidationTimer !== null && handler.consolidationTimer !== void 0,
|
|
8769
|
+
garbageCollection: handler.garbageCollectionTimer !== null && handler.garbageCollectionTimer !== void 0
|
|
8770
|
+
}
|
|
8771
|
+
};
|
|
8772
|
+
if (!handler.transactionResource) {
|
|
8773
|
+
diagnostics.errors.push({
|
|
8774
|
+
resource: resName,
|
|
8775
|
+
field: fieldName,
|
|
8776
|
+
issue: "Missing transaction resource",
|
|
8777
|
+
suggestion: "Ensure plugin is installed and resources are created after plugin installation"
|
|
8778
|
+
});
|
|
8779
|
+
}
|
|
8780
|
+
if (!handler.targetResource) {
|
|
8781
|
+
diagnostics.warnings.push({
|
|
8782
|
+
resource: resName,
|
|
8783
|
+
field: fieldName,
|
|
8784
|
+
issue: "Missing target resource",
|
|
8785
|
+
suggestion: "Target resource may not have been created yet"
|
|
8786
|
+
});
|
|
8787
|
+
}
|
|
8788
|
+
if (handler.analyticsResource && !handler.analyticsResource.name) {
|
|
8789
|
+
diagnostics.errors.push({
|
|
8790
|
+
resource: resName,
|
|
8791
|
+
field: fieldName,
|
|
8792
|
+
issue: "Invalid analytics resource",
|
|
8793
|
+
suggestion: "Analytics resource exists but has no name - possible initialization failure"
|
|
8794
|
+
});
|
|
8795
|
+
}
|
|
8796
|
+
if (includeStats && handler.transactionResource) {
|
|
8797
|
+
try {
|
|
8798
|
+
const [okPending, errPending, pendingTxns] = await handler.transactionResource.query({ applied: false }).catch(() => [false, null, []]);
|
|
8799
|
+
const [okApplied, errApplied, appliedTxns] = await handler.transactionResource.query({ applied: true }).catch(() => [false, null, []]);
|
|
8800
|
+
fieldDiag.stats = {
|
|
8801
|
+
pendingTransactions: okPending ? pendingTxns?.length || 0 : "error",
|
|
8802
|
+
appliedTransactions: okApplied ? appliedTxns?.length || 0 : "error",
|
|
8803
|
+
totalTransactions: okPending && okApplied ? (pendingTxns?.length || 0) + (appliedTxns?.length || 0) : "error"
|
|
8804
|
+
};
|
|
8805
|
+
if (handler.analyticsResource) {
|
|
8806
|
+
const [okAnalytics, errAnalytics, analyticsRecords] = await handler.analyticsResource.list().catch(() => [false, null, []]);
|
|
8807
|
+
fieldDiag.stats.analyticsRecords = okAnalytics ? analyticsRecords?.length || 0 : "error";
|
|
8808
|
+
}
|
|
8809
|
+
} catch (error) {
|
|
8810
|
+
diagnostics.warnings.push({
|
|
8811
|
+
resource: resName,
|
|
8812
|
+
field: fieldName,
|
|
8813
|
+
issue: "Failed to fetch statistics",
|
|
8814
|
+
error: error.message
|
|
8815
|
+
});
|
|
8816
|
+
}
|
|
8817
|
+
}
|
|
8818
|
+
resourceDiag.fields.push(fieldDiag);
|
|
8819
|
+
}
|
|
8820
|
+
if (resourceDiag.fields.length > 0) {
|
|
8821
|
+
diagnostics.resources.push(resourceDiag);
|
|
8822
|
+
}
|
|
8823
|
+
}
|
|
8824
|
+
diagnostics.health = {
|
|
8825
|
+
status: diagnostics.errors.length === 0 ? diagnostics.warnings.length === 0 ? "healthy" : "warning" : "error",
|
|
8826
|
+
totalResources: diagnostics.resources.length,
|
|
8827
|
+
totalFields: diagnostics.resources.reduce((sum, r) => sum + r.fields.length, 0),
|
|
8828
|
+
errorCount: diagnostics.errors.length,
|
|
8829
|
+
warningCount: diagnostics.warnings.length
|
|
8830
|
+
};
|
|
8831
|
+
return diagnostics;
|
|
8832
|
+
}
|
|
8833
|
+
}
|
|
8834
|
+
|
|
8835
|
+
class FulltextError extends S3dbError {
|
|
8836
|
+
constructor(message, details = {}) {
|
|
8837
|
+
const { resourceName, query, operation = "unknown", ...rest } = details;
|
|
8838
|
+
let description = details.description;
|
|
8839
|
+
if (!description) {
|
|
8840
|
+
description = `
|
|
8841
|
+
Fulltext Search Operation Error
|
|
8842
|
+
|
|
8843
|
+
Operation: ${operation}
|
|
8844
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
8845
|
+
${query ? `Query: ${query}` : ""}
|
|
8846
|
+
|
|
8847
|
+
Common causes:
|
|
8848
|
+
1. Resource not indexed for fulltext search
|
|
8849
|
+
2. Invalid query syntax
|
|
8850
|
+
3. Index not built yet
|
|
8851
|
+
4. Search configuration missing
|
|
8852
|
+
5. Field not indexed
|
|
8853
|
+
|
|
8854
|
+
Solution:
|
|
8855
|
+
Ensure resource is configured for fulltext search and index is built.
|
|
8856
|
+
|
|
8857
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/fulltext.md
|
|
8858
|
+
`.trim();
|
|
8859
|
+
}
|
|
8860
|
+
super(message, { ...rest, resourceName, query, operation, description });
|
|
8861
|
+
}
|
|
7779
8862
|
}
|
|
7780
8863
|
|
|
7781
8864
|
class FullTextPlugin extends Plugin {
|
|
@@ -8084,7 +9167,13 @@ class FullTextPlugin extends Plugin {
|
|
|
8084
9167
|
}
|
|
8085
9168
|
const resource = this.database.resources[resourceName];
|
|
8086
9169
|
if (!resource) {
|
|
8087
|
-
throw new
|
|
9170
|
+
throw new FulltextError(`Resource '${resourceName}' not found`, {
|
|
9171
|
+
operation: "searchRecords",
|
|
9172
|
+
resourceName,
|
|
9173
|
+
query,
|
|
9174
|
+
availableResources: Object.keys(this.database.resources),
|
|
9175
|
+
suggestion: "Check resource name or ensure resource is created before searching"
|
|
9176
|
+
});
|
|
8088
9177
|
}
|
|
8089
9178
|
const recordIds = searchResults.map((result2) => result2.recordId);
|
|
8090
9179
|
const records = await resource.getMany(recordIds);
|
|
@@ -8101,7 +9190,12 @@ class FullTextPlugin extends Plugin {
|
|
|
8101
9190
|
async rebuildIndex(resourceName) {
|
|
8102
9191
|
const resource = this.database.resources[resourceName];
|
|
8103
9192
|
if (!resource) {
|
|
8104
|
-
throw new
|
|
9193
|
+
throw new FulltextError(`Resource '${resourceName}' not found`, {
|
|
9194
|
+
operation: "rebuildIndex",
|
|
9195
|
+
resourceName,
|
|
9196
|
+
availableResources: Object.keys(this.database.resources),
|
|
9197
|
+
suggestion: "Check resource name or ensure resource is created before rebuilding index"
|
|
9198
|
+
});
|
|
8105
9199
|
}
|
|
8106
9200
|
for (const [key] of this.indexes.entries()) {
|
|
8107
9201
|
if (key.startsWith(`${resourceName}:`)) {
|
|
@@ -8886,6 +9980,35 @@ function createConsumer(driver, config) {
|
|
|
8886
9980
|
return new ConsumerClass(config);
|
|
8887
9981
|
}
|
|
8888
9982
|
|
|
9983
|
+
class QueueError extends S3dbError {
|
|
9984
|
+
constructor(message, details = {}) {
|
|
9985
|
+
const { queueName, operation = "unknown", messageId, ...rest } = details;
|
|
9986
|
+
let description = details.description;
|
|
9987
|
+
if (!description) {
|
|
9988
|
+
description = `
|
|
9989
|
+
Queue Operation Error
|
|
9990
|
+
|
|
9991
|
+
Operation: ${operation}
|
|
9992
|
+
${queueName ? `Queue: ${queueName}` : ""}
|
|
9993
|
+
${messageId ? `Message ID: ${messageId}` : ""}
|
|
9994
|
+
|
|
9995
|
+
Common causes:
|
|
9996
|
+
1. Queue not properly configured
|
|
9997
|
+
2. Message handler not registered
|
|
9998
|
+
3. Queue resource not found
|
|
9999
|
+
4. SQS/RabbitMQ connection failed
|
|
10000
|
+
5. Message processing timeout
|
|
10001
|
+
|
|
10002
|
+
Solution:
|
|
10003
|
+
Check queue configuration and message handler registration.
|
|
10004
|
+
|
|
10005
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue.md
|
|
10006
|
+
`.trim();
|
|
10007
|
+
}
|
|
10008
|
+
super(message, { ...rest, queueName, operation, messageId, description });
|
|
10009
|
+
}
|
|
10010
|
+
}
|
|
10011
|
+
|
|
8889
10012
|
class QueueConsumerPlugin extends Plugin {
|
|
8890
10013
|
constructor(options = {}) {
|
|
8891
10014
|
super(options);
|
|
@@ -8946,13 +10069,32 @@ class QueueConsumerPlugin extends Plugin {
|
|
|
8946
10069
|
let action = body.action || msg.action;
|
|
8947
10070
|
let data = body.data || msg.data;
|
|
8948
10071
|
if (!resource) {
|
|
8949
|
-
throw new
|
|
10072
|
+
throw new QueueError("Resource not found in message", {
|
|
10073
|
+
operation: "handleMessage",
|
|
10074
|
+
queueName: configuredResource,
|
|
10075
|
+
messageBody: body,
|
|
10076
|
+
suggestion: 'Ensure message includes a "resource" field specifying the target resource name'
|
|
10077
|
+
});
|
|
8950
10078
|
}
|
|
8951
10079
|
if (!action) {
|
|
8952
|
-
throw new
|
|
10080
|
+
throw new QueueError("Action not found in message", {
|
|
10081
|
+
operation: "handleMessage",
|
|
10082
|
+
queueName: configuredResource,
|
|
10083
|
+
resource,
|
|
10084
|
+
messageBody: body,
|
|
10085
|
+
suggestion: 'Ensure message includes an "action" field (insert, update, or delete)'
|
|
10086
|
+
});
|
|
8953
10087
|
}
|
|
8954
10088
|
const resourceObj = this.database.resources[resource];
|
|
8955
|
-
if (!resourceObj)
|
|
10089
|
+
if (!resourceObj) {
|
|
10090
|
+
throw new QueueError(`Resource '${resource}' not found`, {
|
|
10091
|
+
operation: "handleMessage",
|
|
10092
|
+
queueName: configuredResource,
|
|
10093
|
+
resource,
|
|
10094
|
+
availableResources: Object.keys(this.database.resources),
|
|
10095
|
+
suggestion: "Check resource name or ensure resource is created before consuming messages"
|
|
10096
|
+
});
|
|
10097
|
+
}
|
|
8956
10098
|
let result;
|
|
8957
10099
|
const [ok, err, res] = await tryFn(async () => {
|
|
8958
10100
|
if (action === "insert") {
|
|
@@ -8963,7 +10105,14 @@ class QueueConsumerPlugin extends Plugin {
|
|
|
8963
10105
|
} else if (action === "delete") {
|
|
8964
10106
|
result = await resourceObj.delete(data.id);
|
|
8965
10107
|
} else {
|
|
8966
|
-
throw new
|
|
10108
|
+
throw new QueueError(`Unsupported action '${action}'`, {
|
|
10109
|
+
operation: "handleMessage",
|
|
10110
|
+
queueName: configuredResource,
|
|
10111
|
+
resource,
|
|
10112
|
+
action,
|
|
10113
|
+
supportedActions: ["insert", "update", "delete"],
|
|
10114
|
+
suggestion: "Use one of the supported actions: insert, update, or delete"
|
|
10115
|
+
});
|
|
8967
10116
|
}
|
|
8968
10117
|
return result;
|
|
8969
10118
|
});
|
|
@@ -8976,6 +10125,35 @@ class QueueConsumerPlugin extends Plugin {
|
|
|
8976
10125
|
}
|
|
8977
10126
|
}
|
|
8978
10127
|
|
|
10128
|
+
class ReplicationError extends S3dbError {
|
|
10129
|
+
constructor(message, details = {}) {
|
|
10130
|
+
const { replicatorClass = "unknown", operation = "unknown", resourceName, ...rest } = details;
|
|
10131
|
+
let description = details.description;
|
|
10132
|
+
if (!description) {
|
|
10133
|
+
description = `
|
|
10134
|
+
Replication Operation Error
|
|
10135
|
+
|
|
10136
|
+
Replicator: ${replicatorClass}
|
|
10137
|
+
Operation: ${operation}
|
|
10138
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
10139
|
+
|
|
10140
|
+
Common causes:
|
|
10141
|
+
1. Invalid replicator configuration
|
|
10142
|
+
2. Target system not accessible
|
|
10143
|
+
3. Resource not configured for replication
|
|
10144
|
+
4. Invalid operation type
|
|
10145
|
+
5. Transformation function errors
|
|
10146
|
+
|
|
10147
|
+
Solution:
|
|
10148
|
+
Check replicator configuration and ensure target system is accessible.
|
|
10149
|
+
|
|
10150
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md
|
|
10151
|
+
`.trim();
|
|
10152
|
+
}
|
|
10153
|
+
super(message, { ...rest, replicatorClass, operation, resourceName, description });
|
|
10154
|
+
}
|
|
10155
|
+
}
|
|
10156
|
+
|
|
8979
10157
|
class BaseReplicator extends EventEmitter {
|
|
8980
10158
|
constructor(config = {}) {
|
|
8981
10159
|
super();
|
|
@@ -9001,7 +10179,12 @@ class BaseReplicator extends EventEmitter {
|
|
|
9001
10179
|
* @returns {Promise<Object>} replicator result
|
|
9002
10180
|
*/
|
|
9003
10181
|
async replicate(resourceName, operation, data, id) {
|
|
9004
|
-
throw new
|
|
10182
|
+
throw new ReplicationError("replicate() method must be implemented by subclass", {
|
|
10183
|
+
operation: "replicate",
|
|
10184
|
+
replicatorClass: this.name,
|
|
10185
|
+
resourceName,
|
|
10186
|
+
suggestion: "Extend BaseReplicator and implement the replicate() method"
|
|
10187
|
+
});
|
|
9005
10188
|
}
|
|
9006
10189
|
/**
|
|
9007
10190
|
* Replicate multiple records in batch
|
|
@@ -9010,14 +10193,24 @@ class BaseReplicator extends EventEmitter {
|
|
|
9010
10193
|
* @returns {Promise<Object>} Batch replicator result
|
|
9011
10194
|
*/
|
|
9012
10195
|
async replicateBatch(resourceName, records) {
|
|
9013
|
-
throw new
|
|
10196
|
+
throw new ReplicationError("replicateBatch() method must be implemented by subclass", {
|
|
10197
|
+
operation: "replicateBatch",
|
|
10198
|
+
replicatorClass: this.name,
|
|
10199
|
+
resourceName,
|
|
10200
|
+
batchSize: records?.length,
|
|
10201
|
+
suggestion: "Extend BaseReplicator and implement the replicateBatch() method"
|
|
10202
|
+
});
|
|
9014
10203
|
}
|
|
9015
10204
|
/**
|
|
9016
10205
|
* Test the connection to the target
|
|
9017
10206
|
* @returns {Promise<boolean>} True if connection is successful
|
|
9018
10207
|
*/
|
|
9019
10208
|
async testConnection() {
|
|
9020
|
-
throw new
|
|
10209
|
+
throw new ReplicationError("testConnection() method must be implemented by subclass", {
|
|
10210
|
+
operation: "testConnection",
|
|
10211
|
+
replicatorClass: this.name,
|
|
10212
|
+
suggestion: "Extend BaseReplicator and implement the testConnection() method"
|
|
10213
|
+
});
|
|
9021
10214
|
}
|
|
9022
10215
|
/**
|
|
9023
10216
|
* Get replicator status and statistics
|
|
@@ -10189,7 +11382,17 @@ class Client extends EventEmitter {
|
|
|
10189
11382
|
});
|
|
10190
11383
|
this.emit("moveAllObjects", { results, errors }, { prefixFrom, prefixTo });
|
|
10191
11384
|
if (errors.length > 0) {
|
|
10192
|
-
throw new
|
|
11385
|
+
throw new UnknownError("Some objects could not be moved", {
|
|
11386
|
+
bucket: this.config.bucket,
|
|
11387
|
+
operation: "moveAllObjects",
|
|
11388
|
+
prefixFrom,
|
|
11389
|
+
prefixTo,
|
|
11390
|
+
totalKeys: keys.length,
|
|
11391
|
+
failedCount: errors.length,
|
|
11392
|
+
successCount: results.length,
|
|
11393
|
+
errors: errors.map((e) => ({ message: e.message, raw: e.raw })),
|
|
11394
|
+
suggestion: "Check S3 permissions and retry failed objects individually"
|
|
11395
|
+
});
|
|
10193
11396
|
}
|
|
10194
11397
|
return results;
|
|
10195
11398
|
}
|
|
@@ -10903,7 +12106,14 @@ async function handleInsert$4({ resource, data, mappedData, originalData }) {
|
|
|
10903
12106
|
}
|
|
10904
12107
|
});
|
|
10905
12108
|
if (totalSize > effectiveLimit) {
|
|
10906
|
-
throw new
|
|
12109
|
+
throw new MetadataLimitError("Metadata size exceeds 2KB limit on insert", {
|
|
12110
|
+
totalSize,
|
|
12111
|
+
effectiveLimit,
|
|
12112
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
12113
|
+
excess: totalSize - effectiveLimit,
|
|
12114
|
+
resourceName: resource.name,
|
|
12115
|
+
operation: "insert"
|
|
12116
|
+
});
|
|
10907
12117
|
}
|
|
10908
12118
|
return { mappedData, body: "" };
|
|
10909
12119
|
}
|
|
@@ -10918,7 +12128,15 @@ async function handleUpdate$4({ resource, id, data, mappedData, originalData })
|
|
|
10918
12128
|
}
|
|
10919
12129
|
});
|
|
10920
12130
|
if (totalSize > effectiveLimit) {
|
|
10921
|
-
throw new
|
|
12131
|
+
throw new MetadataLimitError("Metadata size exceeds 2KB limit on update", {
|
|
12132
|
+
totalSize,
|
|
12133
|
+
effectiveLimit,
|
|
12134
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
12135
|
+
excess: totalSize - effectiveLimit,
|
|
12136
|
+
resourceName: resource.name,
|
|
12137
|
+
operation: "update",
|
|
12138
|
+
id
|
|
12139
|
+
});
|
|
10922
12140
|
}
|
|
10923
12141
|
return { mappedData, body: JSON.stringify(mappedData) };
|
|
10924
12142
|
}
|
|
@@ -10933,7 +12151,15 @@ async function handleUpsert$4({ resource, id, data, mappedData }) {
|
|
|
10933
12151
|
}
|
|
10934
12152
|
});
|
|
10935
12153
|
if (totalSize > effectiveLimit) {
|
|
10936
|
-
throw new
|
|
12154
|
+
throw new MetadataLimitError("Metadata size exceeds 2KB limit on upsert", {
|
|
12155
|
+
totalSize,
|
|
12156
|
+
effectiveLimit,
|
|
12157
|
+
absoluteLimit: S3_METADATA_LIMIT_BYTES,
|
|
12158
|
+
excess: totalSize - effectiveLimit,
|
|
12159
|
+
resourceName: resource.name,
|
|
12160
|
+
operation: "upsert",
|
|
12161
|
+
id
|
|
12162
|
+
});
|
|
10937
12163
|
}
|
|
10938
12164
|
return { mappedData, body: "" };
|
|
10939
12165
|
}
|
|
@@ -11275,7 +12501,11 @@ const behaviors = {
|
|
|
11275
12501
|
function getBehavior(behaviorName) {
|
|
11276
12502
|
const behavior = behaviors[behaviorName];
|
|
11277
12503
|
if (!behavior) {
|
|
11278
|
-
throw new
|
|
12504
|
+
throw new BehaviorError(`Unknown behavior: ${behaviorName}`, {
|
|
12505
|
+
behavior: behaviorName,
|
|
12506
|
+
availableBehaviors: Object.keys(behaviors),
|
|
12507
|
+
operation: "getBehavior"
|
|
12508
|
+
});
|
|
11279
12509
|
}
|
|
11280
12510
|
return behavior;
|
|
11281
12511
|
}
|
|
@@ -11397,6 +12627,7 @@ ${errorDetails}`,
|
|
|
11397
12627
|
idGenerator: customIdGenerator,
|
|
11398
12628
|
idSize = 22,
|
|
11399
12629
|
versioningEnabled = false,
|
|
12630
|
+
strictValidation = true,
|
|
11400
12631
|
events = {},
|
|
11401
12632
|
asyncEvents = true,
|
|
11402
12633
|
asyncPartitions = true,
|
|
@@ -11410,6 +12641,7 @@ ${errorDetails}`,
|
|
|
11410
12641
|
this.parallelism = parallelism;
|
|
11411
12642
|
this.passphrase = passphrase ?? "secret";
|
|
11412
12643
|
this.versioningEnabled = versioningEnabled;
|
|
12644
|
+
this.strictValidation = strictValidation;
|
|
11413
12645
|
this.setAsyncMode(asyncEvents);
|
|
11414
12646
|
this.idGenerator = this.configureIdGenerator(customIdGenerator, idSize);
|
|
11415
12647
|
if (typeof customIdGenerator === "number" && customIdGenerator > 0) {
|
|
@@ -11657,9 +12889,12 @@ ${errorDetails}`,
|
|
|
11657
12889
|
}
|
|
11658
12890
|
/**
|
|
11659
12891
|
* Validate that all partition fields exist in current resource attributes
|
|
11660
|
-
* @throws {Error} If partition fields don't exist in current schema
|
|
12892
|
+
* @throws {Error} If partition fields don't exist in current schema (only when strictValidation is true)
|
|
11661
12893
|
*/
|
|
11662
12894
|
validatePartitions() {
|
|
12895
|
+
if (!this.strictValidation) {
|
|
12896
|
+
return;
|
|
12897
|
+
}
|
|
11663
12898
|
if (!this.config.partitions) {
|
|
11664
12899
|
return;
|
|
11665
12900
|
}
|
|
@@ -13794,7 +15029,7 @@ class Database extends EventEmitter {
|
|
|
13794
15029
|
this.id = idGenerator(7);
|
|
13795
15030
|
this.version = "1";
|
|
13796
15031
|
this.s3dbVersion = (() => {
|
|
13797
|
-
const [ok, err, version] = tryFn(() => true ? "11.2.
|
|
15032
|
+
const [ok, err, version] = tryFn(() => true ? "11.2.4" : "latest");
|
|
13798
15033
|
return ok ? version : "latest";
|
|
13799
15034
|
})();
|
|
13800
15035
|
this.resources = {};
|
|
@@ -13809,6 +15044,7 @@ class Database extends EventEmitter {
|
|
|
13809
15044
|
this.passphrase = options.passphrase || "secret";
|
|
13810
15045
|
this.versioningEnabled = options.versioningEnabled || false;
|
|
13811
15046
|
this.persistHooks = options.persistHooks || false;
|
|
15047
|
+
this.strictValidation = options.strictValidation !== false;
|
|
13812
15048
|
this._initHooks();
|
|
13813
15049
|
let connectionString = options.connectionString;
|
|
13814
15050
|
if (!connectionString && (options.bucket || options.accessKeyId || options.secretAccessKey)) {
|
|
@@ -13930,6 +15166,7 @@ class Database extends EventEmitter {
|
|
|
13930
15166
|
asyncEvents: versionData.asyncEvents !== void 0 ? versionData.asyncEvents : true,
|
|
13931
15167
|
hooks: this.persistHooks ? this._deserializeHooks(versionData.hooks || {}) : versionData.hooks || {},
|
|
13932
15168
|
versioningEnabled: this.versioningEnabled,
|
|
15169
|
+
strictValidation: this.strictValidation,
|
|
13933
15170
|
map: versionData.map,
|
|
13934
15171
|
idGenerator: restoredIdGenerator,
|
|
13935
15172
|
idSize: restoredIdSize
|
|
@@ -14137,7 +15374,12 @@ class Database extends EventEmitter {
|
|
|
14137
15374
|
const pluginName = name.toLowerCase().replace("plugin", "");
|
|
14138
15375
|
const plugin = this.plugins[pluginName] || this.pluginRegistry[pluginName];
|
|
14139
15376
|
if (!plugin) {
|
|
14140
|
-
throw new
|
|
15377
|
+
throw new DatabaseError(`Plugin '${name}' not found`, {
|
|
15378
|
+
operation: "uninstallPlugin",
|
|
15379
|
+
pluginName: name,
|
|
15380
|
+
availablePlugins: Object.keys(this.pluginRegistry),
|
|
15381
|
+
suggestion: "Check plugin name or list available plugins using Object.keys(db.pluginRegistry)"
|
|
15382
|
+
});
|
|
14141
15383
|
}
|
|
14142
15384
|
if (plugin.stop) {
|
|
14143
15385
|
await plugin.stop();
|
|
@@ -14591,6 +15833,7 @@ class Database extends EventEmitter {
|
|
|
14591
15833
|
autoDecrypt: config.autoDecrypt !== void 0 ? config.autoDecrypt : true,
|
|
14592
15834
|
hooks: hooks || {},
|
|
14593
15835
|
versioningEnabled: this.versioningEnabled,
|
|
15836
|
+
strictValidation: this.strictValidation,
|
|
14594
15837
|
map: config.map,
|
|
14595
15838
|
idGenerator: config.idGenerator,
|
|
14596
15839
|
idSize: config.idSize,
|
|
@@ -14769,10 +16012,20 @@ class Database extends EventEmitter {
|
|
|
14769
16012
|
addHook(event, fn) {
|
|
14770
16013
|
if (!this._hooks) this._initHooks();
|
|
14771
16014
|
if (!this._hooks.has(event)) {
|
|
14772
|
-
throw new
|
|
16015
|
+
throw new DatabaseError(`Unknown hook event: ${event}`, {
|
|
16016
|
+
operation: "addHook",
|
|
16017
|
+
invalidEvent: event,
|
|
16018
|
+
availableEvents: this._hookEvents,
|
|
16019
|
+
suggestion: `Use one of the available hook events: ${this._hookEvents.join(", ")}`
|
|
16020
|
+
});
|
|
14773
16021
|
}
|
|
14774
16022
|
if (typeof fn !== "function") {
|
|
14775
|
-
throw new
|
|
16023
|
+
throw new DatabaseError("Hook function must be a function", {
|
|
16024
|
+
operation: "addHook",
|
|
16025
|
+
event,
|
|
16026
|
+
receivedType: typeof fn,
|
|
16027
|
+
suggestion: "Provide a function that will be called when the hook event occurs"
|
|
16028
|
+
});
|
|
14776
16029
|
}
|
|
14777
16030
|
this._hooks.get(event).push(fn);
|
|
14778
16031
|
}
|
|
@@ -14910,7 +16163,11 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
14910
16163
|
this.targetDatabase = new S3db(targetConfig);
|
|
14911
16164
|
await this.targetDatabase.connect();
|
|
14912
16165
|
} else {
|
|
14913
|
-
throw new
|
|
16166
|
+
throw new ReplicationError("S3dbReplicator requires client or connectionString", {
|
|
16167
|
+
operation: "initialize",
|
|
16168
|
+
replicatorClass: "S3dbReplicator",
|
|
16169
|
+
suggestion: 'Provide either a client instance or connectionString in config: { client: db } or { connectionString: "s3://..." }'
|
|
16170
|
+
});
|
|
14914
16171
|
}
|
|
14915
16172
|
this.emit("connected", {
|
|
14916
16173
|
replicator: this.name,
|
|
@@ -14941,7 +16198,13 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
14941
16198
|
const normResource = normalizeResourceName$1(resource);
|
|
14942
16199
|
const entry = this.resourcesMap[normResource];
|
|
14943
16200
|
if (!entry) {
|
|
14944
|
-
throw new
|
|
16201
|
+
throw new ReplicationError("Resource not configured for replication", {
|
|
16202
|
+
operation: "replicate",
|
|
16203
|
+
replicatorClass: "S3dbReplicator",
|
|
16204
|
+
resourceName: resource,
|
|
16205
|
+
configuredResources: Object.keys(this.resourcesMap),
|
|
16206
|
+
suggestion: 'Add resource to replicator resources map: { resources: { [resourceName]: "destination" } }'
|
|
16207
|
+
});
|
|
14945
16208
|
}
|
|
14946
16209
|
if (Array.isArray(entry)) {
|
|
14947
16210
|
const results = [];
|
|
@@ -15009,7 +16272,14 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
15009
16272
|
} else if (operation === "delete") {
|
|
15010
16273
|
result = await destResourceObj.delete(recordId);
|
|
15011
16274
|
} else {
|
|
15012
|
-
throw new
|
|
16275
|
+
throw new ReplicationError(`Invalid replication operation: ${operation}`, {
|
|
16276
|
+
operation: "replicate",
|
|
16277
|
+
replicatorClass: "S3dbReplicator",
|
|
16278
|
+
invalidOperation: operation,
|
|
16279
|
+
supportedOperations: ["insert", "update", "delete"],
|
|
16280
|
+
resourceName: sourceResource,
|
|
16281
|
+
suggestion: "Use one of the supported operations: insert, update, delete"
|
|
16282
|
+
});
|
|
15013
16283
|
}
|
|
15014
16284
|
return result;
|
|
15015
16285
|
}
|
|
@@ -15077,7 +16347,13 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
15077
16347
|
const norm = normalizeResourceName$1(resource);
|
|
15078
16348
|
const found = available.find((r) => normalizeResourceName$1(r) === norm);
|
|
15079
16349
|
if (!found) {
|
|
15080
|
-
throw new
|
|
16350
|
+
throw new ReplicationError("Destination resource not found in target database", {
|
|
16351
|
+
operation: "_getDestResourceObj",
|
|
16352
|
+
replicatorClass: "S3dbReplicator",
|
|
16353
|
+
destinationResource: resource,
|
|
16354
|
+
availableResources: available,
|
|
16355
|
+
suggestion: "Create the resource in target database or check resource name spelling"
|
|
16356
|
+
});
|
|
15081
16357
|
}
|
|
15082
16358
|
return db.resources[found];
|
|
15083
16359
|
}
|
|
@@ -15126,7 +16402,13 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
15126
16402
|
}
|
|
15127
16403
|
async testConnection() {
|
|
15128
16404
|
const [ok, err] = await tryFn(async () => {
|
|
15129
|
-
if (!this.targetDatabase)
|
|
16405
|
+
if (!this.targetDatabase) {
|
|
16406
|
+
throw new ReplicationError("No target database configured for connection test", {
|
|
16407
|
+
operation: "testConnection",
|
|
16408
|
+
replicatorClass: "S3dbReplicator",
|
|
16409
|
+
suggestion: "Initialize replicator with client or connectionString before testing connection"
|
|
16410
|
+
});
|
|
16411
|
+
}
|
|
15130
16412
|
if (typeof this.targetDatabase.connect === "function") {
|
|
15131
16413
|
await this.targetDatabase.connect();
|
|
15132
16414
|
}
|
|
@@ -15513,7 +16795,12 @@ const REPLICATOR_DRIVERS = {
|
|
|
15513
16795
|
function createReplicator(driver, config = {}, resources = [], client = null) {
|
|
15514
16796
|
const ReplicatorClass = REPLICATOR_DRIVERS[driver];
|
|
15515
16797
|
if (!ReplicatorClass) {
|
|
15516
|
-
throw new
|
|
16798
|
+
throw new ReplicationError(`Unknown replicator driver: ${driver}`, {
|
|
16799
|
+
operation: "createReplicator",
|
|
16800
|
+
driver,
|
|
16801
|
+
availableDrivers: Object.keys(REPLICATOR_DRIVERS),
|
|
16802
|
+
suggestion: `Use one of the available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(", ")}`
|
|
16803
|
+
});
|
|
15517
16804
|
}
|
|
15518
16805
|
return new ReplicatorClass(config, resources, client);
|
|
15519
16806
|
}
|
|
@@ -15525,12 +16812,40 @@ class ReplicatorPlugin extends Plugin {
|
|
|
15525
16812
|
constructor(options = {}) {
|
|
15526
16813
|
super();
|
|
15527
16814
|
if (!options.replicators || !Array.isArray(options.replicators)) {
|
|
15528
|
-
throw new
|
|
16815
|
+
throw new ReplicationError("ReplicatorPlugin requires replicators array", {
|
|
16816
|
+
operation: "constructor",
|
|
16817
|
+
pluginName: "ReplicatorPlugin",
|
|
16818
|
+
providedOptions: Object.keys(options),
|
|
16819
|
+
suggestion: 'Provide replicators array: new ReplicatorPlugin({ replicators: [{ driver: "s3db", resources: [...] }] })'
|
|
16820
|
+
});
|
|
15529
16821
|
}
|
|
15530
16822
|
for (const rep of options.replicators) {
|
|
15531
|
-
if (!rep.driver)
|
|
15532
|
-
|
|
15533
|
-
|
|
16823
|
+
if (!rep.driver) {
|
|
16824
|
+
throw new ReplicationError("Each replicator must have a driver", {
|
|
16825
|
+
operation: "constructor",
|
|
16826
|
+
pluginName: "ReplicatorPlugin",
|
|
16827
|
+
replicatorConfig: rep,
|
|
16828
|
+
suggestion: 'Each replicator entry must specify a driver: { driver: "s3db", resources: {...} }'
|
|
16829
|
+
});
|
|
16830
|
+
}
|
|
16831
|
+
if (!rep.resources || typeof rep.resources !== "object") {
|
|
16832
|
+
throw new ReplicationError("Each replicator must have resources config", {
|
|
16833
|
+
operation: "constructor",
|
|
16834
|
+
pluginName: "ReplicatorPlugin",
|
|
16835
|
+
driver: rep.driver,
|
|
16836
|
+
replicatorConfig: rep,
|
|
16837
|
+
suggestion: 'Provide resources as object or array: { driver: "s3db", resources: ["users"] } or { resources: { users: "people" } }'
|
|
16838
|
+
});
|
|
16839
|
+
}
|
|
16840
|
+
if (Object.keys(rep.resources).length === 0) {
|
|
16841
|
+
throw new ReplicationError("Each replicator must have at least one resource configured", {
|
|
16842
|
+
operation: "constructor",
|
|
16843
|
+
pluginName: "ReplicatorPlugin",
|
|
16844
|
+
driver: rep.driver,
|
|
16845
|
+
replicatorConfig: rep,
|
|
16846
|
+
suggestion: 'Add at least one resource to replicate: { driver: "s3db", resources: ["users"] }'
|
|
16847
|
+
});
|
|
16848
|
+
}
|
|
15534
16849
|
}
|
|
15535
16850
|
this.config = {
|
|
15536
16851
|
replicators: options.replicators || [],
|
|
@@ -15956,7 +17271,13 @@ class ReplicatorPlugin extends Plugin {
|
|
|
15956
17271
|
async syncAllData(replicatorId) {
|
|
15957
17272
|
const replicator = this.replicators.find((r) => r.id === replicatorId);
|
|
15958
17273
|
if (!replicator) {
|
|
15959
|
-
throw new
|
|
17274
|
+
throw new ReplicationError("Replicator not found", {
|
|
17275
|
+
operation: "syncAllData",
|
|
17276
|
+
pluginName: "ReplicatorPlugin",
|
|
17277
|
+
replicatorId,
|
|
17278
|
+
availableReplicators: this.replicators.map((r) => r.id),
|
|
17279
|
+
suggestion: "Check replicator ID or use getReplicatorStats() to list available replicators"
|
|
17280
|
+
});
|
|
15960
17281
|
}
|
|
15961
17282
|
this.stats.lastSync = (/* @__PURE__ */ new Date()).toISOString();
|
|
15962
17283
|
for (const resourceName in this.database.resources) {
|
|
@@ -16486,6 +17807,35 @@ class S3QueuePlugin extends Plugin {
|
|
|
16486
17807
|
}
|
|
16487
17808
|
}
|
|
16488
17809
|
|
|
17810
|
+
class SchedulerError extends S3dbError {
|
|
17811
|
+
constructor(message, details = {}) {
|
|
17812
|
+
const { taskId, operation = "unknown", cronExpression, ...rest } = details;
|
|
17813
|
+
let description = details.description;
|
|
17814
|
+
if (!description) {
|
|
17815
|
+
description = `
|
|
17816
|
+
Scheduler Operation Error
|
|
17817
|
+
|
|
17818
|
+
Operation: ${operation}
|
|
17819
|
+
${taskId ? `Task ID: ${taskId}` : ""}
|
|
17820
|
+
${cronExpression ? `Cron: ${cronExpression}` : ""}
|
|
17821
|
+
|
|
17822
|
+
Common causes:
|
|
17823
|
+
1. Invalid cron expression format
|
|
17824
|
+
2. Task not found or already exists
|
|
17825
|
+
3. Scheduler not properly initialized
|
|
17826
|
+
4. Job execution failure
|
|
17827
|
+
5. Resource conflicts
|
|
17828
|
+
|
|
17829
|
+
Solution:
|
|
17830
|
+
Check task configuration and ensure scheduler is properly initialized.
|
|
17831
|
+
|
|
17832
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/scheduler.md
|
|
17833
|
+
`.trim();
|
|
17834
|
+
}
|
|
17835
|
+
super(message, { ...rest, taskId, operation, cronExpression, description });
|
|
17836
|
+
}
|
|
17837
|
+
}
|
|
17838
|
+
|
|
16489
17839
|
class SchedulerPlugin extends Plugin {
|
|
16490
17840
|
constructor(options = {}) {
|
|
16491
17841
|
super();
|
|
@@ -16519,17 +17869,36 @@ class SchedulerPlugin extends Plugin {
|
|
|
16519
17869
|
}
|
|
16520
17870
|
_validateConfiguration() {
|
|
16521
17871
|
if (Object.keys(this.config.jobs).length === 0) {
|
|
16522
|
-
throw new
|
|
17872
|
+
throw new SchedulerError("At least one job must be defined", {
|
|
17873
|
+
operation: "validateConfiguration",
|
|
17874
|
+
jobCount: 0,
|
|
17875
|
+
suggestion: 'Provide at least one job in the jobs configuration: { jobs: { myJob: { schedule: "* * * * *", action: async () => {...} } } }'
|
|
17876
|
+
});
|
|
16523
17877
|
}
|
|
16524
17878
|
for (const [jobName, job] of Object.entries(this.config.jobs)) {
|
|
16525
17879
|
if (!job.schedule) {
|
|
16526
|
-
throw new
|
|
17880
|
+
throw new SchedulerError(`Job '${jobName}' must have a schedule`, {
|
|
17881
|
+
operation: "validateConfiguration",
|
|
17882
|
+
taskId: jobName,
|
|
17883
|
+
providedConfig: Object.keys(job),
|
|
17884
|
+
suggestion: 'Add a schedule property with a valid cron expression: { schedule: "0 * * * *", action: async () => {...} }'
|
|
17885
|
+
});
|
|
16527
17886
|
}
|
|
16528
17887
|
if (!job.action || typeof job.action !== "function") {
|
|
16529
|
-
throw new
|
|
17888
|
+
throw new SchedulerError(`Job '${jobName}' must have an action function`, {
|
|
17889
|
+
operation: "validateConfiguration",
|
|
17890
|
+
taskId: jobName,
|
|
17891
|
+
actionType: typeof job.action,
|
|
17892
|
+
suggestion: 'Provide an action function: { schedule: "...", action: async (db, ctx) => {...} }'
|
|
17893
|
+
});
|
|
16530
17894
|
}
|
|
16531
17895
|
if (!this._isValidCronExpression(job.schedule)) {
|
|
16532
|
-
throw new
|
|
17896
|
+
throw new SchedulerError(`Job '${jobName}' has invalid cron expression`, {
|
|
17897
|
+
operation: "validateConfiguration",
|
|
17898
|
+
taskId: jobName,
|
|
17899
|
+
cronExpression: job.schedule,
|
|
17900
|
+
suggestion: "Use valid cron format (5 fields: minute hour day month weekday) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
|
|
17901
|
+
});
|
|
16533
17902
|
}
|
|
16534
17903
|
}
|
|
16535
17904
|
}
|
|
@@ -16827,10 +18196,20 @@ class SchedulerPlugin extends Plugin {
|
|
|
16827
18196
|
async runJob(jobName, context = {}) {
|
|
16828
18197
|
const job = this.jobs.get(jobName);
|
|
16829
18198
|
if (!job) {
|
|
16830
|
-
throw new
|
|
18199
|
+
throw new SchedulerError(`Job '${jobName}' not found`, {
|
|
18200
|
+
operation: "runJob",
|
|
18201
|
+
taskId: jobName,
|
|
18202
|
+
availableJobs: Array.from(this.jobs.keys()),
|
|
18203
|
+
suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
|
|
18204
|
+
});
|
|
16831
18205
|
}
|
|
16832
18206
|
if (this.activeJobs.has(jobName)) {
|
|
16833
|
-
throw new
|
|
18207
|
+
throw new SchedulerError(`Job '${jobName}' is already running`, {
|
|
18208
|
+
operation: "runJob",
|
|
18209
|
+
taskId: jobName,
|
|
18210
|
+
executionId: this.activeJobs.get(jobName),
|
|
18211
|
+
suggestion: "Wait for current execution to complete or check job status with getJobStatus()"
|
|
18212
|
+
});
|
|
16834
18213
|
}
|
|
16835
18214
|
await this._executeJob(jobName);
|
|
16836
18215
|
}
|
|
@@ -16840,7 +18219,12 @@ class SchedulerPlugin extends Plugin {
|
|
|
16840
18219
|
enableJob(jobName) {
|
|
16841
18220
|
const job = this.jobs.get(jobName);
|
|
16842
18221
|
if (!job) {
|
|
16843
|
-
throw new
|
|
18222
|
+
throw new SchedulerError(`Job '${jobName}' not found`, {
|
|
18223
|
+
operation: "enableJob",
|
|
18224
|
+
taskId: jobName,
|
|
18225
|
+
availableJobs: Array.from(this.jobs.keys()),
|
|
18226
|
+
suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
|
|
18227
|
+
});
|
|
16844
18228
|
}
|
|
16845
18229
|
job.enabled = true;
|
|
16846
18230
|
this._scheduleNextExecution(jobName);
|
|
@@ -16852,7 +18236,12 @@ class SchedulerPlugin extends Plugin {
|
|
|
16852
18236
|
disableJob(jobName) {
|
|
16853
18237
|
const job = this.jobs.get(jobName);
|
|
16854
18238
|
if (!job) {
|
|
16855
|
-
throw new
|
|
18239
|
+
throw new SchedulerError(`Job '${jobName}' not found`, {
|
|
18240
|
+
operation: "disableJob",
|
|
18241
|
+
taskId: jobName,
|
|
18242
|
+
availableJobs: Array.from(this.jobs.keys()),
|
|
18243
|
+
suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
|
|
18244
|
+
});
|
|
16856
18245
|
}
|
|
16857
18246
|
job.enabled = false;
|
|
16858
18247
|
const timer = this.timers.get(jobName);
|
|
@@ -16951,13 +18340,28 @@ class SchedulerPlugin extends Plugin {
|
|
|
16951
18340
|
*/
|
|
16952
18341
|
addJob(jobName, jobConfig) {
|
|
16953
18342
|
if (this.jobs.has(jobName)) {
|
|
16954
|
-
throw new
|
|
18343
|
+
throw new SchedulerError(`Job '${jobName}' already exists`, {
|
|
18344
|
+
operation: "addJob",
|
|
18345
|
+
taskId: jobName,
|
|
18346
|
+
existingJobs: Array.from(this.jobs.keys()),
|
|
18347
|
+
suggestion: "Use a different job name or remove the existing job first with removeJob()"
|
|
18348
|
+
});
|
|
16955
18349
|
}
|
|
16956
18350
|
if (!jobConfig.schedule || !jobConfig.action) {
|
|
16957
|
-
throw new
|
|
18351
|
+
throw new SchedulerError("Job must have schedule and action", {
|
|
18352
|
+
operation: "addJob",
|
|
18353
|
+
taskId: jobName,
|
|
18354
|
+
providedConfig: Object.keys(jobConfig),
|
|
18355
|
+
suggestion: 'Provide both schedule and action: { schedule: "0 * * * *", action: async (db, ctx) => {...} }'
|
|
18356
|
+
});
|
|
16958
18357
|
}
|
|
16959
18358
|
if (!this._isValidCronExpression(jobConfig.schedule)) {
|
|
16960
|
-
throw new
|
|
18359
|
+
throw new SchedulerError("Invalid cron expression", {
|
|
18360
|
+
operation: "addJob",
|
|
18361
|
+
taskId: jobName,
|
|
18362
|
+
cronExpression: jobConfig.schedule,
|
|
18363
|
+
suggestion: "Use valid cron format (5 fields) or shortcuts (@hourly, @daily, @weekly, @monthly, @yearly)"
|
|
18364
|
+
});
|
|
16961
18365
|
}
|
|
16962
18366
|
const job = {
|
|
16963
18367
|
...jobConfig,
|
|
@@ -16991,7 +18395,12 @@ class SchedulerPlugin extends Plugin {
|
|
|
16991
18395
|
removeJob(jobName) {
|
|
16992
18396
|
const job = this.jobs.get(jobName);
|
|
16993
18397
|
if (!job) {
|
|
16994
|
-
throw new
|
|
18398
|
+
throw new SchedulerError(`Job '${jobName}' not found`, {
|
|
18399
|
+
operation: "removeJob",
|
|
18400
|
+
taskId: jobName,
|
|
18401
|
+
availableJobs: Array.from(this.jobs.keys()),
|
|
18402
|
+
suggestion: "Check job name or use getAllJobsStatus() to list available jobs"
|
|
18403
|
+
});
|
|
16995
18404
|
}
|
|
16996
18405
|
const timer = this.timers.get(jobName);
|
|
16997
18406
|
if (timer) {
|
|
@@ -17045,6 +18454,36 @@ class SchedulerPlugin extends Plugin {
|
|
|
17045
18454
|
}
|
|
17046
18455
|
}
|
|
17047
18456
|
|
|
18457
|
+
class StateMachineError extends S3dbError {
|
|
18458
|
+
constructor(message, details = {}) {
|
|
18459
|
+
const { currentState, targetState, resourceName, operation = "unknown", ...rest } = details;
|
|
18460
|
+
let description = details.description;
|
|
18461
|
+
if (!description) {
|
|
18462
|
+
description = `
|
|
18463
|
+
State Machine Operation Error
|
|
18464
|
+
|
|
18465
|
+
Operation: ${operation}
|
|
18466
|
+
${currentState ? `Current State: ${currentState}` : ""}
|
|
18467
|
+
${targetState ? `Target State: ${targetState}` : ""}
|
|
18468
|
+
${resourceName ? `Resource: ${resourceName}` : ""}
|
|
18469
|
+
|
|
18470
|
+
Common causes:
|
|
18471
|
+
1. Invalid state transition
|
|
18472
|
+
2. State machine not configured
|
|
18473
|
+
3. Transition conditions not met
|
|
18474
|
+
4. State not defined in configuration
|
|
18475
|
+
5. Missing transition handler
|
|
18476
|
+
|
|
18477
|
+
Solution:
|
|
18478
|
+
Check state machine configuration and valid transitions.
|
|
18479
|
+
|
|
18480
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/state-machine.md
|
|
18481
|
+
`.trim();
|
|
18482
|
+
}
|
|
18483
|
+
super(message, { ...rest, currentState, targetState, resourceName, operation, description });
|
|
18484
|
+
}
|
|
18485
|
+
}
|
|
18486
|
+
|
|
17048
18487
|
class StateMachinePlugin extends Plugin {
|
|
17049
18488
|
constructor(options = {}) {
|
|
17050
18489
|
super();
|
|
@@ -17065,17 +18504,36 @@ class StateMachinePlugin extends Plugin {
|
|
|
17065
18504
|
}
|
|
17066
18505
|
_validateConfiguration() {
|
|
17067
18506
|
if (!this.config.stateMachines || Object.keys(this.config.stateMachines).length === 0) {
|
|
17068
|
-
throw new
|
|
18507
|
+
throw new StateMachineError("At least one state machine must be defined", {
|
|
18508
|
+
operation: "validateConfiguration",
|
|
18509
|
+
machineCount: 0,
|
|
18510
|
+
suggestion: "Provide at least one state machine in the stateMachines configuration"
|
|
18511
|
+
});
|
|
17069
18512
|
}
|
|
17070
18513
|
for (const [machineName, machine] of Object.entries(this.config.stateMachines)) {
|
|
17071
18514
|
if (!machine.states || Object.keys(machine.states).length === 0) {
|
|
17072
|
-
throw new
|
|
18515
|
+
throw new StateMachineError(`Machine '${machineName}' must have states defined`, {
|
|
18516
|
+
operation: "validateConfiguration",
|
|
18517
|
+
machineId: machineName,
|
|
18518
|
+
suggestion: "Define at least one state in the states configuration"
|
|
18519
|
+
});
|
|
17073
18520
|
}
|
|
17074
18521
|
if (!machine.initialState) {
|
|
17075
|
-
throw new
|
|
18522
|
+
throw new StateMachineError(`Machine '${machineName}' must have an initialState`, {
|
|
18523
|
+
operation: "validateConfiguration",
|
|
18524
|
+
machineId: machineName,
|
|
18525
|
+
availableStates: Object.keys(machine.states),
|
|
18526
|
+
suggestion: "Specify an initialState property matching one of the defined states"
|
|
18527
|
+
});
|
|
17076
18528
|
}
|
|
17077
18529
|
if (!machine.states[machine.initialState]) {
|
|
17078
|
-
throw new
|
|
18530
|
+
throw new StateMachineError(`Initial state '${machine.initialState}' not found in machine '${machineName}'`, {
|
|
18531
|
+
operation: "validateConfiguration",
|
|
18532
|
+
machineId: machineName,
|
|
18533
|
+
initialState: machine.initialState,
|
|
18534
|
+
availableStates: Object.keys(machine.states),
|
|
18535
|
+
suggestion: "Set initialState to one of the defined states"
|
|
18536
|
+
});
|
|
17079
18537
|
}
|
|
17080
18538
|
}
|
|
17081
18539
|
}
|
|
@@ -17132,12 +18590,25 @@ class StateMachinePlugin extends Plugin {
|
|
|
17132
18590
|
async send(machineId, entityId, event, context = {}) {
|
|
17133
18591
|
const machine = this.machines.get(machineId);
|
|
17134
18592
|
if (!machine) {
|
|
17135
|
-
throw new
|
|
18593
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18594
|
+
operation: "send",
|
|
18595
|
+
machineId,
|
|
18596
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18597
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18598
|
+
});
|
|
17136
18599
|
}
|
|
17137
18600
|
const currentState = await this.getState(machineId, entityId);
|
|
17138
18601
|
const stateConfig = machine.config.states[currentState];
|
|
17139
18602
|
if (!stateConfig || !stateConfig.on || !stateConfig.on[event]) {
|
|
17140
|
-
throw new
|
|
18603
|
+
throw new StateMachineError(`Event '${event}' not valid for state '${currentState}' in machine '${machineId}'`, {
|
|
18604
|
+
operation: "send",
|
|
18605
|
+
machineId,
|
|
18606
|
+
entityId,
|
|
18607
|
+
event,
|
|
18608
|
+
currentState,
|
|
18609
|
+
validEvents: stateConfig && stateConfig.on ? Object.keys(stateConfig.on) : [],
|
|
18610
|
+
suggestion: "Use getValidEvents() to check which events are valid for the current state"
|
|
18611
|
+
});
|
|
17141
18612
|
}
|
|
17142
18613
|
const targetState = stateConfig.on[event];
|
|
17143
18614
|
if (stateConfig.guards && stateConfig.guards[event]) {
|
|
@@ -17148,7 +18619,16 @@ class StateMachinePlugin extends Plugin {
|
|
|
17148
18619
|
() => guard(context, event, { database: this.database, machineId, entityId })
|
|
17149
18620
|
);
|
|
17150
18621
|
if (!guardOk || !guardResult) {
|
|
17151
|
-
throw new
|
|
18622
|
+
throw new StateMachineError(`Transition blocked by guard '${guardName}'`, {
|
|
18623
|
+
operation: "send",
|
|
18624
|
+
machineId,
|
|
18625
|
+
entityId,
|
|
18626
|
+
event,
|
|
18627
|
+
currentState,
|
|
18628
|
+
guardName,
|
|
18629
|
+
guardError: guardErr?.message || "Guard returned false",
|
|
18630
|
+
suggestion: "Check guard conditions or modify the context to satisfy guard requirements"
|
|
18631
|
+
});
|
|
17152
18632
|
}
|
|
17153
18633
|
}
|
|
17154
18634
|
}
|
|
@@ -17258,7 +18738,12 @@ class StateMachinePlugin extends Plugin {
|
|
|
17258
18738
|
async getState(machineId, entityId) {
|
|
17259
18739
|
const machine = this.machines.get(machineId);
|
|
17260
18740
|
if (!machine) {
|
|
17261
|
-
throw new
|
|
18741
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18742
|
+
operation: "getState",
|
|
18743
|
+
machineId,
|
|
18744
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18745
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18746
|
+
});
|
|
17262
18747
|
}
|
|
17263
18748
|
if (machine.currentStates.has(entityId)) {
|
|
17264
18749
|
return machine.currentStates.get(entityId);
|
|
@@ -17284,7 +18769,12 @@ class StateMachinePlugin extends Plugin {
|
|
|
17284
18769
|
async getValidEvents(machineId, stateOrEntityId) {
|
|
17285
18770
|
const machine = this.machines.get(machineId);
|
|
17286
18771
|
if (!machine) {
|
|
17287
|
-
throw new
|
|
18772
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18773
|
+
operation: "getValidEvents",
|
|
18774
|
+
machineId,
|
|
18775
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18776
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18777
|
+
});
|
|
17288
18778
|
}
|
|
17289
18779
|
let state;
|
|
17290
18780
|
if (machine.config.states[stateOrEntityId]) {
|
|
@@ -17333,7 +18823,12 @@ class StateMachinePlugin extends Plugin {
|
|
|
17333
18823
|
async initializeEntity(machineId, entityId, context = {}) {
|
|
17334
18824
|
const machine = this.machines.get(machineId);
|
|
17335
18825
|
if (!machine) {
|
|
17336
|
-
throw new
|
|
18826
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18827
|
+
operation: "initializeEntity",
|
|
18828
|
+
machineId,
|
|
18829
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18830
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18831
|
+
});
|
|
17337
18832
|
}
|
|
17338
18833
|
const initialState = machine.config.initialState;
|
|
17339
18834
|
machine.currentStates.set(entityId, initialState);
|
|
@@ -17352,7 +18847,14 @@ class StateMachinePlugin extends Plugin {
|
|
|
17352
18847
|
})
|
|
17353
18848
|
);
|
|
17354
18849
|
if (!ok && err && !err.message?.includes("already exists")) {
|
|
17355
|
-
throw new
|
|
18850
|
+
throw new StateMachineError("Failed to initialize entity state", {
|
|
18851
|
+
operation: "initializeEntity",
|
|
18852
|
+
machineId,
|
|
18853
|
+
entityId,
|
|
18854
|
+
initialState,
|
|
18855
|
+
original: err,
|
|
18856
|
+
suggestion: "Check state resource configuration and database permissions"
|
|
18857
|
+
});
|
|
17356
18858
|
}
|
|
17357
18859
|
}
|
|
17358
18860
|
const initialStateConfig = machine.config.states[initialState];
|
|
@@ -17381,7 +18883,12 @@ class StateMachinePlugin extends Plugin {
|
|
|
17381
18883
|
visualize(machineId) {
|
|
17382
18884
|
const machine = this.machines.get(machineId);
|
|
17383
18885
|
if (!machine) {
|
|
17384
|
-
throw new
|
|
18886
|
+
throw new StateMachineError(`State machine '${machineId}' not found`, {
|
|
18887
|
+
operation: "visualize",
|
|
18888
|
+
machineId,
|
|
18889
|
+
availableMachines: Array.from(this.machines.keys()),
|
|
18890
|
+
suggestion: "Check machine ID or use getMachines() to list available machines"
|
|
18891
|
+
});
|
|
17385
18892
|
}
|
|
17386
18893
|
let dot = `digraph ${machineId} {
|
|
17387
18894
|
`;
|
|
@@ -17425,5 +18932,5 @@ class StateMachinePlugin extends Plugin {
|
|
|
17425
18932
|
}
|
|
17426
18933
|
}
|
|
17427
18934
|
|
|
17428
|
-
export { AVAILABLE_BEHAVIORS, AuditPlugin, AuthenticationError, BackupPlugin, BaseError, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, EncryptionError, ErrorMap, EventualConsistencyPlugin, FullTextPlugin, InvalidResourceItem, MetricsPlugin, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, PartitionError, PermissionError, Plugin, PluginObject, QueueConsumerPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3QueuePlugin, Database as S3db, S3dbError, SchedulerPlugin, Schema, SchemaError, StateMachinePlugin, UnknownError, ValidationError, Validator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes, clearUTF8Cache, clearUTF8Memo, clearUTF8Memory, decode, decodeDecimal, decrypt, S3db as default, encode, encodeDecimal, encrypt, getBehavior, getSizeBreakdown, idGenerator, mapAwsError, md5, passwordGenerator, sha256, streamToString, transformValue, tryFn, tryFnSync };
|
|
18935
|
+
export { AVAILABLE_BEHAVIORS, AnalyticsNotEnabledError, AuditPlugin, AuthenticationError, BackupPlugin, BaseError, BehaviorError, CachePlugin, Client, ConnectionString, ConnectionStringError, CostsPlugin, CryptoError, DEFAULT_BEHAVIOR, Database, DatabaseError, EncryptionError, ErrorMap, EventualConsistencyPlugin, FullTextPlugin, InvalidResourceItem, MetadataLimitError, MetricsPlugin, MissingMetadata, NoSuchBucket, NoSuchKey, NotFound, PartitionDriverError, PartitionError, PermissionError, Plugin, PluginError, PluginObject, PluginStorageError, QueueConsumerPlugin, ReplicatorPlugin, Resource, ResourceError, ResourceIdsPageReader, ResourceIdsReader, ResourceNotFound, ResourceReader, ResourceWriter, S3QueuePlugin, Database as S3db, S3dbError, SchedulerPlugin, Schema, SchemaError, StateMachinePlugin, StreamError, UnknownError, ValidationError, Validator, behaviors, calculateAttributeNamesSize, calculateAttributeSizes, calculateEffectiveLimit, calculateSystemOverhead, calculateTotalSize, calculateUTF8Bytes, clearUTF8Cache, clearUTF8Memo, clearUTF8Memory, decode, decodeDecimal, decrypt, S3db as default, encode, encodeDecimal, encrypt, getBehavior, getSizeBreakdown, idGenerator, mapAwsError, md5, passwordGenerator, sha256, streamToString, transformValue, tryFn, tryFnSync };
|
|
17429
18936
|
//# sourceMappingURL=s3db.es.js.map
|