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,7 +1,9 @@
1
+ import { PromisePool } from "@supercharge/promise-pool";
1
2
  import { Plugin } from "./plugin.class.js";
2
3
  import tryFn from "../concerns/try-fn.js";
3
4
  import { createReplicator, validateReplicatorConfig } from "./replicators/index.js";
4
5
  import { ReplicationError } from "./replicator.errors.js";
6
+ import { resolveResourceName } from "./concerns/resource-names.js";
5
7
 
6
8
  function normalizeResourceName(name) {
7
9
  return typeof name === 'string' ? name.trim().toLowerCase() : name;
@@ -118,7 +120,7 @@ function normalizeResourceName(name) {
118
120
  */
119
121
  export class ReplicatorPlugin extends Plugin {
120
122
  constructor(options = {}) {
121
- super();
123
+ super(options);
122
124
  // Validation for config tests
123
125
  if (!options.replicators || !Array.isArray(options.replicators)) {
124
126
  throw new ReplicationError('ReplicatorPlugin requires replicators array', {
@@ -156,18 +158,32 @@ export class ReplicatorPlugin extends Plugin {
156
158
  });
157
159
  }
158
160
  }
159
-
161
+
162
+ const resourceNamesOption = options.resourceNames || {};
163
+ const resolvedReplicatorConcurrency = Number.isFinite(options.replicatorConcurrency)
164
+ ? Math.max(1, Math.floor(options.replicatorConcurrency))
165
+ : 5;
166
+ const resolvedStopConcurrency = Number.isFinite(options.stopConcurrency)
167
+ ? Math.max(1, Math.floor(options.stopConcurrency))
168
+ : resolvedReplicatorConcurrency;
160
169
  this.config = {
161
170
  replicators: options.replicators || [],
162
171
  logErrors: options.logErrors !== false,
163
- replicatorLogResource: options.replicatorLogResource || 'replicator_log',
164
172
  persistReplicatorLog: options.persistReplicatorLog || false,
165
173
  enabled: options.enabled !== false,
166
174
  batchSize: options.batchSize || 100,
167
175
  maxRetries: options.maxRetries || 3,
168
176
  timeout: options.timeout || 30000,
169
- verbose: options.verbose || false
177
+ verbose: options.verbose || false,
178
+ replicatorConcurrency: resolvedReplicatorConcurrency,
179
+ stopConcurrency: resolvedStopConcurrency
180
+ };
181
+ this._logResourceDescriptor = {
182
+ defaultName: 'plg_replicator_logs',
183
+ override: resourceNamesOption.log || options.replicatorLogResource
170
184
  };
185
+ this.logResourceName = this._resolveLogResourceName();
186
+ this.config.logResourceName = this.logResourceName;
171
187
 
172
188
  this.replicators = [];
173
189
  this.database = null;
@@ -179,6 +195,21 @@ export class ReplicatorPlugin extends Plugin {
179
195
  lastSync: null
180
196
  };
181
197
  this._afterCreateResourceHook = null;
198
+ this.replicatorLog = null;
199
+ this._logResourceHooksInstalled = false;
200
+ }
201
+
202
+ _resolveLogResourceName() {
203
+ return resolveResourceName('replicator', this._logResourceDescriptor, {
204
+ namespace: this.namespace
205
+ });
206
+ }
207
+
208
+ onNamespaceChanged() {
209
+ this.logResourceName = this._resolveLogResourceName();
210
+ if (this.config) {
211
+ this.config.logResourceName = this.logResourceName;
212
+ }
182
213
  }
183
214
 
184
215
  // Helper to filter out internal S3DB fields
@@ -193,24 +224,41 @@ export class ReplicatorPlugin extends Plugin {
193
224
  return filtered;
194
225
  }
195
226
 
227
+ async prepareReplicationData(resource, data) {
228
+ const complete = await this.getCompleteData(resource, data);
229
+ return this.filterInternalFields(complete);
230
+ }
231
+
232
+ sanitizeBeforeData(beforeData) {
233
+ if (!beforeData) return null;
234
+ return this.filterInternalFields(beforeData);
235
+ }
236
+
196
237
  async getCompleteData(resource, data) {
197
- // Always get the complete record from the resource to ensure we have all data
198
- // This handles all behaviors: body-overflow, truncate-data, body-only, etc.
199
238
  const [ok, err, completeRecord] = await tryFn(() => resource.get(data.id));
200
- return ok ? completeRecord : data;
239
+ if (ok && completeRecord) {
240
+ return completeRecord;
241
+ }
242
+
243
+ if (this.config.verbose) {
244
+ const reason = err?.message || 'record not found';
245
+ console.warn(`[ReplicatorPlugin] Falling back to provided data for ${resource?.name || 'unknown'}#${data?.id}: ${reason}`);
246
+ }
247
+
248
+ return data;
201
249
  }
202
250
 
203
251
  installEventListeners(resource, database, plugin) {
204
252
  if (!resource || this.eventListenersInstalled.has(resource.name) ||
205
- resource.name === this.config.replicatorLogResource) {
253
+ resource.name === this.logResourceName) {
206
254
  return;
207
255
  }
208
256
 
209
257
  // Create handler functions and save references for later removal
210
258
  const insertHandler = async (data) => {
211
259
  const [ok, error] = await tryFn(async () => {
212
- const completeData = { ...data, createdAt: new Date().toISOString() };
213
- await plugin.processReplicatorEvent('insert', resource.name, completeData.id, completeData);
260
+ const payload = await plugin.prepareReplicationData(resource, data);
261
+ await plugin.processReplicatorEvent('insert', resource.name, payload.id, payload);
214
262
  });
215
263
 
216
264
  if (!ok) {
@@ -223,10 +271,9 @@ export class ReplicatorPlugin extends Plugin {
223
271
 
224
272
  const updateHandler = async (data, beforeData) => {
225
273
  const [ok, error] = await tryFn(async () => {
226
- // For updates, we need to get the complete updated record, not just the changed fields
227
- const completeData = await plugin.getCompleteData(resource, data);
228
- const dataWithTimestamp = { ...completeData, updatedAt: new Date().toISOString() };
229
- await plugin.processReplicatorEvent('update', resource.name, completeData.id, dataWithTimestamp, beforeData);
274
+ const payload = await plugin.prepareReplicationData(resource, data);
275
+ const beforePayload = plugin.sanitizeBeforeData(beforeData);
276
+ await plugin.processReplicatorEvent('update', resource.name, payload.id, payload, beforePayload);
230
277
  });
231
278
 
232
279
  if (!ok) {
@@ -239,7 +286,8 @@ export class ReplicatorPlugin extends Plugin {
239
286
 
240
287
  const deleteHandler = async (data) => {
241
288
  const [ok, error] = await tryFn(async () => {
242
- await plugin.processReplicatorEvent('delete', resource.name, data.id, data);
289
+ const sanitized = await plugin.prepareReplicationData(resource, data);
290
+ await plugin.processReplicatorEvent('delete', resource.name, sanitized.id, sanitized);
243
291
  });
244
292
 
245
293
  if (!ok) {
@@ -268,23 +316,41 @@ export class ReplicatorPlugin extends Plugin {
268
316
  async onInstall() {
269
317
  // Create replicator log resource if enabled
270
318
  if (this.config.persistReplicatorLog) {
319
+ const logResourceName = this.logResourceName;
271
320
  const [ok, err, logResource] = await tryFn(() => this.database.createResource({
272
- name: this.config.replicatorLogResource || 'plg_replicator_logs',
321
+ name: logResourceName,
273
322
  attributes: {
274
323
  id: 'string|required',
324
+ replicator: 'string|required',
275
325
  resource: 'string|required',
276
326
  action: 'string|required',
277
327
  data: 'json',
278
328
  timestamp: 'number|required',
279
- createdAt: 'string|required'
329
+ createdAt: 'string|required',
330
+ status: 'string|required',
331
+ error: 'string|optional'
280
332
  },
281
- behavior: 'truncate-data'
333
+ behavior: 'truncate-data',
334
+ partitions: {
335
+ byDate: {
336
+ fields: {
337
+ createdAt: 'string|maxlength:10'
338
+ }
339
+ }
340
+ }
282
341
  }));
283
342
 
284
343
  if (ok) {
285
- this.replicatorLogResource = logResource;
344
+ this.replicatorLog = logResource;
345
+ this.installReplicatorLogHooks();
286
346
  } else {
287
- this.replicatorLogResource = this.database.resources[this.config.replicatorLogResource || 'plg_replicator_logs'];
347
+ const existing = this.database.resources[logResourceName];
348
+ if (existing) {
349
+ this.replicatorLog = existing;
350
+ this.installReplicatorLogHooks();
351
+ } else {
352
+ throw err;
353
+ }
288
354
  }
289
355
  }
290
356
 
@@ -296,7 +362,7 @@ export class ReplicatorPlugin extends Plugin {
296
362
 
297
363
  // Install event listeners for existing resources
298
364
  for (const resource of Object.values(this.database.resources)) {
299
- if (resource.name !== (this.config.replicatorLogResource || 'plg_replicator_logs')) {
365
+ if (resource.name !== this.logResourceName) {
300
366
  this.installEventListeners(resource, this.database, this);
301
367
  }
302
368
  }
@@ -309,7 +375,7 @@ export class ReplicatorPlugin extends Plugin {
309
375
  installDatabaseHooks() {
310
376
  // Store hook reference for later removal
311
377
  this._afterCreateResourceHook = (resource) => {
312
- if (resource.name !== (this.config.replicatorLogResource || 'plg_replicator_logs')) {
378
+ if (resource.name !== this.logResourceName) {
313
379
  this.installEventListeners(resource, this.database, this);
314
380
  }
315
381
  };
@@ -325,6 +391,51 @@ export class ReplicatorPlugin extends Plugin {
325
391
  }
326
392
  }
327
393
 
394
+ installReplicatorLogHooks() {
395
+ if (!this.replicatorLog || typeof this.replicatorLog.addHook !== 'function') {
396
+ return;
397
+ }
398
+
399
+ if (this.replicatorLog._replicatorDefaultsInstalled) {
400
+ this._logResourceHooksInstalled = true;
401
+ return;
402
+ }
403
+
404
+ if (this._logResourceHooksInstalled) {
405
+ return;
406
+ }
407
+
408
+ const ensureInsertDefaults = (data) => {
409
+ if (!data || typeof data !== 'object') {
410
+ return data;
411
+ }
412
+ this._normalizeLogEntry(data, { assignId: true, ensureTimestamp: true });
413
+ return data;
414
+ };
415
+
416
+ const ensureUpdateDefaults = (data) => {
417
+ if (!data || typeof data !== 'object') {
418
+ return data;
419
+ }
420
+ this._normalizeLogEntry(data, { assignId: false, ensureTimestamp: false });
421
+ return data;
422
+ };
423
+
424
+ const ensurePatchDefaults = (payload) => {
425
+ if (payload && typeof payload === 'object' && payload.fields && typeof payload.fields === 'object') {
426
+ this._normalizeLogEntry(payload.fields, { assignId: false, ensureTimestamp: false });
427
+ }
428
+ return payload;
429
+ };
430
+
431
+ this.replicatorLog.addHook('beforeInsert', ensureInsertDefaults);
432
+ this.replicatorLog.addHook('beforeUpdate', ensureUpdateDefaults);
433
+ this.replicatorLog.addHook('beforePatch', ensurePatchDefaults);
434
+
435
+ this.replicatorLog._replicatorDefaultsInstalled = true;
436
+ this._logResourceHooksInstalled = true;
437
+ }
438
+
328
439
  createReplicator(driver, config, resources, client) {
329
440
  return createReplicator(driver, config, resources, client);
330
441
  }
@@ -381,21 +492,108 @@ export class ReplicatorPlugin extends Plugin {
381
492
  throw lastError;
382
493
  }
383
494
 
495
+ _generateLogEntryId() {
496
+ const random = Math.random().toString(36).slice(2, 8);
497
+ return `repl-${Date.now()}-${random}`;
498
+ }
499
+
500
+ _normalizeLogEntry(entry, options = {}) {
501
+ if (!entry || typeof entry !== 'object') {
502
+ return entry;
503
+ }
504
+
505
+ const {
506
+ assignId = false,
507
+ ensureTimestamp = false
508
+ } = options;
509
+
510
+ if (assignId && !entry.id) {
511
+ entry.id = this._generateLogEntryId();
512
+ }
513
+
514
+ const numericTimestamp = Number(entry.timestamp);
515
+ const hasNumericTimestamp = Number.isFinite(numericTimestamp);
516
+ if (hasNumericTimestamp) {
517
+ entry.timestamp = numericTimestamp;
518
+ }
519
+
520
+ if (ensureTimestamp && !hasNumericTimestamp) {
521
+ entry.timestamp = Date.now();
522
+ } else if (!ensureTimestamp && entry.timestamp !== undefined && !hasNumericTimestamp) {
523
+ entry.timestamp = Date.now();
524
+ }
525
+
526
+ if (!entry.createdAt && entry.timestamp) {
527
+ const iso = new Date(entry.timestamp).toISOString();
528
+ entry.createdAt = iso.slice(0, 10);
529
+ }
530
+
531
+ if (ensureTimestamp && !entry.createdAt) {
532
+ const iso = new Date().toISOString();
533
+ entry.createdAt = iso.slice(0, 10);
534
+ }
535
+
536
+ if (entry.resourceName || entry.resource) {
537
+ const normalized = normalizeResourceName(entry.resourceName || entry.resource);
538
+ if (normalized) {
539
+ entry.resourceName = normalized;
540
+ if (!entry.resource) {
541
+ entry.resource = normalized;
542
+ }
543
+ }
544
+ }
545
+
546
+ if (!entry.action && entry.operation) {
547
+ entry.action = entry.operation;
548
+ }
549
+
550
+ if (!entry.operation && entry.action) {
551
+ entry.operation = entry.action;
552
+ }
553
+
554
+ if (!entry.replicator) {
555
+ entry.replicator = entry.replicatorId || 'unknown';
556
+ }
557
+
558
+ let retryCount = entry.retryCount;
559
+ if (typeof retryCount !== 'number') {
560
+ retryCount = Number(retryCount);
561
+ }
562
+
563
+ if (!Number.isFinite(retryCount) || retryCount < 0) {
564
+ entry.retryCount = 0;
565
+ } else {
566
+ entry.retryCount = Math.floor(retryCount);
567
+ }
568
+
569
+ if (!entry.status) {
570
+ entry.status = 'pending';
571
+ }
572
+
573
+ if (!('error' in entry)) {
574
+ entry.error = null;
575
+ }
576
+
577
+ return entry;
578
+ }
579
+
384
580
  async logError(replicator, resourceName, operation, recordId, data, error) {
385
581
  const [ok, logError] = await tryFn(async () => {
386
- const logResourceName = this.config.replicatorLogResource;
387
- if (this.database && this.database.resources && this.database.resources[logResourceName]) {
388
- const logResource = this.database.resources[logResourceName];
389
- await logResource.insert({
390
- replicator: replicator.name || replicator.id,
582
+ if (this.replicatorLog) {
583
+ const logEntry = {
584
+ id: recordId ? `${resourceName}-${recordId}-${Date.now()}` : undefined,
585
+ replicator: replicator.name || replicator.id || 'unknown',
586
+ resource: resourceName,
391
587
  resourceName,
588
+ action: operation,
392
589
  operation,
393
- recordId,
394
- data: JSON.stringify(data),
395
- error: error.message,
396
- timestamp: new Date().toISOString(),
397
- status: 'error'
398
- });
590
+ data: data ? this.filterInternalFields(data) : null,
591
+ status: 'failed',
592
+ error: error?.message,
593
+ retryCount: 0
594
+ };
595
+ this._normalizeLogEntry(logEntry, { assignId: true, ensureTimestamp: true });
596
+ await this.replicatorLog.insert(logEntry);
399
597
  }
400
598
  });
401
599
 
@@ -417,6 +615,18 @@ export class ReplicatorPlugin extends Plugin {
417
615
  async processReplicatorEvent(operation, resourceName, recordId, data, beforeData = null) {
418
616
  if (!this.config.enabled) return;
419
617
 
618
+ if (!recordId) {
619
+ throw new ReplicationError('Replication event missing record identifier', {
620
+ operation,
621
+ resourceName,
622
+ pluginName: 'ReplicatorPlugin',
623
+ suggestion: 'Ensure the replicated record contains an id before emitting change events.'
624
+ });
625
+ }
626
+
627
+ const sanitizedData = data ? this.filterInternalFields(data) : null;
628
+ const sanitizedBefore = beforeData ? this.filterInternalFields(beforeData) : null;
629
+
420
630
  const applicableReplicators = this.replicators.filter(replicator => {
421
631
  const should = replicator.shouldReplicateResource && replicator.shouldReplicateResource(resourceName, operation);
422
632
  return should;
@@ -426,32 +636,42 @@ export class ReplicatorPlugin extends Plugin {
426
636
  return;
427
637
  }
428
638
 
429
- const promises = applicableReplicators.map(async (replicator) => {
430
- const [ok, error, result] = await tryFn(async () => {
431
- const result = await this.retryWithBackoff(
432
- () => replicator.replicate(resourceName, operation, data, recordId, beforeData),
433
- this.config.maxRetries
434
- );
435
-
436
- this.emit('plg:replicator:replicated', {
437
- replicator: replicator.name || replicator.id,
438
- resourceName,
439
- operation,
440
- recordId,
441
- result,
442
- success: true
639
+ const entries = applicableReplicators.map((replicator, index) => ({ replicator, index }));
640
+ const outcomes = new Array(entries.length);
641
+
642
+ const poolResult = await PromisePool
643
+ .withConcurrency(this.config.replicatorConcurrency)
644
+ .for(entries)
645
+ .process(async ({ replicator, index }) => {
646
+ const [ok, error, replicationResult] = await tryFn(async () => {
647
+ const result = await this.retryWithBackoff(
648
+ () => replicator.replicate(resourceName, operation, sanitizedData, recordId, sanitizedBefore),
649
+ this.config.maxRetries
650
+ );
651
+
652
+ this.emit('plg:replicator:replicated', {
653
+ replicator: replicator.name || replicator.id,
654
+ resourceName,
655
+ operation,
656
+ recordId,
657
+ result,
658
+ success: true
659
+ });
660
+
661
+ this.stats.totalReplications += 1;
662
+
663
+ return result;
443
664
  });
444
665
 
445
- return result;
446
- });
447
-
448
- if (ok) {
449
- return result;
450
- } else {
666
+ if (ok) {
667
+ outcomes[index] = { status: 'fulfilled', value: replicationResult };
668
+ return replicationResult;
669
+ }
670
+
451
671
  if (this.config.verbose) {
452
672
  console.warn(`[ReplicatorPlugin] Replication failed for ${replicator.name || replicator.id} on ${resourceName}: ${error.message}`);
453
673
  }
454
-
674
+
455
675
  this.emit('plg:replicator:error', {
456
676
  replicator: replicator.name || replicator.id,
457
677
  resourceName,
@@ -460,15 +680,25 @@ export class ReplicatorPlugin extends Plugin {
460
680
  error: error.message
461
681
  });
462
682
 
683
+ this.stats.totalErrors += 1;
684
+
463
685
  if (this.config.logErrors && this.database) {
464
686
  await this.logError(replicator, resourceName, operation, recordId, data, error);
465
687
  }
466
688
 
689
+ outcomes[index] = { status: 'rejected', reason: error };
467
690
  throw error;
691
+ });
692
+
693
+ if (poolResult.errors.length > 0) {
694
+ for (const { item, error } of poolResult.errors) {
695
+ if (item && typeof item.index === 'number' && !outcomes[item.index]) {
696
+ outcomes[item.index] = { status: 'rejected', reason: error };
697
+ }
468
698
  }
469
- });
699
+ }
470
700
 
471
- return Promise.allSettled(promises);
701
+ return outcomes;
472
702
  }
473
703
 
474
704
  async processReplicatorItem(item) {
@@ -481,51 +711,64 @@ export class ReplicatorPlugin extends Plugin {
481
711
  return;
482
712
  }
483
713
 
484
- const promises = applicableReplicators.map(async (replicator) => {
485
- const [wrapperOk, wrapperError] = await tryFn(async () => {
486
- const [ok, err, result] = await tryFn(() =>
487
- replicator.replicate(item.resourceName, item.operation, item.data, item.recordId, item.beforeData)
488
- );
714
+ const entries = applicableReplicators.map((replicator, index) => ({ replicator, index }));
715
+ const outcomes = new Array(entries.length);
716
+
717
+ await PromisePool
718
+ .withConcurrency(this.config.replicatorConcurrency)
719
+ .for(entries)
720
+ .process(async ({ replicator, index }) => {
721
+ const [wrapperOk, wrapperError] = await tryFn(async () => {
722
+ const preparedData = item.data ? this.filterInternalFields(item.data) : null;
723
+ const preparedBefore = item.beforeData ? this.filterInternalFields(item.beforeData) : null;
724
+ const [ok, err, result] = await tryFn(() =>
725
+ replicator.replicate(item.resourceName, item.operation, preparedData, item.recordId, preparedBefore)
726
+ );
727
+
728
+ if (!ok) {
729
+ if (this.config.verbose) {
730
+ console.warn(`[ReplicatorPlugin] Replicator item processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${err.message}`);
731
+ }
732
+
733
+ this.emit('plg:replicator:error', {
734
+ replicator: replicator.name || replicator.id,
735
+ resourceName: item.resourceName,
736
+ operation: item.operation,
737
+ recordId: item.recordId,
738
+ error: err.message
739
+ });
489
740
 
490
- if (!ok) {
491
- if (this.config.verbose) {
492
- console.warn(`[ReplicatorPlugin] Replicator item processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${err.message}`);
741
+ if (this.config.logErrors && this.database) {
742
+ await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, err);
743
+ }
744
+
745
+ this.stats.totalErrors += 1;
746
+ return { success: false, error: err.message };
493
747
  }
494
-
495
- this.emit('plg:replicator:error', {
748
+
749
+ this.emit('plg:replicator:replicated', {
496
750
  replicator: replicator.name || replicator.id,
497
751
  resourceName: item.resourceName,
498
752
  operation: item.operation,
499
753
  recordId: item.recordId,
500
- error: err.message
754
+ result,
755
+ success: true
501
756
  });
502
757
 
503
- if (this.config.logErrors && this.database) {
504
- await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, err);
505
- }
506
-
507
- return { success: false, error: err.message };
508
- }
758
+ this.stats.totalReplications += 1;
509
759
 
510
- this.emit('plg:replicator:replicated', {
511
- replicator: replicator.name || replicator.id,
512
- resourceName: item.resourceName,
513
- operation: item.operation,
514
- recordId: item.recordId,
515
- result,
516
- success: true
760
+ return { success: true, result };
517
761
  });
518
762
 
519
- return { success: true, result };
520
- });
521
-
522
- if (wrapperOk) {
523
- return wrapperOk;
524
- } else {
763
+ if (wrapperOk) {
764
+ outcomes[index] = { status: 'fulfilled', value: wrapperOk };
765
+ return wrapperOk;
766
+ }
767
+
525
768
  if (this.config.verbose) {
526
769
  console.warn(`[ReplicatorPlugin] Wrapper processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${wrapperError.message}`);
527
770
  }
528
-
771
+
529
772
  this.emit('plg:replicator:error', {
530
773
  replicator: replicator.name || replicator.id,
531
774
  resourceName: item.resourceName,
@@ -538,29 +781,45 @@ export class ReplicatorPlugin extends Plugin {
538
781
  await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, wrapperError);
539
782
  }
540
783
 
541
- return { success: false, error: wrapperError.message };
542
- }
543
- });
784
+ this.stats.totalErrors += 1;
785
+ const failure = { success: false, error: wrapperError.message };
786
+ outcomes[index] = { status: 'fulfilled', value: failure };
787
+ return failure;
788
+ });
544
789
 
545
- return Promise.allSettled(promises);
790
+ return outcomes;
546
791
  }
547
792
 
548
793
  async logReplicator(item) {
549
794
  // Always use the saved reference
550
- const logRes = this.replicatorLog || this.database.resources[normalizeResourceName(this.config.replicatorLogResource)];
795
+ const logRes = this.replicatorLog;
551
796
  if (!logRes) {
552
797
  this.emit('plg:replicator:log-failed', { error: 'replicator log resource not found', item });
553
798
  return;
554
799
  }
555
- // Fix required fields of log resource
800
+ const sanitizedData = item.data ? this.filterInternalFields(item.data) : {};
801
+
802
+ // Fix required fields of log resource
556
803
  const logItem = {
557
- id: item.id || `repl-${Date.now()}-${Math.random().toString(36).slice(2)}`,
804
+ id: item.id,
805
+ replicator: item.replicator || 'unknown',
558
806
  resource: item.resource || item.resourceName || '',
807
+ resourceName: item.resourceName || item.resource || '',
559
808
  action: item.operation || item.action || '',
560
- data: item.data || {},
561
- timestamp: typeof item.timestamp === 'number' ? item.timestamp : Date.now(),
562
- createdAt: item.createdAt || new Date().toISOString().slice(0, 10),
809
+ operation: item.operation || item.action || '',
810
+ data: sanitizedData,
811
+ status: item.status || 'pending',
812
+ error: item.error || null,
813
+ retryCount: item.retryCount || 0
563
814
  };
815
+ if (typeof item.timestamp === 'number') {
816
+ logItem.timestamp = item.timestamp;
817
+ }
818
+ if (item.createdAt) {
819
+ logItem.createdAt = item.createdAt;
820
+ }
821
+
822
+ this._normalizeLogEntry(logItem, { assignId: true, ensureTimestamp: true });
564
823
  const [ok, err] = await tryFn(async () => {
565
824
  await logRes.insert(logItem);
566
825
  });
@@ -590,17 +849,35 @@ export class ReplicatorPlugin extends Plugin {
590
849
 
591
850
  // Utility methods
592
851
  async getReplicatorStats() {
593
- const replicatorStats = await Promise.all(
594
- this.replicators.map(async (replicator) => {
852
+ const entries = this.replicators.map((replicator, index) => ({ replicator, index }));
853
+ const replicatorStats = new Array(entries.length);
854
+
855
+ const poolResult = await PromisePool
856
+ .withConcurrency(this.config.replicatorConcurrency)
857
+ .for(entries)
858
+ .process(async ({ replicator, index }) => {
595
859
  const status = await replicator.getStatus();
596
- return {
860
+ const info = {
597
861
  id: replicator.id,
598
862
  driver: replicator.driver,
599
863
  config: replicator.config,
600
864
  status
601
865
  };
602
- })
603
- );
866
+ replicatorStats[index] = info;
867
+ return info;
868
+ });
869
+
870
+ if (poolResult.errors.length > 0) {
871
+ const { item, error } = poolResult.errors[0];
872
+ const failedReplicator = item?.replicator;
873
+ throw new ReplicationError(`Failed to collect status for replicator ${failedReplicator?.name || failedReplicator?.id || 'unknown'}`, {
874
+ operation: 'getReplicatorStats',
875
+ pluginName: 'ReplicatorPlugin',
876
+ replicatorId: failedReplicator?.id,
877
+ driver: failedReplicator?.driver,
878
+ original: error
879
+ });
880
+ }
604
881
 
605
882
  return {
606
883
  replicators: replicatorStats,
@@ -652,20 +929,75 @@ export class ReplicatorPlugin extends Plugin {
652
929
 
653
930
  let retried = 0;
654
931
 
655
- for (const log of failedLogs || []) {
656
- const [ok, err] = await tryFn(async () => {
657
- // Re-queue the replicator
658
- await this.processReplicatorEvent(
659
- log.operation,
660
- log.resourceName,
661
- log.recordId,
662
- log.data
663
- );
932
+ const processResult = await PromisePool
933
+ .withConcurrency(this.config.replicatorConcurrency)
934
+ .for(failedLogs || [])
935
+ .process(async (log) => {
936
+ const sanitizedData = log.data ? this.filterInternalFields(log.data) : null;
937
+ const sanitizedBefore = log.beforeData ? this.filterInternalFields(log.beforeData) : null;
938
+
939
+ const [ok, err, results] = await tryFn(async () => {
940
+ return await this.processReplicatorEvent(
941
+ log.operation,
942
+ log.resourceName,
943
+ log.recordId,
944
+ sanitizedData,
945
+ sanitizedBefore
946
+ );
947
+ });
948
+
949
+ const isSuccessfulEntry = (entry) => {
950
+ if (!entry || entry.status !== 'fulfilled') {
951
+ return false;
952
+ }
953
+ if (entry.value && typeof entry.value === 'object' && 'success' in entry.value) {
954
+ return entry.value.success !== false;
955
+ }
956
+ return true;
957
+ };
958
+
959
+ if (ok && Array.isArray(results) && results.every(isSuccessfulEntry)) {
960
+ retried += 1;
961
+ await this.updateReplicatorLog(log.id, {
962
+ status: 'success',
963
+ error: null,
964
+ retryCount: log.retryCount || 0,
965
+ lastSuccessAt: new Date().toISOString()
966
+ });
967
+ return;
968
+ }
969
+
970
+ let failureMessage = err?.message || 'Unknown replication failure';
971
+
972
+ if (Array.isArray(results)) {
973
+ const failureEntry = results.find((entry) => {
974
+ if (!entry) return false;
975
+ if (entry.status === 'rejected') return true;
976
+ if (entry.status === 'fulfilled' && entry.value && typeof entry.value === 'object' && 'success' in entry.value) {
977
+ return entry.value.success === false;
978
+ }
979
+ return false;
980
+ });
981
+
982
+ if (failureEntry) {
983
+ if (failureEntry.status === 'rejected') {
984
+ failureMessage = failureEntry.reason?.message || failureMessage;
985
+ } else if (failureEntry.status === 'fulfilled') {
986
+ failureMessage = failureEntry.value?.error || failureMessage;
987
+ }
988
+ }
989
+ }
990
+
991
+ await this.updateReplicatorLog(log.id, {
992
+ status: 'failed',
993
+ error: failureMessage,
994
+ retryCount: (Number(log.retryCount) || 0) + 1
995
+ });
664
996
  });
665
- if (ok) {
666
- retried++;
667
- } else {
668
- // Retry failed, continue
997
+
998
+ if (processResult.errors.length && this.config.verbose) {
999
+ for (const { item, error } of processResult.errors) {
1000
+ console.warn(`[ReplicatorPlugin] Failed to retry log ${item?.id ?? 'unknown'}: ${error.message}`);
669
1001
  }
670
1002
  }
671
1003
 
@@ -687,7 +1019,7 @@ export class ReplicatorPlugin extends Plugin {
687
1019
  this.stats.lastSync = new Date().toISOString();
688
1020
 
689
1021
  for (const resourceName in this.database.resources) {
690
- if (normalizeResourceName(resourceName) === normalizeResourceName('plg_replicator_logs')) continue;
1022
+ if (resourceName === this.logResourceName) continue;
691
1023
 
692
1024
  if (replicator.shouldReplicateResource(resourceName)) {
693
1025
  this.emit('plg:replicator:sync-resource', { resourceName, replicatorId });
@@ -706,8 +1038,52 @@ export class ReplicatorPlugin extends Plugin {
706
1038
  const records = Array.isArray(page) ? page : (page.items || []);
707
1039
  if (records.length === 0) break;
708
1040
 
709
- for (const record of records) {
710
- await replicator.replicate(resourceName, 'insert', record, record.id);
1041
+ const poolResult = await PromisePool
1042
+ .withConcurrency(this.config.replicatorConcurrency)
1043
+ .for(records)
1044
+ .process(async (record) => {
1045
+ const sanitizedRecord = this.filterInternalFields(record);
1046
+ const [replicateOk, replicateError, result] = await tryFn(() =>
1047
+ replicator.replicate(resourceName, 'insert', sanitizedRecord, sanitizedRecord.id)
1048
+ );
1049
+
1050
+ if (!replicateOk) {
1051
+ if (this.config.verbose) {
1052
+ console.warn(`[ReplicatorPlugin] syncAllData failed for ${replicator.name || replicator.id} on ${resourceName}: ${replicateError.message}`);
1053
+ }
1054
+
1055
+ this.stats.totalErrors += 1;
1056
+ this.emit('plg:replicator:error', {
1057
+ replicator: replicator.name || replicator.id,
1058
+ resourceName,
1059
+ operation: 'insert',
1060
+ recordId: sanitizedRecord.id,
1061
+ error: replicateError.message
1062
+ });
1063
+
1064
+ if (this.config.logErrors && this.database) {
1065
+ await this.logError(replicator, resourceName, 'insert', sanitizedRecord.id, sanitizedRecord, replicateError);
1066
+ }
1067
+
1068
+ throw replicateError;
1069
+ }
1070
+
1071
+ this.stats.totalReplications += 1;
1072
+ this.emit('plg:replicator:replicated', {
1073
+ replicator: replicator.name || replicator.id,
1074
+ resourceName,
1075
+ operation: 'insert',
1076
+ recordId: sanitizedRecord.id,
1077
+ result,
1078
+ success: true
1079
+ });
1080
+
1081
+ return result;
1082
+ });
1083
+
1084
+ if (poolResult.errors.length > 0) {
1085
+ const { error } = poolResult.errors[0];
1086
+ throw error;
711
1087
  }
712
1088
 
713
1089
  offset += pageSize;
@@ -721,26 +1097,27 @@ export class ReplicatorPlugin extends Plugin {
721
1097
  async stop() {
722
1098
  const [ok, error] = await tryFn(async () => {
723
1099
  if (this.replicators && this.replicators.length > 0) {
724
- const cleanupPromises = this.replicators.map(async (replicator) => {
725
- const [replicatorOk, replicatorError] = await tryFn(async () => {
726
- if (replicator && typeof replicator.stop === 'function') {
727
- await replicator.stop();
728
- }
729
- });
1100
+ await PromisePool
1101
+ .withConcurrency(this.config.stopConcurrency)
1102
+ .for(this.replicators)
1103
+ .process(async (replicator) => {
1104
+ const [replicatorOk, replicatorError] = await tryFn(async () => {
1105
+ if (replicator && typeof replicator.stop === 'function') {
1106
+ await replicator.stop();
1107
+ }
1108
+ });
730
1109
 
731
- if (!replicatorOk) {
732
- if (this.config.verbose) {
733
- console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
1110
+ if (!replicatorOk) {
1111
+ if (this.config.verbose) {
1112
+ console.warn(`[ReplicatorPlugin] Failed to stop replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
1113
+ }
1114
+ this.emit('plg:replicator:stop-error', {
1115
+ replicator: replicator.name || replicator.id || 'unknown',
1116
+ driver: replicator.driver || 'unknown',
1117
+ error: replicatorError.message
1118
+ });
734
1119
  }
735
- this.emit('plg:replicator:stop-error', {
736
- replicator: replicator.name || replicator.id || 'unknown',
737
- driver: replicator.driver || 'unknown',
738
- error: replicatorError.message
739
- });
740
- }
741
- });
742
-
743
- await Promise.allSettled(cleanupPromises);
1120
+ });
744
1121
  }
745
1122
 
746
1123
  // Remove database hooks
@@ -777,4 +1154,4 @@ export class ReplicatorPlugin extends Plugin {
777
1154
  });
778
1155
  }
779
1156
  }
780
- }
1157
+ }