s3db.js 6.2.0 → 7.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PLUGINS.md +2724 -0
- package/README.md +372 -469
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +12105 -19396
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +12090 -19393
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +12103 -19398
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +44 -38
- package/src/behaviors/body-only.js +110 -0
- package/src/behaviors/body-overflow.js +153 -0
- package/src/behaviors/enforce-limits.js +195 -0
- package/src/behaviors/index.js +39 -0
- package/src/behaviors/truncate-data.js +204 -0
- package/src/behaviors/user-managed.js +147 -0
- package/src/client.class.js +515 -0
- package/src/concerns/base62.js +61 -0
- package/src/concerns/calculator.js +204 -0
- package/src/concerns/crypto.js +159 -0
- package/src/concerns/id.js +8 -0
- package/src/concerns/index.js +5 -0
- package/src/concerns/try-fn.js +151 -0
- package/src/connection-string.class.js +75 -0
- package/src/database.class.js +599 -0
- package/src/errors.js +261 -0
- package/src/index.js +17 -0
- package/src/plugins/audit.plugin.js +442 -0
- package/src/plugins/cache/cache.class.js +53 -0
- package/src/plugins/cache/index.js +6 -0
- package/src/plugins/cache/memory-cache.class.js +164 -0
- package/src/plugins/cache/s3-cache.class.js +189 -0
- package/src/plugins/cache.plugin.js +275 -0
- package/src/plugins/consumers/index.js +24 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
- package/src/plugins/consumers/sqs-consumer.js +102 -0
- package/src/plugins/costs.plugin.js +81 -0
- package/src/plugins/fulltext.plugin.js +473 -0
- package/src/plugins/index.js +12 -0
- package/src/plugins/metrics.plugin.js +603 -0
- package/src/plugins/plugin.class.js +210 -0
- package/src/plugins/plugin.obj.js +13 -0
- package/src/plugins/queue-consumer.plugin.js +134 -0
- package/src/plugins/replicator.plugin.js +769 -0
- package/src/plugins/replicators/base-replicator.class.js +85 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
- package/src/plugins/replicators/index.js +44 -0
- package/src/plugins/replicators/postgres-replicator.class.js +427 -0
- package/src/plugins/replicators/s3db-replicator.class.js +352 -0
- package/src/plugins/replicators/sqs-replicator.class.js +427 -0
- package/src/resource.class.js +2626 -0
- package/src/s3db.d.ts +1263 -0
- package/src/schema.class.js +706 -0
- package/src/stream/index.js +16 -0
- package/src/stream/resource-ids-page-reader.class.js +10 -0
- package/src/stream/resource-ids-reader.class.js +63 -0
- package/src/stream/resource-reader.class.js +81 -0
- package/src/stream/resource-writer.class.js +92 -0
- 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;
|