s3db.js 13.6.0 → 14.0.2

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 (193) hide show
  1. package/README.md +139 -43
  2. package/dist/s3db.cjs +72425 -38970
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72177 -38764
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/lib/base-handler.js +157 -0
  7. package/mcp/lib/handlers/connection-handler.js +280 -0
  8. package/mcp/lib/handlers/query-handler.js +533 -0
  9. package/mcp/lib/handlers/resource-handler.js +428 -0
  10. package/mcp/lib/tool-registry.js +336 -0
  11. package/mcp/lib/tools/connection-tools.js +161 -0
  12. package/mcp/lib/tools/query-tools.js +267 -0
  13. package/mcp/lib/tools/resource-tools.js +404 -0
  14. package/package.json +94 -49
  15. package/src/clients/memory-client.class.js +346 -191
  16. package/src/clients/memory-storage.class.js +300 -84
  17. package/src/clients/s3-client.class.js +7 -6
  18. package/src/concerns/geo-encoding.js +19 -2
  19. package/src/concerns/ip.js +59 -9
  20. package/src/concerns/money.js +8 -1
  21. package/src/concerns/password-hashing.js +49 -8
  22. package/src/concerns/plugin-storage.js +186 -18
  23. package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
  24. package/src/database.class.js +139 -29
  25. package/src/errors.js +332 -42
  26. package/src/plugins/api/auth/oidc-auth.js +66 -17
  27. package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
  28. package/src/plugins/api/auth/strategies/factory.class.js +63 -0
  29. package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
  30. package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
  31. package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
  32. package/src/plugins/api/concerns/failban-manager.js +106 -57
  33. package/src/plugins/api/concerns/opengraph-helper.js +116 -0
  34. package/src/plugins/api/concerns/route-context.js +601 -0
  35. package/src/plugins/api/concerns/state-machine.js +288 -0
  36. package/src/plugins/api/index.js +180 -41
  37. package/src/plugins/api/routes/auth-routes.js +198 -30
  38. package/src/plugins/api/routes/resource-routes.js +19 -4
  39. package/src/plugins/api/server/health-manager.class.js +163 -0
  40. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  41. package/src/plugins/api/server/router.class.js +472 -0
  42. package/src/plugins/api/server.js +280 -1303
  43. package/src/plugins/api/utils/custom-routes.js +17 -5
  44. package/src/plugins/api/utils/guards.js +76 -17
  45. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  46. package/src/plugins/api/utils/openapi-generator.js +7 -6
  47. package/src/plugins/api/utils/template-engine.js +77 -3
  48. package/src/plugins/audit.plugin.js +30 -8
  49. package/src/plugins/backup.plugin.js +110 -14
  50. package/src/plugins/cache/cache.class.js +22 -5
  51. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  52. package/src/plugins/cache/memory-cache.class.js +211 -57
  53. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  54. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  55. package/src/plugins/cache/redis-cache.class.js +552 -0
  56. package/src/plugins/cache/s3-cache.class.js +17 -8
  57. package/src/plugins/cache.plugin.js +176 -61
  58. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  59. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  60. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  62. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  66. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  67. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  68. package/src/plugins/cloud-inventory/index.js +29 -8
  69. package/src/plugins/cloud-inventory/registry.js +64 -42
  70. package/src/plugins/cloud-inventory.plugin.js +240 -138
  71. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  72. package/src/plugins/concerns/resource-names.js +100 -0
  73. package/src/plugins/consumers/index.js +10 -2
  74. package/src/plugins/consumers/sqs-consumer.js +12 -2
  75. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  76. package/src/plugins/cookie-farm.errors.js +73 -0
  77. package/src/plugins/cookie-farm.plugin.js +869 -0
  78. package/src/plugins/costs.plugin.js +7 -1
  79. package/src/plugins/eventual-consistency/analytics.js +94 -19
  80. package/src/plugins/eventual-consistency/config.js +15 -7
  81. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  82. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  83. package/src/plugins/eventual-consistency/helpers.js +39 -14
  84. package/src/plugins/eventual-consistency/install.js +21 -2
  85. package/src/plugins/eventual-consistency/utils.js +32 -10
  86. package/src/plugins/fulltext.plugin.js +38 -11
  87. package/src/plugins/geo.plugin.js +61 -9
  88. package/src/plugins/identity/concerns/config.js +61 -0
  89. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  90. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  91. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  92. package/src/plugins/identity/concerns/token-generator.js +29 -4
  93. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  94. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  95. package/src/plugins/identity/drivers/index.js +18 -0
  96. package/src/plugins/identity/drivers/password-driver.js +122 -0
  97. package/src/plugins/identity/email-service.js +17 -2
  98. package/src/plugins/identity/index.js +413 -69
  99. package/src/plugins/identity/oauth2-server.js +413 -30
  100. package/src/plugins/identity/oidc-discovery.js +16 -8
  101. package/src/plugins/identity/rsa-keys.js +115 -35
  102. package/src/plugins/identity/server.js +166 -45
  103. package/src/plugins/identity/session-manager.js +53 -7
  104. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  105. package/src/plugins/identity/ui/routes.js +363 -255
  106. package/src/plugins/importer/index.js +153 -20
  107. package/src/plugins/index.js +9 -2
  108. package/src/plugins/kubernetes-inventory/index.js +6 -0
  109. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  110. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  111. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  112. package/src/plugins/metrics.plugin.js +64 -16
  113. package/src/plugins/ml/base-model.class.js +25 -15
  114. package/src/plugins/ml/regression-model.class.js +1 -1
  115. package/src/plugins/ml.errors.js +57 -25
  116. package/src/plugins/ml.plugin.js +28 -4
  117. package/src/plugins/namespace.js +210 -0
  118. package/src/plugins/plugin.class.js +180 -8
  119. package/src/plugins/puppeteer/console-monitor.js +729 -0
  120. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  121. package/src/plugins/puppeteer/network-monitor.js +816 -0
  122. package/src/plugins/puppeteer/performance-manager.js +746 -0
  123. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  124. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  125. package/src/plugins/puppeteer.errors.js +81 -0
  126. package/src/plugins/puppeteer.plugin.js +1327 -0
  127. package/src/plugins/queue-consumer.plugin.js +69 -14
  128. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  129. package/src/plugins/recon/concerns/command-runner.js +148 -0
  130. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  131. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  132. package/src/plugins/recon/concerns/process-manager.js +338 -0
  133. package/src/plugins/recon/concerns/report-generator.js +478 -0
  134. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  135. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  136. package/src/plugins/recon/config/defaults.js +321 -0
  137. package/src/plugins/recon/config/resources.js +370 -0
  138. package/src/plugins/recon/index.js +778 -0
  139. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  140. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  141. package/src/plugins/recon/managers/storage-manager.js +745 -0
  142. package/src/plugins/recon/managers/target-manager.js +274 -0
  143. package/src/plugins/recon/stages/asn-stage.js +314 -0
  144. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  145. package/src/plugins/recon/stages/dns-stage.js +107 -0
  146. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  147. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  148. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  149. package/src/plugins/recon/stages/http-stage.js +89 -0
  150. package/src/plugins/recon/stages/latency-stage.js +148 -0
  151. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  152. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  153. package/src/plugins/recon/stages/ports-stage.js +169 -0
  154. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  155. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  156. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  157. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  158. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  159. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  160. package/src/plugins/recon/stages/whois-stage.js +349 -0
  161. package/src/plugins/recon.plugin.js +75 -0
  162. package/src/plugins/recon.plugin.js.backup +2635 -0
  163. package/src/plugins/relation.errors.js +87 -14
  164. package/src/plugins/replicator.plugin.js +514 -137
  165. package/src/plugins/replicators/base-replicator.class.js +89 -1
  166. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  167. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  168. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  169. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  170. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  171. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  172. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  173. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  174. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  175. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  176. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  177. package/src/plugins/s3-queue.plugin.js +464 -65
  178. package/src/plugins/scheduler.plugin.js +20 -6
  179. package/src/plugins/state-machine.plugin.js +40 -9
  180. package/src/plugins/tfstate/README.md +126 -126
  181. package/src/plugins/tfstate/base-driver.js +28 -4
  182. package/src/plugins/tfstate/errors.js +65 -10
  183. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  184. package/src/plugins/tfstate/index.js +163 -90
  185. package/src/plugins/tfstate/s3-driver.js +64 -6
  186. package/src/plugins/ttl.plugin.js +72 -17
  187. package/src/plugins/vector/distances.js +18 -12
  188. package/src/plugins/vector/kmeans.js +26 -4
  189. package/src/resource.class.js +115 -19
  190. package/src/testing/factory.class.js +20 -3
  191. package/src/testing/seeder.class.js +7 -1
  192. package/src/clients/memory-client.md +0 -917
  193. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -1,4 +1,5 @@
