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.
Files changed (46) hide show
  1. package/dist/s3db.cjs.js +1650 -136
  2. package/dist/s3db.cjs.js.map +1 -1
  3. package/dist/s3db.es.js +1644 -137
  4. package/dist/s3db.es.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/behaviors/enforce-limits.js +28 -4
  7. package/src/behaviors/index.js +6 -1
  8. package/src/client.class.js +11 -1
  9. package/src/concerns/partition-queue.js +7 -1
  10. package/src/concerns/plugin-storage.js +75 -13
  11. package/src/database.class.js +22 -4
  12. package/src/errors.js +414 -24
  13. package/src/partition-drivers/base-partition-driver.js +12 -2
  14. package/src/partition-drivers/index.js +7 -1
  15. package/src/partition-drivers/memory-partition-driver.js +20 -5
  16. package/src/partition-drivers/sqs-partition-driver.js +6 -1
  17. package/src/plugins/audit.errors.js +46 -0
  18. package/src/plugins/backup/base-backup-driver.class.js +36 -6
  19. package/src/plugins/backup/filesystem-backup-driver.class.js +55 -7
  20. package/src/plugins/backup/index.js +40 -9
  21. package/src/plugins/backup/multi-backup-driver.class.js +69 -9
  22. package/src/plugins/backup/s3-backup-driver.class.js +48 -6
  23. package/src/plugins/backup.errors.js +45 -0
  24. package/src/plugins/cache/cache.class.js +8 -1
  25. package/src/plugins/cache/memory-cache.class.js +216 -33
  26. package/src/plugins/cache.errors.js +47 -0
  27. package/src/plugins/cache.plugin.js +94 -3
  28. package/src/plugins/eventual-consistency/analytics.js +145 -0
  29. package/src/plugins/eventual-consistency/index.js +203 -1
  30. package/src/plugins/fulltext.errors.js +46 -0
  31. package/src/plugins/fulltext.plugin.js +15 -3
  32. package/src/plugins/metrics.errors.js +46 -0
  33. package/src/plugins/queue-consumer.plugin.js +31 -4
  34. package/src/plugins/queue.errors.js +46 -0
  35. package/src/plugins/replicator.errors.js +46 -0
  36. package/src/plugins/replicator.plugin.js +40 -5
  37. package/src/plugins/replicators/base-replicator.class.js +19 -3
  38. package/src/plugins/replicators/index.js +9 -3
  39. package/src/plugins/replicators/s3db-replicator.class.js +38 -8
  40. package/src/plugins/scheduler.errors.js +46 -0
  41. package/src/plugins/scheduler.plugin.js +79 -19
  42. package/src/plugins/state-machine.errors.js +47 -0
  43. package/src/plugins/state-machine.plugin.js +86 -17
  44. package/src/resource.class.js +8 -1
  45. package/src/stream/index.js +6 -1
  46. 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 Error(`Resource '${resourceName}' not found`);
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 Error(`Resource '${resourceName}' not found`);
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 Error('QueueConsumerPlugin: resource not found in message');
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 Error('QueueConsumerPlugin: action not found in message');
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) throw new Error(`QueueConsumerPlugin: resource '${resource}' not found`);
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 Error(`QueueConsumerPlugin: unsupported action '${action}'`);
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 Error('ReplicatorPlugin: replicators array is required');
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) throw new Error('ReplicatorPlugin: each replicator must have a driver');
127
- if (!rep.resources || typeof rep.resources !== 'object') throw new Error('ReplicatorPlugin: each replicator must have resources config');
128
- if (Object.keys(rep.resources).length === 0) throw new Error('ReplicatorPlugin: each replicator must have at least one resource configured');
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 Error(`Replicator not found: ${replicatorId}`);
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 Error(`replicate() method must be implemented by ${this.name}`);
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 Error(`replicateBatch() method must be implemented by ${this.name}`);
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 Error(`testConnection() method must be implemented by ${this.name}`);
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 Error(`Unknown replicator driver: ${driver}. Available drivers: ${Object.keys(REPLICATOR_DRIVERS).join(', ')}`);
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