s3db.js 6.2.0 → 7.0.0

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 (60) hide show
  1. package/PLUGINS.md +2724 -0
  2. package/README.md +372 -469
  3. package/UNLICENSE +24 -0
  4. package/dist/s3db.cjs.js +30057 -18387
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +30043 -18384
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +29730 -18061
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -69
  12. package/src/behaviors/body-only.js +110 -0
  13. package/src/behaviors/body-overflow.js +153 -0
  14. package/src/behaviors/enforce-limits.js +195 -0
  15. package/src/behaviors/index.js +39 -0
  16. package/src/behaviors/truncate-data.js +204 -0
  17. package/src/behaviors/user-managed.js +147 -0
  18. package/src/client.class.js +515 -0
  19. package/src/concerns/base62.js +61 -0
  20. package/src/concerns/calculator.js +204 -0
  21. package/src/concerns/crypto.js +142 -0
  22. package/src/concerns/id.js +8 -0
  23. package/src/concerns/index.js +5 -0
  24. package/src/concerns/try-fn.js +151 -0
  25. package/src/connection-string.class.js +75 -0
  26. package/src/database.class.js +599 -0
  27. package/src/errors.js +261 -0
  28. package/src/index.js +17 -0
  29. package/src/plugins/audit.plugin.js +442 -0
  30. package/src/plugins/cache/cache.class.js +53 -0
  31. package/src/plugins/cache/index.js +6 -0
  32. package/src/plugins/cache/memory-cache.class.js +164 -0
  33. package/src/plugins/cache/s3-cache.class.js +189 -0
  34. package/src/plugins/cache.plugin.js +275 -0
  35. package/src/plugins/consumers/index.js +24 -0
  36. package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
  37. package/src/plugins/consumers/sqs-consumer.js +102 -0
  38. package/src/plugins/costs.plugin.js +81 -0
  39. package/src/plugins/fulltext.plugin.js +473 -0
  40. package/src/plugins/index.js +12 -0
  41. package/src/plugins/metrics.plugin.js +603 -0
  42. package/src/plugins/plugin.class.js +210 -0
  43. package/src/plugins/plugin.obj.js +13 -0
  44. package/src/plugins/queue-consumer.plugin.js +134 -0
  45. package/src/plugins/replicator.plugin.js +769 -0
  46. package/src/plugins/replicators/base-replicator.class.js +85 -0
  47. package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
  48. package/src/plugins/replicators/index.js +44 -0
  49. package/src/plugins/replicators/postgres-replicator.class.js +427 -0
  50. package/src/plugins/replicators/s3db-replicator.class.js +352 -0
  51. package/src/plugins/replicators/sqs-replicator.class.js +427 -0
  52. package/src/resource.class.js +2626 -0
  53. package/src/s3db.d.ts +1263 -0
  54. package/src/schema.class.js +706 -0
  55. package/src/stream/index.js +16 -0
  56. package/src/stream/resource-ids-page-reader.class.js +10 -0
  57. package/src/stream/resource-ids-reader.class.js +63 -0
  58. package/src/stream/resource-reader.class.js +81 -0
  59. package/src/stream/resource-writer.class.js +92 -0
  60. package/src/validator.class.js +97 -0