1
1
  import EventEmitter from 'events';
2
+ import { PromisePool } from '@supercharge/promise-pool';
2
3
  import { ReplicationError } from '../replicator.errors.js';
3
4
 
4
5
  /**
@@ -11,6 +12,7 @@ export class BaseReplicator extends EventEmitter {
11
12
  this.config = config;
12
13
  this.name = this.constructor.name;
13
14
  this.enabled = config.enabled !== false; // Default to enabled unless explicitly disabled
15
+ this.batchConcurrency = Math.max(1, config.batchConcurrency ?? 5);
14
16
  }
15
17
 
16
18
  /**
@@ -89,6 +91,92 @@ export class BaseReplicator extends EventEmitter {
89
91
  this.emit('cleanup', { replicator: this.name });
90
92
  }
91
93
 
94
+ /**
95
+ * Update the default batch concurrency at runtime
96
+ * @param {number} value
97
+ */
98
+ setBatchConcurrency(value) {
99
+ if (!Number.isFinite(value) || value <= 0) {
100
+ throw new ReplicationError('Batch concurrency must be a positive number', {
101
+ operation: 'setBatchConcurrency',
102
+ replicatorClass: this.name,
103
+ providedValue: value,
104
+ suggestion: 'Provide a positive integer value greater than zero.'
105
+ });
106
+ }
107
+ this.batchConcurrency = Math.floor(value);
108
+ }
109
+
110
+ /**
111
+ * Generic helper to process batches with controlled concurrency
112
+ * @param {Array} records - Items to process
113
+ * @param {Function} handler - Async handler executed for each record
114
+ * @param {Object} options
115
+ * @param {number} [options.concurrency] - Concurrency override
116
+ * @param {Function} [options.mapError] - Maps thrown errors before collection
117
+ * @returns {Promise<{results: Array, errors: Array}>}
118
+ */
119
+ async processBatch(records = [], handler, { concurrency, mapError } = {}) {
120
+ if (!Array.isArray(records) || records.length === 0) {
121
+ return { results: [], errors: [] };
122
+ }
123
+
124
+ if (typeof handler !== 'function') {
125
+ throw new ReplicationError('processBatch requires an async handler function', {
126
+ operation: 'processBatch',
127
+ replicatorClass: this.name,
128
+ suggestion: 'Provide an async handler: async (record) => { ... }'
129
+ });
130
+ }
131
+
132
+ const limit = Math.max(1, concurrency ?? this.batchConcurrency ?? 5);
133
+ const results = [];
134
+ const errors = [];
135
+
136
+ await PromisePool
137
+ .withConcurrency(limit)
138
+ .for(records)
139
+ .process(async record => {
140
+ try {
141
+ const result = await handler(record);
142
+ results.push(result);
143
+ } catch (error) {
144
+ if (typeof mapError === 'function') {
145
+ const mapped = mapError(error, record);
146
+ if (mapped !== undefined) {
147
+ errors.push(mapped);
148
+ }
149
+ } else {
150
+ errors.push({ record, error });
151
+ }
152
+ }
153
+ });
154
+
155
+ return { results, errors };
156
+ }
157
+
158
+ /**
159
+ * Helper to build replication errors with contextual metadata
160
+ * @param {string} message
161
+ * @param {Object} details
162
+ * @returns {ReplicationError}
163
+ */
164
+ createError(message, details = {}) {
165
+ return new ReplicationError(message, {
166
+ replicatorClass: this.name,
167
+ operation: details.operation || 'unknown',
168
+ resourceName: details.resourceName,
169
+ statusCode: details.statusCode ?? 500,
170
+ retriable: details.retriable ?? false,
171
+ suggestion: details.suggestion,
172
+ description: details.description,
173
+ docs: details.docs,
174
+ hint: details.hint,
175
+ metadata: details.metadata,
176
+ ...details
177
+ });
178
+ }
179
+
92
180
  /**
93
181
  * Validate replicator configuration
94
182
  * @returns {Object} Validation result
@@ -98,4 +186,4 @@ export class BaseReplicator extends EventEmitter {
98
186
  }
99
187
  }
100
188
 
101
- export default BaseReplicator;
189
+ export default BaseReplicator;
@@ -87,7 +87,12 @@ class BigqueryReplicator extends BaseReplicator {
87
87
  _validateMutability(mutability) {
88
88
  const validModes = ['append-only', 'mutable', 'immutable'];
89
89
  if (!validModes.includes(mutability)) {
90
- throw new Error(`Invalid mutability mode: ${mutability}. Must be one of: ${validModes.join(', ')}`);
90
+ throw this.createError(`Invalid mutability mode: ${mutability}`, {
91
+ operation: 'config',
92
+ statusCode: 400,
93
+ retriable: false,
94
+ suggestion: `Use one of the supported mutability modes: ${validModes.join(', ')}.`
95
+ });
91
96
  }
92
97
  }
93
98
 
@@ -213,11 +218,12 @@ class BigqueryReplicator extends BaseReplicator {
213
218
  continue;
214
219
  }
215
220
 
216
- const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
221
+ // Use $schema for reliable access to resource definition
222
+ const allAttributes = resource.$schema.attributes || {};
217
223
 
218
224
  // Filter out plugin attributes - they are internal and should not be replicated
219
- const pluginAttrNames = resource.schema?._pluginAttributes
220
- ? Object.values(resource.schema._pluginAttributes).flat()
225
+ const pluginAttrNames = resource.$schema._pluginAttributes
226
+ ? Object.values(resource.$schema._pluginAttributes).flat()
221
227
  : [];
222
228
  const attributes = Object.fromEntries(
223
229
  Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
@@ -235,7 +241,15 @@ class BigqueryReplicator extends BaseReplicator {
235
241
  const message = `Schema sync failed for table ${tableName}: ${errSync.message}`;
236
242
 
237
243
  if (this.schemaSync.onMismatch === 'error') {
238
- throw new Error(message);
244
+ throw this.createError(message, {
245
+ operation: 'schemaSync',
246
+ resourceName,
247
+ tableName,
248
+ statusCode: 409,
249
+ retriable: errSync?.retriable ?? false,
250
+ suggestion: 'Review the BigQuery table schema and align it with the S3DB resource definition or relax schemaSync.onMismatch.',
251
+ docs: 'docs/plugins/replicator.md'
252
+ });
239
253
  } else if (this.schemaSync.onMismatch === 'warn') {
240
254
  console.warn(`[BigQueryReplicator] ${message}`);
241
255
  }
@@ -261,11 +275,23 @@ class BigqueryReplicator extends BaseReplicator {
261
275
 
262
276
  if (!exists) {
263
277
  if (!this.schemaSync.autoCreateTable) {
264
- throw new Error(`Table ${tableName} does not exist and autoCreateTable is disabled`);
278
+ throw this.createError(`Table ${tableName} does not exist and autoCreateTable is disabled`, {
279
+ operation: 'schemaSync',
280
+ tableName,
281
+ statusCode: 404,
282
+ retriable: false,
283
+ suggestion: 'Create the BigQuery table manually or enable schemaSync.autoCreateTable.'
284
+ });
265
285
  }
266
286
 
267
287
  if (this.schemaSync.strategy === 'validate-only') {
268
- throw new Error(`Table ${tableName} does not exist (validate-only mode)`);
288
+ throw this.createError(`Table ${tableName} does not exist (validate-only mode)`, {
289
+ operation: 'schemaSync',
290
+ tableName,
291
+ statusCode: 404,
292
+ retriable: false,
293
+ suggestion: 'Provision the table before running validate-only checks or switch the schemaSync.strategy to alter.'
294
+ });
269
295
  }
270
296
 
271
297
  // Create table with schema (including tracking fields based on mutability)
@@ -339,7 +365,13 @@ class BigqueryReplicator extends BaseReplicator {
339
365
  const newFields = generateBigQuerySchemaUpdate(attributes, existingSchema, mutability);
340
366
 
341
367
  if (newFields.length > 0) {
342
- throw new Error(`Table ${tableName} schema mismatch. Missing columns: ${newFields.length}`);
368
+ throw this.createError(`Table ${tableName} schema mismatch. Missing columns: ${newFields.length}`, {
369
+ operation: 'schemaValidation',
370
+ tableName,
371
+ statusCode: 409,
372
+ retriable: false,
373
+ suggestion: 'Update the BigQuery table schema to include the missing columns or enable schemaSync.autoCreateColumns.'
374
+ });
343
375
  }
344
376
  }
345
377
  }
@@ -555,7 +587,14 @@ class BigqueryReplicator extends BaseReplicator {
555
587
  throw error;
556
588
  }
557
589
  } else {
558
- throw new Error(`Unsupported operation: ${operation}`);
590
+ throw this.createError(`Unsupported operation: ${operation}`, {
591
+ operation: 'replicate',
592
+ resourceName,
593
+ tableName: tableConfig.table,
594
+ statusCode: 400,
595
+ retriable: false,
596
+ suggestion: 'Replicator supports insert, update, or delete actions. Adjust the resources configuration accordingly.'
597
+ });
559
598
  }
560
599
 
561
600
  results.push({
@@ -634,26 +673,31 @@ class BigqueryReplicator extends BaseReplicator {
634
673
  }
635
674
 
636
675
  async replicateBatch(resourceName, records) {
637
- const results = [];
638
- const errors = [];
639
-
640
- for (const record of records) {
641
- const [ok, err, res] = await tryFn(() => this.replicate(
676
+ const { results, errors } = await this.processBatch(
677
+ records,
678
+ async (record) => {
679
+ const [ok, err, res] = await tryFn(() => this.replicate(
642
680
  resourceName,
643
681
  record.operation,
644
682
  record.data,
645
683
  record.id,
646
684
  record.beforeData
647
685
  ));
648
- if (ok) {
649
- results.push(res);
650
- } else {
651
- if (this.config.verbose) {
652
- console.warn(`[BigqueryReplicator] Batch replication failed for record ${record.id}: ${err.message}`);
686
+ if (!ok) {
687
+ throw err;
688
+ }
689
+ return res;
690
+ },
691
+ {
692
+ concurrency: this.config.batchConcurrency,
693
+ mapError: (error, record) => {
694
+ if (this.config.verbose) {
695
+ console.warn(`[BigqueryReplicator] Batch replication failed for record ${record.id}: ${error.message}`);
696
+ }
697
+ return { id: record.id, error: error.message };
653
698
  }
654
- errors.push({ id: record.id, error: err.message });
655
699
  }
656
- }
700
+ );
657
701
 
658
702
  // Log errors if any occurred during batch processing
659
703
  if (errors.length > 0) {
@@ -699,4 +743,4 @@ class BigqueryReplicator extends BaseReplicator {
699
743
  }
700
744
  }
701
745
 
702
- export default BigqueryReplicator;
746
+ export default BigqueryReplicator;
@@ -315,22 +315,24 @@ class DynamoDBReplicator extends BaseReplicator {
315
315
  }
316
316
 
317
317
  async replicateBatch(resourceName, records) {
318
- const results = [];
319
- const errors = [];
320
-
321
- // DynamoDB batch operations (up to 25 items)
322
- // For now, process sequentially (can be optimized with BatchWriteItem)
323
- for (const record of records) {
324
- const [ok, err, result] = await tryFn(() =>
325
- this.replicate(resourceName, record.operation, record.data, record.id)
326
- );
318
+ const { results, errors } = await this.processBatch(
319
+ records,
320
+ async (record) => {
321
+ const [ok, err, result] = await tryFn(() =>
322
+ this.replicate(resourceName, record.operation, record.data, record.id)
323
+ );
324
+
325
+ if (!ok) {
326
+ throw err;
327
+ }
327
328
 
328
- if (ok) {
329
- results.push(result);
330
- } else {
331
- errors.push({ id: record.id, error: err.message });
329
+ return result;
330
+ },
331
+ {
332
+ concurrency: this.config.batchConcurrency,
333
+ mapError: (error, record) => ({ id: record.id, error: error.message })
332
334
  }
333
- }
335
+ );
334
336
 
335
337
  return {
336
338
  success: errors.length === 0,
@@ -343,7 +345,12 @@ class DynamoDBReplicator extends BaseReplicator {
343
345
  async testConnection() {
344
346
  const [ok, err] = await tryFn(async () => {
345
347
  if (!this.client) {
346
- throw new Error('Client not initialized');
348
+ throw this.createError('Client not initialized', {
349
+ operation: 'testConnection',
350
+ statusCode: 503,
351
+ retriable: true,
352
+ suggestion: 'Call initialize() before testing connectivity or ensure the DynamoDB client was created successfully.'
353
+ });
347
354
  }
348
355
 
349
356
  const { ListTablesCommand } = requirePluginDependency('@aws-sdk/client-dynamodb', 'DynamoDBReplicator');
@@ -324,22 +324,24 @@ class MongoDBReplicator extends BaseReplicator {
324
324
  }
325
325
 
326
326
  async replicateBatch(resourceName, records) {
327
- const results = [];
328
- const errors = [];
329
-
330
- // MongoDB supports bulk operations, but for consistency with other replicators
331
- // and error handling, we process sequentially
332
- for (const record of records) {
333
- const [ok, err, result] = await tryFn(() =>
334
- this.replicate(resourceName, record.operation, record.data, record.id)
335
- );
327
+ const { results, errors } = await this.processBatch(
328
+ records,
329
+ async (record) => {
330
+ const [ok, err, result] = await tryFn(() =>
331
+ this.replicate(resourceName, record.operation, record.data, record.id)
332
+ );
333
+
334
+ if (!ok) {
335
+ throw err;
336
+ }
336
337
 
337
- if (ok) {
338
- results.push(result);
339
- } else {
340
- errors.push({ id: record.id, error: err.message });
338
+ return result;
339
+ },
340
+ {
341
+ concurrency: this.config.batchConcurrency,
342
+ mapError: (error, record) => ({ id: record.id, error: error.message })
341
343
  }
342
- }
344
+ );
343
345
 
344
346
  return {
345
347
  success: errors.length === 0,
@@ -352,7 +354,12 @@ class MongoDBReplicator extends BaseReplicator {
352
354
  async testConnection() {
353
355
  const [ok, err] = await tryFn(async () => {
354
356
  if (!this.client) {
355
- throw new Error('Client not initialized');
357
+ throw this.createError('Client not initialized', {
358
+ operation: 'testConnection',
359
+ statusCode: 503,
360
+ retriable: true,
361
+ suggestion: 'Ensure initialize() was called and the MongoDB client connected before testing connectivity.'
362
+ });
356
363
  }
357
364
 
358
365
  await this.db.admin().ping();
@@ -242,7 +242,15 @@ class MySQLReplicator extends BaseReplicator {
242
242
  const message = `Schema sync failed for table ${tableName}: ${errSync.message}`;
243
243
 
244
244
  if (this.schemaSync.onMismatch === 'error') {
245
- throw new Error(message);
245
+ throw this.createError(message, {
246
+ operation: 'schemaSync',
247
+ resourceName,
248
+ tableName,
249
+ statusCode: 409,
250
+ retriable: errSync?.retriable ?? false,
251
+ suggestion: 'Update the MySQL table schema to match the resource definition or adjust schemaSync.onMismatch.',
252
+ docs: 'docs/plugins/replicator.md'
253
+ });
246
254
  } else if (this.schemaSync.onMismatch === 'warn') {
247
255
  console.warn(`[MySQLReplicator] ${message}`);
248
256
  }
@@ -268,11 +276,23 @@ class MySQLReplicator extends BaseReplicator {
268
276
 
269
277
  if (!existingSchema) {
270
278
  if (!this.schemaSync.autoCreateTable) {
271
- throw new Error(`Table ${tableName} does not exist and autoCreateTable is disabled`);
279
+ throw this.createError(`Table ${tableName} does not exist and autoCreateTable is disabled`, {
280
+ operation: 'schemaSync',
281
+ tableName,
282
+ statusCode: 404,
283
+ retriable: false,
284
+ suggestion: 'Provision the table manually or enable schemaSync.autoCreateTable.'
285
+ });
272
286
  }
273
287
 
274
288
  if (this.schemaSync.strategy === 'validate-only') {
275
- throw new Error(`Table ${tableName} does not exist (validate-only mode)`);
289
+ throw this.createError(`Table ${tableName} does not exist (validate-only mode)`, {
290
+ operation: 'schemaSync',
291
+ tableName,
292
+ statusCode: 404,
293
+ retriable: false,
294
+ suggestion: 'Create the table before running validate-only checks or choose the alter strategy.'
295
+ });
276
296
  }
277
297
 
278
298
  // Create table
@@ -336,7 +356,13 @@ class MySQLReplicator extends BaseReplicator {
336
356
  const alterStatements = generateMySQLAlterTable(tableName, attributes, existingSchema);
337
357
 
338
358
  if (alterStatements.length > 0) {
339
- throw new Error(`Table ${tableName} schema mismatch. Missing columns: ${alterStatements.length}`);
359
+ throw this.createError(`Table ${tableName} schema mismatch. Missing columns: ${alterStatements.length}`, {
360
+ operation: 'schemaValidation',
361
+ tableName,
362
+ statusCode: 409,
363
+ retriable: false,
364
+ suggestion: 'Add the missing columns to the MySQL table or enable schemaSync.autoCreateColumns.'
365
+ });
340
366
  }
341
367
  }
342
368
  } finally {
@@ -498,20 +524,24 @@ class MySQLReplicator extends BaseReplicator {
498
524
  }
499
525
 
500
526
  async replicateBatch(resourceName, records) {
501
- const results = [];
502
- const errors = [];
503
-
504
- for (const record of records) {
505
- const [ok, err, result] = await tryFn(() =>
506
- this.replicate(resourceName, record.operation, record.data, record.id)
507
- );
527
+ const { results, errors } = await this.processBatch(
528
+ records,
529
+ async (record) => {
530
+ const [ok, err, result] = await tryFn(() =>
531
+ this.replicate(resourceName, record.operation, record.data, record.id)
532
+ );
533
+
534
+ if (!ok) {
535
+ throw err;
536
+ }
508
537
 
509
- if (ok) {
510
- results.push(result);
511
- } else {
512
- errors.push({ id: record.id, error: err.message });
538
+ return result;
539
+ },
540
+ {
541
+ concurrency: this.config.batchConcurrency,
542
+ mapError: (error, record) => ({ id: record.id, error: error.message })
513
543
  }
514
- }
544
+ );
515
545
 
516
546
  return {
517
547
  success: errors.length === 0,
@@ -524,7 +554,12 @@ class MySQLReplicator extends BaseReplicator {
524
554
  async testConnection() {
525
555
  const [ok, err] = await tryFn(async () => {
526
556
  if (!this.pool) {
527
- throw new Error('Pool not initialized');
557
+ throw this.createError('Pool not initialized', {
558
+ operation: 'testConnection',
559
+ statusCode: 503,
560
+ retriable: true,
561
+ suggestion: 'Call initialize() before testing the connection or ensure the pool was not cleaned up prematurely.'
562
+ });
528
563
  }
529
564
 
530
565
  const connection = await this.pool.promise().getConnection();
@@ -204,7 +204,15 @@ class PlanetScaleReplicator extends BaseReplicator {
204
204
  const message = `Schema sync failed for table ${tableName}: ${errSync.message}`;
205
205
 
206
206
  if (this.schemaSync.onMismatch === 'error') {
207
- throw new Error(message);
207
+ throw this.createError(message, {
208
+ operation: 'schemaSync',
209
+ resourceName,
210
+ tableName,
211
+ statusCode: 409,
212
+ retriable: errSync?.retriable ?? false,
213
+ suggestion: 'Align the PlanetScale table schema with the resource definition or relax schemaSync.onMismatch.',
214
+ docs: 'docs/plugins/replicator.md'
215
+ });
208
216
  } else if (this.schemaSync.onMismatch === 'warn') {
209
217
  console.warn(`[PlanetScaleReplicator] ${message}`);
210
218
  }
@@ -227,11 +235,23 @@ class PlanetScaleReplicator extends BaseReplicator {
227
235
 
228
236
  if (!existingSchema) {
229
237
  if (!this.schemaSync.autoCreateTable) {
230
- throw new Error(`Table ${tableName} does not exist and autoCreateTable is disabled`);
238
+ throw this.createError(`Table ${tableName} does not exist and autoCreateTable is disabled`, {
239
+ operation: 'schemaSync',
240
+ tableName,
241
+ statusCode: 404,
242
+ retriable: false,
243
+ suggestion: 'Create the table manually or enable schemaSync.autoCreateTable.'
244
+ });
231
245
  }
232
246
 
233
247
  if (this.schemaSync.strategy === 'validate-only') {
234
- throw new Error(`Table ${tableName} does not exist (validate-only mode)`);
248
+ throw this.createError(`Table ${tableName} does not exist (validate-only mode)`, {
249
+ operation: 'schemaSync',
250
+ tableName,
251
+ statusCode: 404,
252
+ retriable: false,
253
+ suggestion: 'Provision the table before running validate-only checks or choose the alter strategy.'
254
+ });
235
255
  }
236
256
 
237
257
  // Create table
@@ -295,7 +315,13 @@ class PlanetScaleReplicator extends BaseReplicator {
295
315
  const alterStatements = generateMySQLAlterTable(tableName, attributes, existingSchema);
296
316
 
297
317
  if (alterStatements.length > 0) {
298
- throw new Error(`Table ${tableName} schema mismatch. Missing columns: ${alterStatements.length}`);
318
+ throw this.createError(`Table ${tableName} schema mismatch. Missing columns: ${alterStatements.length}`, {
319
+ operation: 'schemaValidation',
320
+ tableName,
321
+ statusCode: 409,
322
+ retriable: false,
323
+ suggestion: 'Add the missing columns to the PlanetScale table or enable schemaSync.autoCreateColumns.'
324
+ });
299
325
  }
300
326
  }
301
327
  }
@@ -247,7 +247,15 @@ class PostgresReplicator extends BaseReplicator {
247
247
  const message = `Schema sync failed for table ${tableName}: ${errSync.message}`;
248
248
 
249
249
  if (this.schemaSync.onMismatch === 'error') {
250
- throw new Error(message);
250
+ throw this.createError(message, {
251
+ operation: 'schemaSync',
252
+ resourceName,
253
+ tableName,
254
+ statusCode: 409,
255
+ retriable: errSync?.retriable ?? false,
256
+ suggestion: 'Align the PostgreSQL table schema with the resource definition or relax schemaSync.onMismatch.',
257
+ docs: 'docs/plugins/replicator.md'
258
+ });
251
259
  } else if (this.schemaSync.onMismatch === 'warn') {
252
260
  console.warn(`[PostgresReplicator] ${message}`);
253
261
  }
@@ -272,11 +280,23 @@ class PostgresReplicator extends BaseReplicator {
272
280
  if (!existingSchema) {
273
281
  // Table doesn't exist
274
282
  if (!this.schemaSync.autoCreateTable) {
275
- throw new Error(`Table ${tableName} does not exist and autoCreateTable is disabled`);
283
+ throw this.createError(`Table ${tableName} does not exist and autoCreateTable is disabled`, {
284
+ operation: 'schemaSync',
285
+ tableName,
286
+ statusCode: 404,
287
+ retriable: false,
288
+ suggestion: 'Create the table manually or enable schemaSync.autoCreateTable to let the replicator provision it.'
289
+ });
276
290
  }
277
291
 
278
292
  if (this.schemaSync.strategy === 'validate-only') {
279
- throw new Error(`Table ${tableName} does not exist (validate-only mode)`);
293
+ throw this.createError(`Table ${tableName} does not exist (validate-only mode)`, {
294
+ operation: 'schemaSync',
295
+ tableName,
296
+ statusCode: 404,
297
+ retriable: false,
298
+ suggestion: 'Provision the destination table before running validate-only schema checks or switch to the alter strategy.'
299
+ });
280
300
  }
281
301
 
282
302
  // Create table
@@ -343,7 +363,13 @@ class PostgresReplicator extends BaseReplicator {
343
363
  const alterStatements = generatePostgresAlterTable(tableName, attributes, existingSchema);
344
364
 
345
365
  if (alterStatements.length > 0) {
346
- throw new Error(`Table ${tableName} schema mismatch. Missing columns: ${alterStatements.length}`);
366
+ throw this.createError(`Table ${tableName} schema mismatch. Missing columns: ${alterStatements.length}`, {
367
+ operation: 'schemaValidation',
368
+ tableName,
369
+ statusCode: 409,
370
+ retriable: false,
371
+ suggestion: 'Update the PostgreSQL table to include the missing columns or allow the replicator to manage schema changes.'
372
+ });
347
373
  }
348
374
  }
349
375
  }
@@ -416,7 +442,14 @@ class PostgresReplicator extends BaseReplicator {
416
442
  const sql = `DELETE FROM ${table} WHERE id=$1 RETURNING *`;
417
443
  result = await this.client.query(sql, [id]);
418
444
  } else {
419
- throw new Error(`Unsupported operation: ${operation}`);
445
+ throw this.createError(`Unsupported operation: ${operation}`, {
446
+ operation: 'replicate',
447
+ resourceName,
448
+ tableName: table,
449
+ statusCode: 400,
450
+ retriable: false,
451
+ suggestion: 'Use one of the supported actions: insert, update, or delete.'
452
+ });
420
453
  }
421
454
 
422
455
  results.push({
@@ -484,36 +517,38 @@ class PostgresReplicator extends BaseReplicator {
484
517
  }
485
518
 
486
519
  async replicateBatch(resourceName, records) {
487
- const results = [];
488
- const errors = [];
489
-
490
- for (const record of records) {
520
+ const { results, errors } = await this.processBatch(records, async (record) => {
491
521
  const [ok, err, res] = await tryFn(() => this.replicate(
492
- resourceName,
493
- record.operation,
494
- record.data,
495
- record.id,
522
+ resourceName,
523
+ record.operation,
524
+ record.data,
525
+ record.id,
496
526
  record.beforeData
497
527
  ));
498
- if (ok) {
499
- results.push(res);
500
- } else {
528
+
529
+ if (!ok) {
530
+ throw err;
531
+ }
532
+
533
+ return res;
534
+ }, {
535
+ concurrency: this.config.batchConcurrency,
536
+ mapError: (error, record) => {
501
537
  if (this.config.verbose) {
502
- console.warn(`[PostgresReplicator] Batch replication failed for record ${record.id}: ${err.message}`);
538
+ console.warn(`[PostgresReplicator] Batch replication failed for record ${record.id}: ${error.message}`);
503
539
  }
504
- errors.push({ id: record.id, error: err.message });
540
+ return { id: record.id, error: error.message };
505
541
  }
506
- }
507
-
508
- // Log errors if any occurred during batch processing
542
+ });
543
+
509
544
  if (errors.length > 0) {
510
545
  console.warn(`[PostgresReplicator] Batch replication completed with ${errors.length} error(s) for ${resourceName}:`, errors);
511
546
  }
512
-
513
- return {
514
- success: errors.length === 0,
515
- results,
516
- errors
547
+
548
+ return {
549
+ success: errors.length === 0,
550
+ results,
551
+ errors
517
552
  };
518
553
  }
519
554
 
@@ -562,4 +597,4 @@ class PostgresReplicator extends BaseReplicator {
562
597
  }
563
598
 
564
599
 
565
- export default PostgresReplicator;
600
+ export default PostgresReplicator;