s3db.js 7.3.5 → 7.3.8

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.
@@ -48,18 +48,16 @@ class S3dbReplicator extends BaseReplicator {
48
48
  }
49
49
 
50
50
  _normalizeResources(resources) {
51
- // Supports array, object, function, string
51
+ // Supports object, function, string, and arrays of destination configurations
52
52
  if (!resources) return {};
53
53
  if (Array.isArray(resources)) {
54
54
  const map = {};
55
55
  for (const res of resources) {
56
56
  if (typeof res === 'string') map[normalizeResourceName(res)] = res;
57
- else if (Array.isArray(res) && typeof res[0] === 'string') map[normalizeResourceName(res[0])] = res;
58
57
  else if (typeof res === 'object' && res.resource) {
59
- // Array of objects with resource/action/transformer
60
- map[normalizeResourceName(res.resource)] = { ...res };
58
+ // Objects with resource/transform/actions - keep as is
59
+ map[normalizeResourceName(res.resource)] = res;
61
60
  }
62
- // Do NOT set actions: ['insert'] or any default actions here
63
61
  }
64
62
  return map;
65
63
  }
@@ -69,20 +67,19 @@ class S3dbReplicator extends BaseReplicator {
69
67
  const normSrc = normalizeResourceName(src);
70
68
  if (typeof dest === 'string') map[normSrc] = dest;
71
69
  else if (Array.isArray(dest)) {
72
- // Array of destinations/objects/transformers
70
+ // Array of multiple destinations - support multi-destination replication
73
71
  map[normSrc] = dest.map(item => {
74
72
  if (typeof item === 'string') return item;
75
- if (typeof item === 'function') return item;
76
73
  if (typeof item === 'object' && item.resource) {
77
- // Copy all fields (resource, transformer, actions, etc.)
78
- return { ...item };
74
+ // Keep object items as is
75
+ return item;
79
76
  }
80
77
  return item;
81
78
  });
82
79
  } else if (typeof dest === 'function') map[normSrc] = dest;
83
80
  else if (typeof dest === 'object' && dest.resource) {
84
- // Copy all fields (resource, transformer, actions, etc.)
85
- map[normSrc] = { ...dest };
81
+ // Support { resource, transform/transformer } format - keep as is
82
+ map[normSrc] = dest;
86
83
  }
87
84
  }
88
85
  return map;
@@ -90,10 +87,6 @@ class S3dbReplicator extends BaseReplicator {
90
87
  if (typeof resources === 'function') {
91
88
  return resources;
92
89
  }
93
- if (typeof resources === 'string') {
94
- const map = { [normalizeResourceName(resources)]: resources };
95
- return map;
96
- }
97
90
  return {};
98
91
  }
99
92
 
@@ -110,27 +103,34 @@ class S3dbReplicator extends BaseReplicator {
110
103
  }
111
104
 
112
105
  async initialize(database) {
113
- try {
114
106
  await super.initialize(database);
107
+
108
+ const [ok, err] = await tryFn(async () => {
115
109
  if (this.client) {
116
110
  this.targetDatabase = this.client;
117
111
  } else if (this.connectionString) {
118
- const targetConfig = {
119
- connectionString: this.connectionString,
120
- region: this.region,
121
- keyPrefix: this.keyPrefix,
122
- verbose: this.config.verbose || false
123
- };
124
- this.targetDatabase = new S3db(targetConfig);
125
- await this.targetDatabase.connect();
112
+ const targetConfig = {
113
+ connectionString: this.connectionString,
114
+ region: this.region,
115
+ keyPrefix: this.keyPrefix,
116
+ verbose: this.config.verbose || false
117
+ };
118
+ this.targetDatabase = new S3db(targetConfig);
119
+ await this.targetDatabase.connect();
126
120
  } else {
127
121
  throw new Error('S3dbReplicator: No client or connectionString provided');
128
122
  }
129
- this.emit('connected', {
130
- replicator: this.name,
131
- target: this.connectionString || 'client-provided'
123
+
124
+ this.emit('connected', {
125
+ replicator: this.name,
126
+ target: this.connectionString || 'client-provided'
127
+ });
132
128
  });
133
- } catch (err) {
129
+
130
+ if (!ok) {
131
+ if (this.config.verbose) {
132
+ console.warn(`[S3dbReplicator] Initialization failed: ${err.message}`);
133
+ }
134
134
  throw err;
135
135
  }
136
136
  }
@@ -154,21 +154,95 @@ class S3dbReplicator extends BaseReplicator {
154
154
  }
155
155
 
156
156
  const normResource = normalizeResourceName(resource);
157
- const destResource = this._resolveDestResource(normResource, payload);
158
- const destResourceObj = this._getDestResourceObj(destResource);
157
+ const entry = this.resourcesMap[normResource];
159
158
 
160
- // Apply transformer before replicating
161
- const transformedData = this._applyTransformer(normResource, payload);
159
+ if (!entry) {
160
+ throw new Error(`[S3dbReplicator] Resource not configured: ${resource}`);
161
+ }
162
+
163
+ // Handle multi-destination arrays
164
+ if (Array.isArray(entry)) {
165
+ const results = [];
166
+ for (const destConfig of entry) {
167
+ const [ok, error, result] = await tryFn(async () => {
168
+ return await this._replicateToSingleDestination(destConfig, normResource, op, payload, id);
169
+ });
170
+
171
+ if (!ok) {
172
+ if (this.config && this.config.verbose) {
173
+ console.warn(`[S3dbReplicator] Failed to replicate to destination ${JSON.stringify(destConfig)}: ${error.message}`);
174
+ }
175
+ throw error;
176
+ }
177
+ results.push(result);
178
+ }
179
+ return results;
180
+ } else {
181
+ // Single destination
182
+ const [ok, error, result] = await tryFn(async () => {
183
+ return await this._replicateToSingleDestination(entry, normResource, op, payload, id);
184
+ });
185
+
186
+ if (!ok) {
187
+ if (this.config && this.config.verbose) {
188
+ console.warn(`[S3dbReplicator] Failed to replicate to destination ${JSON.stringify(entry)}: ${error.message}`);
189
+ }
190
+ throw error;
191
+ }
192
+ return result;
193
+ }
194
+ }
195
+
196
+ async _replicateToSingleDestination(destConfig, sourceResource, operation, data, recordId) {
197
+ // Determine destination resource name
198
+ let destResourceName;
199
+ if (typeof destConfig === 'string') {
200
+ destResourceName = destConfig;
201
+ } else if (typeof destConfig === 'object' && destConfig.resource) {
202
+ destResourceName = destConfig.resource;
203
+ } else {
204
+ destResourceName = sourceResource;
205
+ }
206
+
207
+ // Check if this destination supports the operation
208
+ if (typeof destConfig === 'object' && destConfig.actions && Array.isArray(destConfig.actions)) {
209
+ if (!destConfig.actions.includes(operation)) {
210
+ return { skipped: true, reason: 'action_not_supported', action: operation, destination: destResourceName };
211
+ }
212
+ }
213
+
214
+ const destResourceObj = this._getDestResourceObj(destResourceName);
162
215
 
216
+ // Apply appropriate transformer for this destination
217
+ let transformedData;
218
+ if (typeof destConfig === 'object' && destConfig.transform && typeof destConfig.transform === 'function') {
219
+ transformedData = destConfig.transform(data);
220
+ // Ensure ID is preserved
221
+ if (transformedData && data && data.id && !transformedData.id) {
222
+ transformedData.id = data.id;
223
+ }
224
+ } else if (typeof destConfig === 'object' && destConfig.transformer && typeof destConfig.transformer === 'function') {
225
+ transformedData = destConfig.transformer(data);
226
+ // Ensure ID is preserved
227
+ if (transformedData && data && data.id && !transformedData.id) {
228
+ transformedData.id = data.id;
229
+ }
230
+ } else {
231
+ transformedData = data;
232
+ }
233
+
234
+ // Fallback: if transformer returns undefined/null, use original data
235
+ if (!transformedData && data) transformedData = data;
236
+
163
237
  let result;
164
- if (op === 'insert') {
238
+ if (operation === 'insert') {
165
239
  result = await destResourceObj.insert(transformedData);
166
- } else if (op === 'update') {
167
- result = await destResourceObj.update(id, transformedData);
168
- } else if (op === 'delete') {
169
- result = await destResourceObj.delete(id);
240
+ } else if (operation === 'update') {
241
+ result = await destResourceObj.update(recordId, transformedData);
242
+ } else if (operation === 'delete') {
243
+ result = await destResourceObj.delete(recordId);
170
244
  } else {
171
- throw new Error(`Invalid operation: ${op}. Supported operations are: insert, update, delete`);
245
+ throw new Error(`Invalid operation: ${operation}. Supported operations are: insert, update, delete`);
172
246
  }
173
247
 
174
248
  return result;
@@ -179,18 +253,34 @@ class S3dbReplicator extends BaseReplicator {
179
253
  const entry = this.resourcesMap[normResource];
180
254
  let result;
181
255
  if (!entry) return data;
182
- // Array: [resource, transformer]
183
- if (Array.isArray(entry) && typeof entry[1] === 'function') {
184
- result = entry[1](data);
256
+
257
+ // Array of multiple destinations - use first transform found
258
+ if (Array.isArray(entry)) {
259
+ for (const item of entry) {
260
+ if (typeof item === 'object' && item.transform && typeof item.transform === 'function') {
261
+ result = item.transform(data);
262
+ break;
263
+ } else if (typeof item === 'object' && item.transformer && typeof item.transformer === 'function') {
264
+ result = item.transformer(data);
265
+ break;
266
+ }
267
+ }
268
+ if (!result) result = data;
269
+ } else if (typeof entry === 'object') {
270
+ // Prefer transform, fallback to transformer for backwards compatibility
271
+ if (typeof entry.transform === 'function') {
272
+ result = entry.transform(data);
273
+ } else if (typeof entry.transformer === 'function') {
274
+ result = entry.transformer(data);
275
+ }
185
276
  } else if (typeof entry === 'function') {
277
+ // Function directly as transformer
186
278
  result = entry(data);
187
- } else if (typeof entry === 'object') {
188
- if (typeof entry.transform === 'function') result = entry.transform(data);
189
- else if (typeof entry.transformer === 'function') result = entry.transformer(data);
190
279
  } else {
191
280
  result = data;
192
281
  }
193
- // Ensure that id is always present
282
+
283
+ // Ensure that id is always present
194
284
  if (result && data && data.id && !result.id) result.id = data.id;
195
285
  // Fallback: if transformer returns undefined/null, use original data
196
286
  if (!result && data) result = data;
@@ -201,11 +291,14 @@ class S3dbReplicator extends BaseReplicator {
201
291
  const normResource = normalizeResourceName(resource);
202
292
  const entry = this.resourcesMap[normResource];
203
293
  if (!entry) return resource;
204
- // Array: [resource, transformer]
294
+
295
+ // Array of multiple destinations - use first resource found
205
296
  if (Array.isArray(entry)) {
206
- if (typeof entry[0] === 'string') return entry[0];
207
- if (typeof entry[0] === 'object' && entry[0].resource) return entry[0].resource;
208
- if (typeof entry[0] === 'function') return resource; // fallback
297
+ for (const item of entry) {
298
+ if (typeof item === 'string') return item;
299
+ if (typeof item === 'object' && item.resource) return item.resource;
300
+ }
301
+ return resource; // fallback
209
302
  }
210
303
  // String mapping
211
304
  if (typeof entry === 'string') return entry;
@@ -217,8 +310,7 @@ class S3dbReplicator extends BaseReplicator {
217
310
  }
218
311
 
219
312
  _getDestResourceObj(resource) {
220
- if (!this.client || !this.client.resources) return null;
221
- const available = Object.keys(this.client.resources);
313
+ const available = Object.keys(this.client.resources || {});
222
314
  const norm = normalizeResourceName(resource);
223
315
  const found = available.find(r => normalizeResourceName(r) === norm);
224
316
  if (!found) {
@@ -243,8 +335,19 @@ class S3dbReplicator extends BaseReplicator {
243
335
  data: record.data,
244
336
  beforeData: record.beforeData
245
337
  }));
246
- if (ok) results.push(result);
247
- else errors.push({ id: record.id, error: err.message });
338
+ if (ok) {
339
+ results.push(result);
340
+ } else {
341
+ if (this.config.verbose) {
342
+ console.warn(`[S3dbReplicator] Batch replication failed for record ${record.id}: ${err.message}`);
343
+ }
344
+ errors.push({ id: record.id, error: err.message });
345
+ }
346
+ }
347
+
348
+ // Log errors if any occurred during batch processing
349
+ if (errors.length > 0) {
350
+ console.warn(`[S3dbReplicator] Batch replication completed with ${errors.length} error(s) for ${resourceName}:`, errors);
248
351
  }
249
352
 
250
353
  this.emit('batch_replicated', {
@@ -265,19 +368,25 @@ class S3dbReplicator extends BaseReplicator {
265
368
 
266
369
  async testConnection() {
267
370
  const [ok, err] = await tryFn(async () => {
268
- if (!this.targetDatabase) {
269
- await this.initialize(this.database);
270
- }
371
+ if (!this.targetDatabase) throw new Error('No target database configured');
372
+
271
373
  // Try to list resources to test connection
272
- await this.targetDatabase.listResources();
374
+ if (typeof this.targetDatabase.connect === 'function') {
375
+ await this.targetDatabase.connect();
376
+ }
377
+
273
378
  return true;
274
379
  });
275
- if (ok) return true;
276
- this.emit('connection_error', {
277
- replicator: this.name,
278
- error: err.message
279
- });
280
- return false;
380
+
381
+ if (!ok) {
382
+ if (this.config.verbose) {
383
+ console.warn(`[S3dbReplicator] Connection test failed: ${err.message}`);
384
+ }
385
+ this.emit('connection_error', { replicator: this.name, error: err.message });
386
+ return false;
387
+ }
388
+
389
+ return true;
281
390
  }
282
391
 
283
392
  async getStatus() {
@@ -308,22 +417,22 @@ class S3dbReplicator extends BaseReplicator {
308
417
  // If no action is specified, just check if resource is configured
309
418
  if (!action) return true;
310
419
 
311
- // Support for all configuration styles
312
- // If it's an array of objects, check actions
420
+ // Array of multiple destinations - check if any supports the action
313
421
  if (Array.isArray(entry)) {
314
422
  for (const item of entry) {
315
423
  if (typeof item === 'object' && item.resource) {
316
424
  if (item.actions && Array.isArray(item.actions)) {
317
425
  if (item.actions.includes(action)) return true;
318
426
  } else {
319
- return true; // If no actions, accept all
427
+ return true; // If no actions specified, accept all
320
428
  }
321
- } else if (typeof item === 'string' || typeof item === 'function') {
322
- return true;
429
+ } else if (typeof item === 'string') {
430
+ return true; // String destinations accept all actions
323
431
  }
324
432
  }
325
433
  return false;
326
434
  }
435
+
327
436
  if (typeof entry === 'object' && entry.resource) {
328
437
  if (entry.actions && Array.isArray(entry.actions)) {
329
438
  return entry.actions.includes(action);
@@ -95,7 +95,7 @@ class SqsReplicator extends BaseReplicator {
95
95
 
96
96
  if (!entry) return data;
97
97
 
98
- // Check for transform function in resource config
98
+ // Support both transform and transformer (backwards compatibility)
99
99
  if (typeof entry.transform === 'function') {
100
100
  result = entry.transform(data);
101
101
  } else if (typeof entry.transformer === 'function') {
@@ -146,6 +146,9 @@ class SqsReplicator extends BaseReplicator {
146
146
  if (!this.sqsClient) {
147
147
  const [ok, err, sdk] = await tryFn(() => import('@aws-sdk/client-sqs'));
148
148
  if (!ok) {
149
+ if (this.config.verbose) {
150
+ console.warn(`[SqsReplicator] Failed to import SQS SDK: ${err.message}`);
151
+ }
149
152
  this.emit('initialization_error', {
150
153
  replicator: this.name,
151
154
  error: err.message
@@ -199,6 +202,9 @@ class SqsReplicator extends BaseReplicator {
199
202
  return { success: true, results };
200
203
  });
201
204
  if (ok) return result;
205
+ if (this.config.verbose) {
206
+ console.warn(`[SqsReplicator] Replication failed for ${resource}: ${err.message}`);
207
+ }
202
208
  this.emit('replicator_error', {
203
209
  replicator: this.name,
204
210
  resource,
@@ -254,6 +260,11 @@ class SqsReplicator extends BaseReplicator {
254
260
  }
255
261
  }
256
262
  }
263
+ // Log errors if any occurred during batch processing
264
+ if (errors.length > 0) {
265
+ console.warn(`[SqsReplicator] Batch replication completed with ${errors.length} error(s) for ${resource}:`, errors);
266
+ }
267
+
257
268
  this.emit('batch_replicated', {
258
269
  replicator: this.name,
259
270
  resource,
@@ -272,6 +283,9 @@ class SqsReplicator extends BaseReplicator {
272
283
  });
273
284
  if (ok) return result;
274
285
  const errorMessage = err?.message || err || 'Unknown error';
286
+ if (this.config.verbose) {
287
+ console.warn(`[SqsReplicator] Batch replication failed for ${resource}: ${errorMessage}`);
288
+ }
275
289
  this.emit('batch_replicator_error', {
276
290
  replicator: this.name,
277
291
  resource,
@@ -295,6 +309,9 @@ class SqsReplicator extends BaseReplicator {
295
309
  return true;
296
310
  });
297
311
  if (ok) return true;
312
+ if (this.config.verbose) {
313
+ console.warn(`[SqsReplicator] Connection test failed: ${err.message}`);
314
+ }
298
315
  this.emit('connection_error', {
299
316
  replicator: this.name,
300
317
  error: err.message