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
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
runConsolidation
|
|
18
18
|
} from "./consolidation.js";
|
|
19
19
|
import { runGarbageCollection } from "./garbage-collection.js";
|
|
20
|
-
import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getYearByWeek, getMonthByWeek, getMonthByHour, getTopRecords, getYearByDay, getWeekByDay, getWeekByHour, getLastNHours, getLastNWeeks, getLastNMonths } from "./analytics.js";
|
|
20
|
+
import { updateAnalytics, getAnalytics, getMonthByDay, getDayByHour, getLastNDays, getYearByMonth, getYearByWeek, getMonthByWeek, getMonthByHour, getTopRecords, getYearByDay, getWeekByDay, getWeekByHour, getLastNHours, getLastNWeeks, getLastNMonths, getRawEvents } from "./analytics.js";
|
|
21
21
|
import { onInstall, onStart, onStop, watchForResource, completeFieldSetup } from "./install.js";
|
|
22
22
|
|
|
23
23
|
export class EventualConsistencyPlugin extends Plugin {
|
|
@@ -531,6 +531,208 @@ export class EventualConsistencyPlugin extends Plugin {
|
|
|
531
531
|
async getLastNMonths(resourceName, field, months = 12, options = {}) {
|
|
532
532
|
return await getLastNMonths(resourceName, field, months, options, this.fieldHandlers);
|
|
533
533
|
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Get raw transaction events for custom aggregation
|
|
537
|
+
*
|
|
538
|
+
* This method provides direct access to the underlying transaction events,
|
|
539
|
+
* allowing developers to perform custom aggregations beyond the pre-built analytics.
|
|
540
|
+
* Useful for complex queries, custom metrics, or when you need the raw event data.
|
|
541
|
+
*
|
|
542
|
+
* @param {string} resourceName - Resource name
|
|
543
|
+
* @param {string} field - Field name
|
|
544
|
+
* @param {Object} options - Query options
|
|
545
|
+
* @param {string} options.recordId - Filter by specific record ID
|
|
546
|
+
* @param {string} options.startDate - Start date filter (YYYY-MM-DD or YYYY-MM-DDTHH)
|
|
547
|
+
* @param {string} options.endDate - End date filter (YYYY-MM-DD or YYYY-MM-DDTHH)
|
|
548
|
+
* @param {string} options.cohortDate - Filter by cohort date (YYYY-MM-DD)
|
|
549
|
+
* @param {string} options.cohortHour - Filter by cohort hour (YYYY-MM-DDTHH)
|
|
550
|
+
* @param {string} options.cohortMonth - Filter by cohort month (YYYY-MM)
|
|
551
|
+
* @param {boolean} options.applied - Filter by applied status (true/false/undefined for both)
|
|
552
|
+
* @param {string} options.operation - Filter by operation type ('add', 'sub', 'set')
|
|
553
|
+
* @param {number} options.limit - Maximum number of events to return
|
|
554
|
+
* @returns {Promise<Array>} Raw transaction events
|
|
555
|
+
*
|
|
556
|
+
* @example
|
|
557
|
+
* // Get all events for a specific record
|
|
558
|
+
* const events = await plugin.getRawEvents('wallets', 'balance', {
|
|
559
|
+
* recordId: 'wallet1'
|
|
560
|
+
* });
|
|
561
|
+
*
|
|
562
|
+
* @example
|
|
563
|
+
* // Get events for a specific time range
|
|
564
|
+
* const events = await plugin.getRawEvents('wallets', 'balance', {
|
|
565
|
+
* startDate: '2025-10-01',
|
|
566
|
+
* endDate: '2025-10-31'
|
|
567
|
+
* });
|
|
568
|
+
*
|
|
569
|
+
* @example
|
|
570
|
+
* // Get only pending (unapplied) transactions
|
|
571
|
+
* const pending = await plugin.getRawEvents('wallets', 'balance', {
|
|
572
|
+
* applied: false
|
|
573
|
+
* });
|
|
574
|
+
*/
|
|
575
|
+
async getRawEvents(resourceName, field, options = {}) {
|
|
576
|
+
return await getRawEvents(resourceName, field, options, this.fieldHandlers);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Get diagnostics information about the plugin state
|
|
581
|
+
*
|
|
582
|
+
* This method provides comprehensive diagnostic information about the EventualConsistencyPlugin,
|
|
583
|
+
* including configured resources, field handlers, timers, and overall health status.
|
|
584
|
+
* Useful for debugging initialization issues, configuration problems, or runtime errors.
|
|
585
|
+
*
|
|
586
|
+
* @param {Object} options - Diagnostic options
|
|
587
|
+
* @param {string} options.resourceName - Optional: limit diagnostics to specific resource
|
|
588
|
+
* @param {string} options.field - Optional: limit diagnostics to specific field
|
|
589
|
+
* @param {boolean} options.includeStats - Include transaction statistics (default: false)
|
|
590
|
+
* @returns {Promise<Object>} Diagnostic information
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* // Get overall plugin diagnostics
|
|
594
|
+
* const diagnostics = await plugin.getDiagnostics();
|
|
595
|
+
* console.log(diagnostics);
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* // Get diagnostics for specific resource/field with stats
|
|
599
|
+
* const diagnostics = await plugin.getDiagnostics({
|
|
600
|
+
* resourceName: 'wallets',
|
|
601
|
+
* field: 'balance',
|
|
602
|
+
* includeStats: true
|
|
603
|
+
* });
|
|
604
|
+
*/
|
|
605
|
+
async getDiagnostics(options = {}) {
|
|
606
|
+
const { resourceName, field, includeStats = false } = options;
|
|
607
|
+
|
|
608
|
+
const diagnostics = {
|
|
609
|
+
plugin: {
|
|
610
|
+
name: 'EventualConsistencyPlugin',
|
|
611
|
+
initialized: this.database !== null && this.database !== undefined,
|
|
612
|
+
verbose: this.config.verbose || false,
|
|
613
|
+
timezone: this.config.cohort?.timezone || 'UTC',
|
|
614
|
+
consolidation: {
|
|
615
|
+
mode: this.config.consolidation?.mode || 'timer',
|
|
616
|
+
interval: this.config.consolidation?.interval || 60000,
|
|
617
|
+
batchSize: this.config.consolidation?.batchSize || 100
|
|
618
|
+
},
|
|
619
|
+
garbageCollection: {
|
|
620
|
+
enabled: this.config.garbageCollection?.enabled !== false,
|
|
621
|
+
retentionDays: this.config.garbageCollection?.retentionDays || 30,
|
|
622
|
+
interval: this.config.garbageCollection?.interval || 3600000
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
resources: [],
|
|
626
|
+
errors: [],
|
|
627
|
+
warnings: []
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
// Iterate through configured resources
|
|
631
|
+
for (const [resName, resourceHandlers] of this.fieldHandlers.entries()) {
|
|
632
|
+
// Skip if filtering by resource and this isn't it
|
|
633
|
+
if (resourceName && resName !== resourceName) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const resourceDiag = {
|
|
638
|
+
name: resName,
|
|
639
|
+
fields: []
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
for (const [fieldName, handler] of resourceHandlers.entries()) {
|
|
643
|
+
// Skip if filtering by field and this isn't it
|
|
644
|
+
if (field && fieldName !== field) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const fieldDiag = {
|
|
649
|
+
name: fieldName,
|
|
650
|
+
type: handler.type || 'counter',
|
|
651
|
+
analyticsEnabled: handler.analyticsResource !== null && handler.analyticsResource !== undefined,
|
|
652
|
+
resources: {
|
|
653
|
+
transaction: handler.transactionResource?.name || null,
|
|
654
|
+
target: handler.targetResource?.name || null,
|
|
655
|
+
analytics: handler.analyticsResource?.name || null
|
|
656
|
+
},
|
|
657
|
+
timers: {
|
|
658
|
+
consolidation: handler.consolidationTimer !== null && handler.consolidationTimer !== undefined,
|
|
659
|
+
garbageCollection: handler.garbageCollectionTimer !== null && handler.garbageCollectionTimer !== undefined
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
// Check for common issues
|
|
664
|
+
if (!handler.transactionResource) {
|
|
665
|
+
diagnostics.errors.push({
|
|
666
|
+
resource: resName,
|
|
667
|
+
field: fieldName,
|
|
668
|
+
issue: 'Missing transaction resource',
|
|
669
|
+
suggestion: 'Ensure plugin is installed and resources are created after plugin installation'
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (!handler.targetResource) {
|
|
674
|
+
diagnostics.warnings.push({
|
|
675
|
+
resource: resName,
|
|
676
|
+
field: fieldName,
|
|
677
|
+
issue: 'Missing target resource',
|
|
678
|
+
suggestion: 'Target resource may not have been created yet'
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (handler.analyticsResource && !handler.analyticsResource.name) {
|
|
683
|
+
diagnostics.errors.push({
|
|
684
|
+
resource: resName,
|
|
685
|
+
field: fieldName,
|
|
686
|
+
issue: 'Invalid analytics resource',
|
|
687
|
+
suggestion: 'Analytics resource exists but has no name - possible initialization failure'
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Include statistics if requested
|
|
692
|
+
if (includeStats && handler.transactionResource) {
|
|
693
|
+
try {
|
|
694
|
+
const [okPending, errPending, pendingTxns] = await handler.transactionResource.query({ applied: false }).catch(() => [false, null, []]);
|
|
695
|
+
const [okApplied, errApplied, appliedTxns] = await handler.transactionResource.query({ applied: true }).catch(() => [false, null, []]);
|
|
696
|
+
|
|
697
|
+
fieldDiag.stats = {
|
|
698
|
+
pendingTransactions: okPending ? (pendingTxns?.length || 0) : 'error',
|
|
699
|
+
appliedTransactions: okApplied ? (appliedTxns?.length || 0) : 'error',
|
|
700
|
+
totalTransactions: (okPending && okApplied) ? ((pendingTxns?.length || 0) + (appliedTxns?.length || 0)) : 'error'
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
if (handler.analyticsResource) {
|
|
704
|
+
const [okAnalytics, errAnalytics, analyticsRecords] = await handler.analyticsResource.list().catch(() => [false, null, []]);
|
|
705
|
+
fieldDiag.stats.analyticsRecords = okAnalytics ? (analyticsRecords?.length || 0) : 'error';
|
|
706
|
+
}
|
|
707
|
+
} catch (error) {
|
|
708
|
+
diagnostics.warnings.push({
|
|
709
|
+
resource: resName,
|
|
710
|
+
field: fieldName,
|
|
711
|
+
issue: 'Failed to fetch statistics',
|
|
712
|
+
error: error.message
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
resourceDiag.fields.push(fieldDiag);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (resourceDiag.fields.length > 0) {
|
|
721
|
+
diagnostics.resources.push(resourceDiag);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Overall health check
|
|
726
|
+
diagnostics.health = {
|
|
727
|
+
status: diagnostics.errors.length === 0 ? (diagnostics.warnings.length === 0 ? 'healthy' : 'warning') : 'error',
|
|
728
|
+
totalResources: diagnostics.resources.length,
|
|
729
|
+
totalFields: diagnostics.resources.reduce((sum, r) => sum + r.fields.length, 0),
|
|
730
|
+
errorCount: diagnostics.errors.length,
|
|
731
|
+
warningCount: diagnostics.warnings.length
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
return diagnostics;
|
|
735
|
+
}
|
|
534
736
|
}
|
|
535
737
|
|
|
536
738
|
export default EventualConsistencyPlugin;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { S3dbError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FulltextError - Errors related to fulltext search operations
|
|
5
|
+
*
|
|
6
|
+
* Used for fulltext search operations including:
|
|
7
|
+
* - Index creation and updates
|
|
8
|
+
* - Search query execution
|
|
9
|
+
* - Index configuration
|
|
10
|
+
* - Text analysis and tokenization
|
|
11
|
+
* - Search result ranking
|
|
12
|
+
*
|
|
13
|
+
* @extends S3dbError
|
|
14
|
+
*/
|
|
15
|
+
export class FulltextError extends S3dbError {
|
|
16
|
+
constructor(message, details = {}) {
|
|
17
|
+
const { resourceName, query, operation = 'unknown', ...rest } = details;
|
|
18
|
+
|
|
19
|
+
let description = details.description;
|
|
20
|
+
if (!description) {
|
|
21
|
+
description = `
|
|
22
|
+
Fulltext Search Operation Error
|
|
23
|
+
|
|
24
|
+
Operation: ${operation}
|
|
25
|
+
${resourceName ? `Resource: ${resourceName}` : ''}
|
|
26
|
+
${query ? `Query: ${query}` : ''}
|
|
27
|
+
|
|
28
|
+
Common causes:
|
|
29
|
+
1. Resource not indexed for fulltext search
|
|
30
|
+
2. Invalid query syntax
|
|
31
|
+
3. Index not built yet
|
|
32
|
+
4. Search configuration missing
|
|
33
|
+
5. Field not indexed
|
|
34
|
+
|
|
35
|
+
Solution:
|
|
36
|
+
Ensure resource is configured for fulltext search and index is built.
|
|
37
|
+
|
|
38
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/fulltext.md
|
|
39
|
+
`.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
super(message, { ...rest, resourceName, query, operation, description });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default FulltextError;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Plugin from "./plugin.class.js";
|
|
2
2
|
import tryFn from "../concerns/try-fn.js";
|
|
3
|
+
import { FulltextError } from "./fulltext.errors.js";
|
|
3
4
|
|
|
4
5
|
export class FullTextPlugin extends Plugin {
|
|
5
6
|
constructor(options = {}) {
|
|
@@ -399,14 +400,20 @@ export class FullTextPlugin extends Plugin {
|
|
|
399
400
|
// Search and return full records
|
|
400
401
|
async searchRecords(resourceName, query, options = {}) {
|
|
401
402
|
const searchResults = await this.search(resourceName, query, options);
|
|
402
|
-
|
|
403
|
+
|
|
403
404
|
if (searchResults.length === 0) {
|
|
404
405
|
return [];
|
|
405
406
|
}
|
|
406
407
|
|
|
407
408
|
const resource = this.database.resources[resourceName];
|
|
408
409
|
if (!resource) {
|
|
409
|
-
throw new
|
|
410
|
+
throw new FulltextError(`Resource '${resourceName}' not found`, {
|
|
411
|
+
operation: 'searchRecords',
|
|
412
|
+
resourceName,
|
|
413
|
+
query,
|
|
414
|
+
availableResources: Object.keys(this.database.resources),
|
|
415
|
+
suggestion: 'Check resource name or ensure resource is created before searching'
|
|
416
|
+
});
|
|
410
417
|
}
|
|
411
418
|
|
|
412
419
|
const recordIds = searchResults.map(result => result.recordId);
|
|
@@ -430,7 +437,12 @@ export class FullTextPlugin extends Plugin {
|
|
|
430
437
|
async rebuildIndex(resourceName) {
|
|
431
438
|
const resource = this.database.resources[resourceName];
|
|
432
439
|
if (!resource) {
|
|
433
|
-
throw new
|
|
440
|
+
throw new FulltextError(`Resource '${resourceName}' not found`, {
|
|
441
|
+
operation: 'rebuildIndex',
|
|
442
|
+
resourceName,
|
|
443
|
+
availableResources: Object.keys(this.database.resources),
|
|
444
|
+
suggestion: 'Check resource name or ensure resource is created before rebuilding index'
|
|
445
|
+
});
|
|
434
446
|
}
|
|
435
447
|
|
|
436
448
|
// Clear existing indexes for this resource
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { S3dbError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MetricsError - Errors related to metrics operations
|
|
5
|
+
*
|
|
6
|
+
* Used for metrics operations including:
|
|
7
|
+
* - Metric collection and recording
|
|
8
|
+
* - Metric aggregation
|
|
9
|
+
* - Metric querying
|
|
10
|
+
* - Performance tracking
|
|
11
|
+
* - Statistics computation
|
|
12
|
+
*
|
|
13
|
+
* @extends S3dbError
|
|
14
|
+
*/
|
|
15
|
+
export class MetricsError extends S3dbError {
|
|
16
|
+
constructor(message, details = {}) {
|
|
17
|
+
const { metricName, operation = 'unknown', resourceName, ...rest } = details;
|
|
18
|
+
|
|
19
|
+
let description = details.description;
|
|
20
|
+
if (!description) {
|
|
21
|
+
description = `
|
|
22
|
+
Metrics Operation Error
|
|
23
|
+
|
|
24
|
+
Operation: ${operation}
|
|
25
|
+
${metricName ? `Metric: ${metricName}` : ''}
|
|
26
|
+
${resourceName ? `Resource: ${resourceName}` : ''}
|
|
27
|
+
|
|
28
|
+
Common causes:
|
|
29
|
+
1. Metric not configured
|
|
30
|
+
2. Invalid metric value or type
|
|
31
|
+
3. Metrics storage not accessible
|
|
32
|
+
4. Aggregation function error
|
|
33
|
+
5. Query parameters invalid
|
|
34
|
+
|
|
35
|
+
Solution:
|
|
36
|
+
Check metrics configuration and ensure proper initialization.
|
|
37
|
+
|
|
38
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/metrics.md
|
|
39
|
+
`.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
super(message, { ...rest, metricName, operation, resourceName, description });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default MetricsError;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Plugin } from './plugin.class.js';
|
|
2
2
|
import { createConsumer } from './consumers/index.js';
|
|
3
3
|
import tryFn from "../concerns/try-fn.js";
|
|
4
|
+
import { QueueError } from "./queue.errors.js";
|
|
4
5
|
|
|
5
6
|
// Example configuration for SQS:
|
|
6
7
|
// const plugin = new QueueConsumerPlugin({
|
|
@@ -101,13 +102,32 @@ export class QueueConsumerPlugin extends Plugin {
|
|
|
101
102
|
|
|
102
103
|
|
|
103
104
|
if (!resource) {
|
|
104
|
-
throw new
|
|
105
|
+
throw new QueueError('Resource not found in message', {
|
|
106
|
+
operation: 'handleMessage',
|
|
107
|
+
queueName: configuredResource,
|
|
108
|
+
messageBody: body,
|
|
109
|
+
suggestion: 'Ensure message includes a "resource" field specifying the target resource name'
|
|
110
|
+
});
|
|
105
111
|
}
|
|
106
112
|
if (!action) {
|
|
107
|
-
throw new
|
|
113
|
+
throw new QueueError('Action not found in message', {
|
|
114
|
+
operation: 'handleMessage',
|
|
115
|
+
queueName: configuredResource,
|
|
116
|
+
resource,
|
|
117
|
+
messageBody: body,
|
|
118
|
+
suggestion: 'Ensure message includes an "action" field (insert, update, or delete)'
|
|
119
|
+
});
|
|
108
120
|
}
|
|
109
121
|
const resourceObj = this.database.resources[resource];
|
|
110
|
-
if (!resourceObj)
|
|
122
|
+
if (!resourceObj) {
|
|
123
|
+
throw new QueueError(`Resource '${resource}' not found`, {
|
|
124
|
+
operation: 'handleMessage',
|
|
125
|
+
queueName: configuredResource,
|
|
126
|
+
resource,
|
|
127
|
+
availableResources: Object.keys(this.database.resources),
|
|
128
|
+
suggestion: 'Check resource name or ensure resource is created before consuming messages'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
111
131
|
|
|
112
132
|
let result;
|
|
113
133
|
const [ok, err, res] = await tryFn(async () => {
|
|
@@ -119,7 +139,14 @@ export class QueueConsumerPlugin extends Plugin {
|
|
|
119
139
|
} else if (action === 'delete') {
|
|
120
140
|
result = await resourceObj.delete(data.id);
|
|
121
141
|
} else {
|
|
122
|
-
throw new
|
|
142
|
+
throw new QueueError(`Unsupported action '${action}'`, {
|
|
143
|
+
operation: 'handleMessage',
|
|
144
|
+
queueName: configuredResource,
|
|
145
|
+
resource,
|
|
146
|
+
action,
|
|
147
|
+
supportedActions: ['insert', 'update', 'delete'],
|
|
148
|
+
suggestion: 'Use one of the supported actions: insert, update, or delete'
|
|
149
|
+
});
|
|
123
150
|
}
|
|
124
151
|
return result;
|
|
125
152
|
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { S3dbError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QueueError - Errors related to queue operations
|
|
5
|
+
*
|
|
6
|
+
* Used for queue operations including:
|
|
7
|
+
* - Message enqueueing and dequeueing
|
|
8
|
+
* - Queue consumer registration
|
|
9
|
+
* - Message processing
|
|
10
|
+
* - Dead letter queue handling
|
|
11
|
+
* - Queue configuration and management
|
|
12
|
+
*
|
|
13
|
+
* @extends S3dbError
|
|
14
|
+
*/
|
|
15
|
+
export class QueueError extends S3dbError {
|
|
16
|
+
constructor(message, details = {}) {
|
|
17
|
+
const { queueName, operation = 'unknown', messageId, ...rest } = details;
|
|
18
|
+
|
|
19
|
+
let description = details.description;
|
|
20
|
+
if (!description) {
|
|
21
|
+
description = `
|
|
22
|
+
Queue Operation Error
|
|
23
|
+
|
|
24
|
+
Operation: ${operation}
|
|
25
|
+
${queueName ? `Queue: ${queueName}` : ''}
|
|
26
|
+
${messageId ? `Message ID: ${messageId}` : ''}
|
|
27
|
+
|
|
28
|
+
Common causes:
|
|
29
|
+
1. Queue not properly configured
|
|
30
|
+
2. Message handler not registered
|
|
31
|
+
3. Queue resource not found
|
|
32
|
+
4. SQS/RabbitMQ connection failed
|
|
33
|
+
5. Message processing timeout
|
|
34
|
+
|
|
35
|
+
Solution:
|
|
36
|
+
Check queue configuration and message handler registration.
|
|
37
|
+
|
|
38
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/queue.md
|
|
39
|
+
`.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
super(message, { ...rest, queueName, operation, messageId, description });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default QueueError;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { S3dbError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ReplicationError - Errors related to replication operations
|
|
5
|
+
*
|
|
6
|
+
* Used for replicator operations including:
|
|
7
|
+
* - Replicator initialization and setup
|
|
8
|
+
* - Data replication to target systems
|
|
9
|
+
* - Resource mapping and transformation
|
|
10
|
+
* - Connection management
|
|
11
|
+
* - Batch replication operations
|
|
12
|
+
*
|
|
13
|
+
* @extends S3dbError
|
|
14
|
+
*/
|
|
15
|
+
export class ReplicationError extends S3dbError {
|
|
16
|
+
constructor(message, details = {}) {
|
|
17
|
+
const { replicatorClass = 'unknown', operation = 'unknown', resourceName, ...rest } = details;
|
|
18
|
+
|
|
19
|
+
let description = details.description;
|
|
20
|
+
if (!description) {
|
|
21
|
+
description = `
|
|
22
|
+
Replication Operation Error
|
|
23
|
+
|
|
24
|
+
Replicator: ${replicatorClass}
|
|
25
|
+
Operation: ${operation}
|
|
26
|
+
${resourceName ? `Resource: ${resourceName}` : ''}
|
|
27
|
+
|
|
28
|
+
Common causes:
|
|
29
|
+
1. Invalid replicator configuration
|
|
30
|
+
2. Target system not accessible
|
|
31
|
+
3. Resource not configured for replication
|
|
32
|
+
4. Invalid operation type
|
|
33
|
+
5. Transformation function errors
|
|
34
|
+
|
|
35
|
+
Solution:
|
|
36
|
+
Check replicator configuration and ensure target system is accessible.
|
|
37
|
+
|
|
38
|
+
Docs: https://github.com/forattini-dev/s3db.js/blob/main/docs/plugins/replicator.md
|
|
39
|
+
`.trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
super(message, { ...rest, replicatorClass, operation, resourceName, description });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default ReplicationError;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Plugin from "./plugin.class.js";
|
|
2
2
|
import tryFn from "../concerns/try-fn.js";
|
|
3
3
|
import { createReplicator, validateReplicatorConfig } from "./replicators/index.js";
|
|
4
|
+
import { ReplicationError } from "./replicator.errors.js";
|
|
4
5
|
|
|
5
6
|
function normalizeResourceName(name) {
|
|
6
7
|
return typeof name === 'string' ? name.trim().toLowerCase() : name;
|
|
@@ -120,12 +121,40 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
120
121
|
super();
|
|
121
122
|
// Validation for config tests
|
|
122
123
|
if (!options.replicators || !Array.isArray(options.replicators)) {
|
|
123
|
-
throw new
|
|
124
|
+
throw new ReplicationError('ReplicatorPlugin requires replicators array', {
|
|
125
|
+
operation: 'constructor',
|
|
126
|
+
pluginName: 'ReplicatorPlugin',
|
|
127
|
+
providedOptions: Object.keys(options),
|
|
128
|
+
suggestion: 'Provide replicators array: new ReplicatorPlugin({ replicators: [{ driver: "s3db", resources: [...] }] })'
|
|
129
|
+
});
|
|
124
130
|
}
|
|
125
131
|
for (const rep of options.replicators) {
|
|
126
|
-
if (!rep.driver)
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
if (!rep.driver) {
|
|
133
|
+
throw new ReplicationError('Each replicator must have a driver', {
|
|
134
|
+
operation: 'constructor',
|
|
135
|
+
pluginName: 'ReplicatorPlugin',
|
|
136
|
+
replicatorConfig: rep,
|
|
137
|
+
suggestion: 'Each replicator entry must specify a driver: { driver: "s3db", resources: {...} }'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!rep.resources || typeof rep.resources !== 'object') {
|
|
141
|
+
throw new ReplicationError('Each replicator must have resources config', {
|
|
142
|
+
operation: 'constructor',
|
|
143
|
+
pluginName: 'ReplicatorPlugin',
|
|
144
|
+
driver: rep.driver,
|
|
145
|
+
replicatorConfig: rep,
|
|
146
|
+
suggestion: 'Provide resources as object or array: { driver: "s3db", resources: ["users"] } or { resources: { users: "people" } }'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (Object.keys(rep.resources).length === 0) {
|
|
150
|
+
throw new ReplicationError('Each replicator must have at least one resource configured', {
|
|
151
|
+
operation: 'constructor',
|
|
152
|
+
pluginName: 'ReplicatorPlugin',
|
|
153
|
+
driver: rep.driver,
|
|
154
|
+
replicatorConfig: rep,
|
|
155
|
+
suggestion: 'Add at least one resource to replicate: { driver: "s3db", resources: ["users"] }'
|
|
156
|
+
});
|
|
157
|
+
}
|
|
129
158
|
}
|
|
130
159
|
|
|
131
160
|
this.config = {
|
|
@@ -657,7 +686,13 @@ export class ReplicatorPlugin extends Plugin {
|
|
|
657
686
|
async syncAllData(replicatorId) {
|
|
658
687
|
const replicator = this.replicators.find(r => r.id === replicatorId);
|
|
659
688
|
if (!replicator) {
|
|
660
|
-
throw new
|
|
689
|
+
throw new ReplicationError('Replicator not found', {
|
|
690
|
+
operation: 'syncAllData',
|
|
691
|
+
pluginName: 'ReplicatorPlugin',
|
|
692
|
+
replicatorId,
|
|
693
|
+
availableReplicators: this.replicators.map(r => r.id),
|
|
694
|
+
suggestion: 'Check replicator ID or use getReplicatorStats() to list available replicators'
|
|
695
|
+
});
|
|
661
696
|
}
|
|
662
697
|
|
|
663
698
|
this.stats.lastSync = new Date().toISOString();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
|
+
import { ReplicationError } from '../replicator.errors.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Base class for all replicator drivers
|
|
@@ -31,7 +32,12 @@ export class BaseReplicator extends EventEmitter {
|
|
|
31
32
|
* @returns {Promise<Object>} replicator result
|
|
32
33
|
*/
|
|
33
34
|
async replicate(resourceName, operation, data, id) {
|
|
34
|
-
throw new
|
|
35
|
+
throw new ReplicationError('replicate() method must be implemented by subclass', {
|
|
36
|
+
operation: 'replicate',
|
|
37
|
+
replicatorClass: this.name,
|
|
38
|
+
resourceName,
|
|
39
|
+
suggestion: 'Extend BaseReplicator and implement the replicate() method'
|
|
40
|
+
});
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -41,7 +47,13 @@ export class BaseReplicator extends EventEmitter {
|
|
|
41
47
|
* @returns {Promise<Object>} Batch replicator result
|
|
42
48
|
*/
|
|
43
49
|
async replicateBatch(resourceName, records) {
|
|
44
|
-
throw new
|
|
50
|
+
throw new ReplicationError('replicateBatch() method must be implemented by subclass', {
|
|
51
|
+
operation: 'replicateBatch',
|
|
52
|
+
replicatorClass: this.name,
|
|
53
|
+
resourceName,
|
|
54
|
+
batchSize: records?.length,
|
|
55
|
+
suggestion: 'Extend BaseReplicator and implement the replicateBatch() method'
|
|
56
|
+
});
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
/**
|
|
@@ -49,7 +61,11 @@ export class BaseReplicator extends EventEmitter {
|
|
|
49
61
|
* @returns {Promise<boolean>} True if connection is successful
|
|
50
62
|
*/
|
|
51
63
|
async testConnection() {
|
|
52
|
-
throw new
|
|
64
|
+
throw new ReplicationError('testConnection() method must be implemented by subclass', {
|
|
65
|
+
operation: 'testConnection',
|
|
66
|
+
replicatorClass: this.name,
|
|
67
|
+
suggestion: 'Extend BaseReplicator and implement the testConnection() method'
|
|
68
|
+
});
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
/**
|
|
@@ -3,6 +3,7 @@ import BigqueryReplicator from './bigquery-replicator.class.js';
|
|
|
3
3
|
import PostgresReplicator from './postgres-replicator.class.js';
|
|
4
4
|
import S3dbReplicator from './s3db-replicator.class.js';
|
|
5
5
|
import SqsReplicator from './sqs-replicator.class.js';
|
|
6
|
+
import { ReplicationError } from '../replicator.errors.js';
|
|
6
7
|
|
|
7
8
|
export { BaseReplicator, BigqueryReplicator, PostgresReplicator, S3dbReplicator, SqsReplicator };
|
|
8
9
|
|
|
@@ -24,11 +25,16 @@ export const REPLICATOR_DRIVERS = {
|
|
|
24
25
|
*/
|
|
25
26
|
export function createReplicator(driver, config = {}, resources = [], client = null) {
|
|
26
27
|
const ReplicatorClass = REPLICATOR_DRIVERS[driver];
|
|
27
|
-
|
|
28
|
+
|
|
28
29
|
if (!ReplicatorClass) {
|
|
29
|
-
throw new
|
|
30
|
+
throw new ReplicationError(`Unknown replicator driver: ${driver}`, {
|
|
31
|
+
operation: 'createReplicator',
|
|
32
|
+
driver,
|
|
33
|
+
availableDrivers: Object.keys(REPLICATOR_DRIVERS),
|
|
34
|
+
suggestion: `Use one of the available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(', ')}`
|
|
35
|
+
});
|
|
30
36
|
}
|
|
31
|
-
|
|
37
|
+
|
|
32
38
|
return new ReplicatorClass(config, resources, client);
|
|
33
39
|
}
|
|
34
40
|
|