@@ -0,0 +1,769 @@
1
+ import { isPlainObject } from 'lodash-es';
2
+
3
+ import Plugin from "./plugin.class.js";
4
+ import tryFn from "../concerns/try-fn.js";
5
+ import { createReplicator, validateReplicatorConfig } from "./replicators/index.js";
6
+
7
+ function normalizeResourceName(name) {
8
+ return typeof name === 'string' ? name.trim().toLowerCase() : name;
9
+ }
10
+
11
+ /**
12
+ * ReplicatorPlugin - S3DB replicator System
13
+ *
14
+ * This plugin enables flexible, robust replicator between S3DB databases and other systems.
15
+ *
16
+ * === Plugin-Level Configuration Options ===
17
+ *
18
+ * - persistReplicatorLog (boolean, default: false)
19
+ * If true, the plugin will persist all replicator events to a log resource.
20
+ * If false, no replicator log resource is created or used.
21
+ *
22
+ * - replicatorLogResource (string, default: 'replicator_logs')
23
+ * The name of the resource used to store replicator logs.
24
+ *
25
+ * === replicator Log Resource Structure ===
26
+ *
27
+ * If persistReplicatorLog is true, the following resource is created (if not present):
28
+ *
29
+ * name: <replicatorLogResource>
30
+ * behavior: 'truncate-data'
31
+ * attributes:
32
+ * - id: string|required
33
+ * - resource: string|required
34
+ * - action: string|required
35
+ * - data: object
36
+ * - timestamp: number|required
37
+ * - createdAt: string|required
38
+ * partitions:
39
+ * byDate: { fields: { createdAt: 'string|maxlength:10' } }
40
+ *
41
+ * This enables efficient log truncation and partitioned queries by date.
42
+ *
43
+ * === Replicator Configuration Syntax ===
44
+ *
45
+ * Each replicator entry supports the following options:
46
+ *
47
+ * - driver: 's3db' | 'sqs' | ...
48
+ * - client: (optional) destination database/client instance
49
+ * - config: {
50
+ * connectionString?: string,
51
+ * resources?: <see below>,
52
+ * ...driver-specific options
53
+ * }
54
+ * - resources: <see below> (can be at top-level or inside config)
55
+ *
56
+ * === Supported Resource Mapping Syntaxes ===
57
+ *
58
+ * You can specify which resources to replicate and how, using any of:
59
+ *
60
+ * 1. Array of resource names (replicate to itself):
61
+ * resources: ['users']
62
+ *
63
+ * 2. Map: source resource → destination resource name:
64
+ * resources: { users: 'people' }
65
+ *
66
+ * 3. Map: source resource → [destination, transformer]:
67
+ * resources: { users: ['people', (el) => ({ ...el, fullName: el.name })] }
68
+ *
69
+ * 4. Map: source resource → { resource, transformer }:
70
+ * resources: { users: { resource: 'people', transformer: fn } }
71
+ *
72
+ * 5. Map: source resource → array of objects (multi-destination):
73
+ * resources: { users: [ { resource: 'people', transformer: fn } ] }
74
+ *
75
+ * 6. Map: source resource → function (transformer only):
76
+ * resources: { users: (el) => ({ ...el, fullName: el.name }) }
77
+ *
78
+ * All forms can be mixed and matched. The transformer is always available (default: identity function).
79
+ *
80
+ * === Example Plugin Configurations ===
81
+ *
82
+ * // Basic replicator to another database
83
+ * new ReplicatorPlugin({
84
+ * replicators: [
85
+ * { driver: 's3db', client: dbB, resources: ['users'] }
86
+ * ]
87
+ * });
88
+ *
89
+ * // Replicate with custom log resource and persistence
90
+ * new ReplicatorPlugin({
91
+ * persistReplicatorLog: true,
92
+ * replicatorLogResource: 'custom_logs',
93
+ * replicators: [
94
+ * { driver: 's3db', client: dbB, config: { resources: { users: 'people' } } }
95
+ * ]
96
+ * });
97
+ *
98
+ * // Advanced mapping with transformer
99
+ * new ReplicatorPlugin({
100
+ * replicators: [
101
+ * { driver: 's3db', client: dbB, config: { resources: { users: ['people', (el) => ({ ...el, fullName: el.name })] } } }
102
+ * ]
103
+ * });
104
+ *
105
+ * // replicator using a connection string
106
+ * new ReplicatorPlugin({
107
+ * replicators: [
108
+ * { driver: 's3db', config: { connectionString: 's3://user:pass@bucket/path', resources: ['users'] } }
109
+ * ]
110
+ * });
111
+ *
112
+ * === Default Behaviors and Extensibility ===
113
+ *
114
+ * - If persistReplicatorLog is not set, no log resource is created.
115
+ * - The log resource is only created if it does not already exist.
116
+ * - The plugin supports multiple replicators and drivers.
117
+ * - All resource mapping syntaxes are supported and can be mixed.
118
+ * - The log resource uses the 'truncate-data' behavior for efficient log management.
119
+ * - Partitioning by date enables efficient queries and retention policies.
120
+ *
121
+ * === See also ===
122
+ * - S3dbReplicator for advanced resource mapping logic
123
+ * - SqsReplicator for SQS integration
124
+ * - ReplicatorPlugin tests for usage examples
125
+ */
126
+ export class ReplicatorPlugin extends Plugin {
127
+ constructor(options = {}) {
128
+ super();
129
+ if (options.verbose) {
130
+ console.log('[PLUGIN][CONSTRUCTOR] ReplicatorPlugin constructor called');
131
+ }
132
+ if (options.verbose) {
133
+ console.log('[PLUGIN][constructor] New ReplicatorPlugin instance created with config:', options);
134
+ }
135
+ // Validation for config tests
136
+ if (!options.replicators || !Array.isArray(options.replicators)) {
137
+ throw new Error('ReplicatorPlugin: replicators array is required');
138
+ }
139
+ for (const rep of options.replicators) {
140
+ if (!rep.driver) throw new Error('ReplicatorPlugin: each replicator must have a driver');
141
+ }
142
+ // Aceita apenas os parâmetros válidos
143
+ this.config = {
144
+ verbose: options.verbose ?? false,
145
+ persistReplicatorLog: options.persistReplicatorLog ?? false,
146
+ replicatorLogResource: options.replicatorLogResource ?? 'replicator_logs',
147
+ replicators: options.replicators || [],
148
+ };
149
+ this.replicators = [];
150
+ this.queue = [];
151
+ this.isProcessing = false;
152
+ this.stats = {
153
+ totalOperations: 0,
154
+ totalErrors: 0,
155
+ lastError: null,
156
+ };
157
+ this._installedListeners = [];
158
+ }
159
+
160
+ /**
161
+ * Decompress data if it was compressed
162
+ */
163
+ async decompressData(data) {
164
+ return data;
165
+ }
166
+
167
+ // Helper to filter out internal S3DB fields
168
+ filterInternalFields(obj) {
169
+ if (!obj || typeof obj !== 'object') return obj;
170
+ const filtered = {};
171
+ for (const [key, value] of Object.entries(obj)) {
172
+ if (!key.startsWith('_') && key !== '$overflow' && key !== '$before' && key !== '$after') {
173
+ filtered[key] = value;
174
+ }
175
+ }
176
+ return filtered;
177
+ }
178
+
179
+ installEventListeners(resource) {
180
+ const plugin = this;
181
+ if (plugin.config.verbose) {
182
+ console.log('[PLUGIN] installEventListeners called for:', resource && resource.name, {
183
+ hasDatabase: !!resource.database,
184
+ sameDatabase: resource.database === plugin.database,
185
+ alreadyInstalled: resource._replicatorListenersInstalled,
186
+ resourceObj: resource,
187
+ resourceObjId: resource && resource.id,
188
+ resourceObjType: typeof resource,
189
+ resourceObjIs: resource && Object.is(resource, plugin.database.resources && plugin.database.resources[resource.name]),
190
+ resourceObjEq: resource === (plugin.database.resources && plugin.database.resources[resource.name])
191
+ });
192
+ }
193
+ // Only install listeners on resources belonging to the source database
194
+ if (!resource || resource.name === plugin.config.replicatorLogResource || !resource.database || resource.database !== plugin.database) return;
195
+ if (resource._replicatorListenersInstalled) return;
196
+ resource._replicatorListenersInstalled = true;
197
+ // Track listeners for cleanup
198
+ this._installedListeners.push(resource);
199
+ if (plugin.config.verbose) {
200
+ console.log(`[PLUGIN] installEventListeners INSTALLED for resource: ${resource && resource.name}`);
201
+ }
202
+ // Insert event
203
+ resource.on('insert', async (data) => {
204
+ if (plugin.config.verbose) {
205
+ console.log('[PLUGIN] Listener INSERT on', resource.name, 'plugin.replicators.length:', plugin.replicators.length, plugin.replicators.map(r => ({id: r.id, driver: r.driver})));
206
+ }
207
+ try {
208
+ const completeData = await plugin.getCompleteData(resource, data);
209
+ if (plugin.config.verbose) {
210
+ console.log(`[PLUGIN] Listener INSERT completeData for ${resource.name} id=${data && data.id}:`, completeData);
211
+ }
212
+ await plugin.processReplicatorEvent(resource.name, 'insert', data.id, completeData, null);
213
+ } catch (err) {
214
+ if (plugin.config.verbose) {
215
+ console.error(`[PLUGIN] Listener INSERT error on ${resource.name} id=${data && data.id}:`, err);
216
+ }
217
+ }
218
+ });
219
+
220
+ // Update event
221
+ resource.on('update', async (data) => {
222
+ console.log('[PLUGIN][Listener][UPDATE][START] triggered for resource:', resource.name, 'data:', data);
223
+ const beforeData = data && data.$before;
224
+ if (plugin.config.verbose) {
225
+ console.log('[PLUGIN] Listener UPDATE on', resource.name, 'plugin.replicators.length:', plugin.replicators.length, plugin.replicators.map(r => ({id: r.id, driver: r.driver})), 'data:', data, 'beforeData:', beforeData);
226
+ }
227
+ try {
228
+ // Always fetch the full, current object for update replication
229
+ let completeData;
230
+ const [ok, err, record] = await tryFn(() => resource.get(data.id));
231
+ if (ok && record) {
232
+ completeData = record;
233
+ } else {
234
+ completeData = data;
235
+ }
236
+ await plugin.processReplicatorEvent(resource.name, 'update', data.id, completeData, beforeData);
237
+ } catch (err) {
238
+ if (plugin.config.verbose) {
239
+ console.error(`[PLUGIN] Listener UPDATE erro em ${resource.name} id=${data && data.id}:`, err);
240
+ }
241
+ }
242
+ });
243
+
244
+ // Delete event
245
+ resource.on('delete', async (data, beforeData) => {
246
+ if (plugin.config.verbose) {
247
+ console.log('[PLUGIN] Listener DELETE on', resource.name, 'plugin.replicators.length:', plugin.replicators.length, plugin.replicators.map(r => ({id: r.id, driver: r.driver})));
248
+ }
249
+ try {
250
+ await plugin.processReplicatorEvent(resource.name, 'delete', data.id, null, beforeData);
251
+ } catch (err) {
252
+ if (plugin.config.verbose) {
253
+ console.error(`[PLUGIN] Listener DELETE erro em ${resource.name} id=${data && data.id}:`, err);
254
+ }
255
+ }
256
+ });
257
+ if (plugin.config.verbose) {
258
+ console.log(`[PLUGIN] Listeners instalados para resource: ${resource && resource.name} (insert: ${resource.listenerCount('insert')}, update: ${resource.listenerCount('update')}, delete: ${resource.listenerCount('delete')})`);
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Get complete data by always fetching the full record from the resource
264
+ * This ensures we always have the complete data regardless of behavior or data size
265
+ */
266
+ async getCompleteData(resource, data) {
267
+ // Always get the complete record from the resource to ensure we have all data
268
+ // This handles all behaviors: body-overflow, truncate-data, body-only, etc.
269
+ const [ok, err, completeRecord] = await tryFn(() => resource.get(data.id));
270
+ return ok ? completeRecord : data;
271
+ }
272
+
273
+ async setup(database) {
274
+ console.log('[PLUGIN][SETUP] setup called');
275
+ if (this.config.verbose) {
276
+ console.log('[PLUGIN][setup] called with database:', database && database.name);
277
+ }
278
+ this.database = database;
279
+ // 1. Sempre crie a resource de log antes de qualquer outra coisa
280
+ if (this.config.persistReplicatorLog) {
281
+ let logRes = database.resources[normalizeResourceName(this.config.replicatorLogResource)];
282
+ if (!logRes) {
283
+ logRes = await database.createResource({
284
+ name: this.config.replicatorLogResource,
285
+ behavior: 'truncate-data',
286
+ attributes: {
287
+ id: 'string|required',
288
+ resource: 'string|required',
289
+ action: 'string|required',
290
+ data: 'object',
291
+ timestamp: 'number|required',
292
+ createdAt: 'string|required',
293
+ },
294
+ partitions: {
295
+ byDate: { fields: { 'createdAt': 'string|maxlength:10' } }
296
+ }
297
+ });
298
+ if (this.config.verbose) {
299
+ console.log('[PLUGIN] Log resource created:', this.config.replicatorLogResource, !!logRes);
300
+ }
301
+ }
302
+ database.resources[normalizeResourceName(this.config.replicatorLogResource)] = logRes;
303
+ this.replicatorLog = logRes; // Salva referência para uso futuro
304
+ if (this.config.verbose) {
305
+ console.log('[PLUGIN] Log resource created and registered:', this.config.replicatorLogResource, !!database.resources[normalizeResourceName(this.config.replicatorLogResource)]);
306
+ }
307
+ // Persist the log resource to metadata
308
+ if (typeof database.uploadMetadataFile === 'function') {
309
+ await database.uploadMetadataFile();
310
+ if (this.config.verbose) {
311
+ console.log('[PLUGIN] uploadMetadataFile called. database.resources keys:', Object.keys(database.resources));
312
+ }
313
+ }
314
+ }
315
+ // 2. Só depois inicialize replicators e listeners
316
+ if (this.config.replicators && this.config.replicators.length > 0 && this.replicators.length === 0) {
317
+ await this.initializeReplicators();
318
+ console.log('[PLUGIN][SETUP] after initializeReplicators, replicators.length:', this.replicators.length);
319
+ if (this.config.verbose) {
320
+ console.log('[PLUGIN][setup] After initializeReplicators, replicators.length:', this.replicators.length, this.replicators.map(r => ({id: r.id, driver: r.driver})));
321
+ }
322
+ }
323
+ // Only install event listeners after replicators are initialized
324
+ for (const resourceName in database.resources) {
325
+ if (normalizeResourceName(resourceName) !== normalizeResourceName(this.config.replicatorLogResource)) {
326
+ this.installEventListeners(database.resources[resourceName]);
327
+ }
328
+ }
329
+ database.on('connected', () => {
330
+ for (const resourceName in database.resources) {
331
+ if (normalizeResourceName(resourceName) !== normalizeResourceName(this.config.replicatorLogResource)) {
332
+ this.installEventListeners(database.resources[resourceName]);
333
+ }
334
+ }
335
+ });
336
+ const originalCreateResource = database.createResource.bind(database);
337
+ database.createResource = async (config) => {
338
+ if (this.config.verbose) {
339
+ console.log('[PLUGIN] createResource proxy called for:', config && config.name);
340
+ }
341
+ const resource = await originalCreateResource(config);
342
+ if (resource && resource.name !== this.config.replicatorLogResource) {
343
+ this.installEventListeners(resource);
344
+ }
345
+ return resource;
346
+ };
347
+ database.on('s3db.resourceCreated', (resourceName) => {
348
+ const resource = database.resources[resourceName];
349
+ if (resource && resource.name !== this.config.replicatorLogResource) {
350
+ this.installEventListeners(resource);
351
+ }
352
+ });
353
+
354
+ database.on('s3db.resourceUpdated', (resourceName) => {
355
+ const resource = database.resources[resourceName];
356
+ if (resource && resource.name !== this.config.replicatorLogResource) {
357
+ this.installEventListeners(resource);
358
+ }
359
+ });
360
+ }
361
+
362
+ async initializeReplicators() {
363
+ console.log('[PLUGIN][INIT] initializeReplicators called');
364
+ for (const replicatorConfig of this.config.replicators) {
365
+ try {
366
+ console.log('[PLUGIN][INIT] processing replicatorConfig:', replicatorConfig);
367
+ const driver = replicatorConfig.driver;
368
+ const resources = replicatorConfig.resources;
369
+ const client = replicatorConfig.client;
370
+ const replicator = createReplicator(driver, replicatorConfig, resources, client);
371
+ if (replicator) {
372
+ // Initialize the replicator with the database
373
+ await replicator.initialize(this.database);
374
+
375
+ this.replicators.push({
376
+ id: Math.random().toString(36).slice(2),
377
+ driver,
378
+ config: replicatorConfig,
379
+ resources,
380
+ instance: replicator
381
+ });
382
+ console.log('[PLUGIN][INIT] pushed replicator:', driver, resources);
383
+ } else {
384
+ console.log('[PLUGIN][INIT] createReplicator returned null/undefined for driver:', driver);
385
+ }
386
+ } catch (err) {
387
+ console.error('[PLUGIN][INIT] Error creating replicator:', err);
388
+ }
389
+ }
390
+ }
391
+
392
+ async start() {
393
+ // Plugin is ready
394
+ }
395
+
396
+ async stop() {
397
+ // Stop queue processing
398
+ // this.isProcessing = false; // Removed as per edit hint
399
+ // Process remaining queue items
400
+ // await this.processQueue(); // Removed as per edit hint
401
+ }
402
+
403
+ async processReplicatorEvent(resourceName, operation, recordId, data, beforeData = null) {
404
+ if (this.config.verbose) {
405
+ console.log('[PLUGIN][processReplicatorEvent] replicators.length:', this.replicators.length, this.replicators.map(r => ({id: r.id, driver: r.driver})));
406
+ console.log(`[PLUGIN][processReplicatorEvent] operation: ${operation}, resource: ${resourceName}, recordId: ${recordId}, data:`, data, 'beforeData:', beforeData);
407
+ }
408
+ if (this.config.verbose) {
409
+ console.log(`[PLUGIN] processReplicatorEvent: resource=${resourceName} op=${operation} id=${recordId} data=`, data);
410
+ }
411
+ if (this.config.verbose) {
412
+ console.log(`[PLUGIN] processReplicatorEvent: resource=${resourceName} op=${operation} replicators=${this.replicators.length}`);
413
+ }
414
+ if (this.replicators.length === 0) {
415
+ if (this.config.verbose) {
416
+ console.log('[PLUGIN] No replicators registered');
417
+ }
418
+ return;
419
+ }
420
+ const applicableReplicators = this.replicators.filter(replicator => {
421
+ const should = replicator.instance.shouldReplicateResource(resourceName, operation);
422
+ if (this.config.verbose) {
423
+ console.log(`[PLUGIN] Replicator ${replicator.driver} shouldReplicateResource(${resourceName}, ${operation}):`, should);
424
+ }
425
+ return should;
426
+ });
427
+ if (this.config.verbose) {
428
+ console.log(`[PLUGIN] processReplicatorEvent: applicableReplicators for resource=${resourceName}:`, applicableReplicators.map(r => r.driver));
429
+ }
430
+ if (applicableReplicators.length === 0) {
431
+ if (this.config.verbose) {
432
+ console.log('[PLUGIN] No applicable replicators for resource', resourceName);
433
+ }
434
+ return;
435
+ }
436
+
437
+ // Filtrar campos internos antes de replicar
438
+ const filteredData = this.filterInternalFields(isPlainObject(data) ? data : { raw: data });
439
+ const filteredBeforeData = beforeData ? this.filterInternalFields(isPlainObject(beforeData) ? beforeData : { raw: beforeData }) : null;
440
+
441
+ const item = {
442
+ id: `repl-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
443
+ resourceName,
444
+ operation,
445
+ recordId,
446
+ data: filteredData,
447
+ beforeData: filteredBeforeData,
448
+ timestamp: new Date().toISOString(),
449
+ attempts: 0
450
+ };
451
+
452
+ // Log replicator attempt
453
+ const logId = await this.logreplicator(item);
454
+
455
+ // Sempre processa imediatamente (sincrono)
456
+ const [ok, err, result] = await tryFn(async () => this.processreplicatorItem(item));
457
+ if (ok) {
458
+ if (logId) {
459
+ await this.updatereplicatorLog(logId, {
460
+ status: result.success ? 'success' : 'failed',
461
+ attempts: 1,
462
+ error: result.success ? '' : JSON.stringify(result.results)
463
+ });
464
+ }
465
+ this.stats.totalOperations++;
466
+ if (result.success) {
467
+ this.stats.successfulOperations++;
468
+ } else {
469
+ this.stats.failedOperations++;
470
+ }
471
+ } else {
472
+ if (logId) {
473
+ await this.updatereplicatorLog(logId, {
474
+ status: 'failed',
475
+ attempts: 1,
476
+ error: err.message
477
+ });
478
+ }
479
+ this.stats.failedOperations++;
480
+ }
481
+ }
482
+
483
+ async processreplicatorItem(item) {
484
+ if (this.config.verbose) {
485
+ console.log('[PLUGIN][processreplicatorItem] called with item:', item);
486
+ }
487
+ const applicableReplicators = this.replicators.filter(replicator => {
488
+ const should = replicator.instance.shouldReplicateResource(item.resourceName, item.operation);
489
+ if (this.config.verbose) {
490
+ console.log(`[PLUGIN] processreplicatorItem: Replicator ${replicator.driver} shouldReplicateResource(${item.resourceName}, ${item.operation}):`, should);
491
+ }
492
+ return should;
493
+ });
494
+ if (this.config.verbose) {
495
+ console.log(`[PLUGIN] processreplicatorItem: applicableReplicators for resource=${item.resourceName}:`, applicableReplicators.map(r => r.driver));
496
+ }
497
+ if (applicableReplicators.length === 0) {
498
+ if (this.config.verbose) {
499
+ console.log('[PLUGIN] processreplicatorItem: No applicable replicators for resource', item.resourceName);
500
+ }
501
+ return { success: true, skipped: true, reason: 'no_applicable_replicators' };
502
+ }
503
+
504
+ const results = [];
505
+
506
+ for (const replicator of applicableReplicators) {
507
+ let result;
508
+ let ok, err;
509
+ if (this.config.verbose) {
510
+ console.log('[PLUGIN] processReplicatorItem', {
511
+ resource: item.resourceName,
512
+ operation: item.operation,
513
+ data: item.data,
514
+ beforeData: item.beforeData,
515
+ replicator: replicator.instance?.constructor?.name
516
+ });
517
+ }
518
+ if (replicator.instance && replicator.instance.constructor && replicator.instance.constructor.name === 'S3dbReplicator') {
519
+ [ok, err, result] = await tryFn(() =>
520
+ replicator.instance.replicate({
521
+ resource: item.resourceName,
522
+ operation: item.operation,
523
+ data: item.data,
524
+ id: item.recordId,
525
+ beforeData: item.beforeData
526
+ })
527
+ );
528
+ } else {
529
+ [ok, err, result] = await tryFn(() =>
530
+ replicator.instance.replicate(
531
+ item.resourceName,
532
+ item.operation,
533
+ item.data,
534
+ item.recordId,
535
+ item.beforeData
536
+ )
537
+ );
538
+ }
539
+ // Remove or comment out this line:
540
+ // console.log('[PLUGIN] replicate result', { ok, err, result });
541
+ results.push({
542
+ replicatorId: replicator.id,
543
+ driver: replicator.driver,
544
+ success: result && result.success,
545
+ error: result && result.error,
546
+ skipped: result && result.skipped
547
+ });
548
+ }
549
+
550
+ return {
551
+ success: results.every(r => r.success || r.skipped),
552
+ results
553
+ };
554
+ }
555
+
556
+ async logreplicator(item) {
557
+ // Use sempre a referência salva
558
+ const logRes = this.replicatorLog || this.database.resources[normalizeResourceName(this.config.replicatorLogResource)];
559
+ if (!logRes) {
560
+ if (this.config.verbose) {
561
+ console.error('[PLUGIN] replicator log resource not found!');
562
+ }
563
+ if (this.database) {
564
+ if (this.config.verbose) {
565
+ console.warn('[PLUGIN] database.resources keys:', Object.keys(this.database.resources));
566
+ }
567
+ if (this.database.options && this.database.options.connectionString) {
568
+ if (this.config.verbose) {
569
+ console.warn('[PLUGIN] database connectionString:', this.database.options.connectionString);
570
+ }
571
+ }
572
+ }
573
+ this.emit('replicator.log.failed', { error: 'replicator log resource not found', item });
574
+ return;
575
+ }
576
+ // Corrigir campos obrigatórios do log resource
577
+ const logItem = {
578
+ id: item.id || `repl-${Date.now()}-${Math.random().toString(36).slice(2)}`,
579
+ resource: item.resource || item.resourceName || '',
580
+ action: item.operation || item.action || '',
581
+ data: item.data || {},
582
+ timestamp: typeof item.timestamp === 'number' ? item.timestamp : Date.now(),
583
+ createdAt: item.createdAt || new Date().toISOString().slice(0, 10),
584
+ };
585
+ try {
586
+ await logRes.insert(logItem);
587
+ } catch (err) {
588
+ if (this.config.verbose) {
589
+ console.error('[PLUGIN] Error writing to replicator log:', err);
590
+ }
591
+ this.emit('replicator.log.failed', { error: err, item });
592
+ }
593
+ }
594
+
595
+ async updatereplicatorLog(logId, updates) {
596
+ if (!this.replicatorLog) return;
597
+
598
+ const [ok, err] = await tryFn(async () => {
599
+ await this.replicatorLog.update(logId, {
600
+ ...updates,
601
+ lastAttempt: new Date().toISOString()
602
+ });
603
+ });
604
+ if (!ok) {
605
+ this.emit('replicator.updateLog.failed', { error: err.message, logId, updates });
606
+ }
607
+ }
608
+
609
+ // Utility methods
610
+ async getreplicatorStats() {
611
+ const replicatorStats = await Promise.all(
612
+ this.replicators.map(async (replicator) => {
613
+ const status = await replicator.instance.getStatus();
614
+ return {
615
+ id: replicator.id,
616
+ driver: replicator.driver,
617
+ config: replicator.config,
618
+ status
619
+ };
620
+ })
621
+ );
622
+
623
+ return {
624
+ replicators: replicatorStats,
625
+ queue: {
626
+ length: this.queue.length,
627
+ isProcessing: this.isProcessing
628
+ },
629
+ stats: this.stats,
630
+ lastSync: this.stats.lastSync
631
+ };
632
+ }
633
+
634
+ async getreplicatorLogs(options = {}) {
635
+ if (!this.replicatorLog) {
636
+ return [];
637
+ }
638
+
639
+ const {
640
+ resourceName,
641
+ operation,
642
+ status,
643
+ limit = 100,
644
+ offset = 0
645
+ } = options;
646
+
647
+ let query = {};
648
+
649
+ if (resourceName) {
650
+ query.resourceName = resourceName;
651
+ }
652
+
653
+ if (operation) {
654
+ query.operation = operation;
655
+ }
656
+
657
+ if (status) {
658
+ query.status = status;
659
+ }
660
+
661
+ const logs = await this.replicatorLog.list(query);
662
+
663
+ // Apply pagination
664
+ return logs.slice(offset, offset + limit);
665
+ }
666
+
667
+ async retryFailedreplicators() {
668
+ if (!this.replicatorLog) {
669
+ return { retried: 0 };
670
+ }
671
+
672
+ const failedLogs = await this.replicatorLog.list({
673
+ status: 'failed'
674
+ });
675
+
676
+ let retried = 0;
677
+
678
+ for (const log of failedLogs) {
679
+ const [ok, err] = await tryFn(async () => {
680
+ // Re-queue the replicator
681
+ await this.processReplicatorEvent(
682
+ log.resourceName,
683
+ log.operation,
684
+ log.recordId,
685
+ log.data
686
+ );
687
+ });
688
+ if (ok) {
689
+ retried++;
690
+ } else {
691
+ if (this.config.verbose) {
692
+ console.error('Failed to retry replicator:', err);
693
+ }
694
+ }
695
+ }
696
+
697
+ return { retried };
698
+ }
699
+
700
+ async syncAllData(replicatorId) {
701
+ const replicator = this.replicators.find(r => r.id === replicatorId);
702
+ if (!replicator) {
703
+ throw new Error(`Replicator not found: ${replicatorId}`);
704
+ }
705
+
706
+ this.stats.lastSync = new Date().toISOString();
707
+
708
+ for (const resourceName in this.database.resources) {
709
+ if (normalizeResourceName(resourceName) === normalizeResourceName('replicator_logs')) continue;
710
+
711
+ if (replicator.instance.shouldReplicateResource(resourceName)) {
712
+ this.emit('replicator.sync.resource', { resourceName, replicatorId });
713
+
714
+ const resource = this.database.resources[resourceName];
715
+ const allRecords = await resource.getAll();
716
+
717
+ for (const record of allRecords) {
718
+ await replicator.instance.replicate(resourceName, 'insert', record, record.id);
719
+ }
720
+ }
721
+ }
722
+
723
+ this.emit('replicator.sync.completed', { replicatorId, stats: this.stats });
724
+ }
725
+
726
+ async cleanup() {
727
+ if (this.config.verbose) {
728
+ console.log('[PLUGIN][CLEANUP] Cleaning up ReplicatorPlugin');
729
+ }
730
+ // Remove all event listeners installed on resources
731
+ if (this._installedListeners && Array.isArray(this._installedListeners)) {
732
+ for (const resource of this._installedListeners) {
733
+ if (resource && typeof resource.removeAllListeners === 'function') {
734
+ resource.removeAllListeners('insert');
735
+ resource.removeAllListeners('update');
736
+ resource.removeAllListeners('delete');
737
+ }
738
+ resource._replicatorListenersInstalled = false;
739
+ }
740
+ this._installedListeners = [];
741
+ }
742
+ // Remove all event listeners from the database
743
+ if (this.database && typeof this.database.removeAllListeners === 'function') {
744
+ this.database.removeAllListeners();
745
+ }
746
+ // Cleanup all replicator instances
747
+ if (this.replicators && Array.isArray(this.replicators)) {
748
+ for (const rep of this.replicators) {
749
+ if (rep.instance && typeof rep.instance.cleanup === 'function') {
750
+ await rep.instance.cleanup();
751
+ }
752
+ }
753
+ this.replicators = [];
754
+ }
755
+ // Clear other internal state
756
+ this.queue = [];
757
+ this.isProcessing = false;
758
+ this.stats = {
759
+ totalOperations: 0,
760
+ totalErrors: 0,
761
+ lastError: null,
762
+ };
763
+ if (this.config.verbose) {
764
+ console.log('[PLUGIN][CLEANUP] ReplicatorPlugin cleanup complete');
765
+ }
766
+ }
767
+ }
768
+
769
+ export default ReplicatorPlugin;