s3db.js 7.3.4 → 7.3.6

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,24 +291,26 @@ 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;
212
- // Mapping function - when there's only transformer, use original resource
213
- if (resource && !targetResourceName) targetResourceName = resource;
214
- // Object: { resource, transform }
305
+ // Mapping function - when there's only transformer, use original resource
306
+ if (typeof entry === 'function') return resource;
307
+ // Object: { resource, transform }
215
308
  if (typeof entry === 'object' && entry.resource) return entry.resource;
216
309
  return resource;
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,14 @@ 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
+ }
248
346
  }
249
347
 
250
348
  this.emit('batch_replicated', {
@@ -265,19 +363,25 @@ class S3dbReplicator extends BaseReplicator {
265
363
 
266
364
  async testConnection() {
267
365
  const [ok, err] = await tryFn(async () => {
268
- if (!this.targetDatabase) {
269
- await this.initialize(this.database);
270
- }
366
+ if (!this.targetDatabase) throw new Error('No target database configured');
367
+
271
368
  // Try to list resources to test connection
272
- await this.targetDatabase.listResources();
369
+ if (typeof this.targetDatabase.connect === 'function') {
370
+ await this.targetDatabase.connect();
371
+ }
372
+
273
373
  return true;
274
374
  });
275
- if (ok) return true;
276
- this.emit('connection_error', {
277
- replicator: this.name,
278
- error: err.message
279
- });
280
- return false;
375
+
376
+ if (!ok) {
377
+ if (this.config.verbose) {
378
+ console.warn(`[S3dbReplicator] Connection test failed: ${err.message}`);
379
+ }
380
+ this.emit('connection_error', { replicator: this.name, error: err.message });
381
+ return false;
382
+ }
383
+
384
+ return true;
281
385
  }
282
386
 
283
387
  async getStatus() {
@@ -308,22 +412,22 @@ class S3dbReplicator extends BaseReplicator {
308
412
  // If no action is specified, just check if resource is configured
309
413
  if (!action) return true;
310
414
 
311
- // Support for all configuration styles
312
- // If it's an array of objects, check actions
415
+ // Array of multiple destinations - check if any supports the action
313
416
  if (Array.isArray(entry)) {
314
417
  for (const item of entry) {
315
418
  if (typeof item === 'object' && item.resource) {
316
419
  if (item.actions && Array.isArray(item.actions)) {
317
420
  if (item.actions.includes(action)) return true;
318
421
  } else {
319
- return true; // If no actions, accept all
422
+ return true; // If no actions specified, accept all
320
423
  }
321
- } else if (typeof item === 'string' || typeof item === 'function') {
322
- return true;
424
+ } else if (typeof item === 'string') {
425
+ return true; // String destinations accept all actions
323
426
  }
324
427
  }
325
428
  return false;
326
429
  }
430
+
327
431
  if (typeof entry === 'object' && entry.resource) {
328
432
  if (entry.actions && Array.isArray(entry.actions)) {
329
433
  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,
@@ -272,6 +278,9 @@ class SqsReplicator extends BaseReplicator {
272
278
  });
273
279
  if (ok) return result;
274
280
  const errorMessage = err?.message || err || 'Unknown error';
281
+ if (this.config.verbose) {
282
+ console.warn(`[SqsReplicator] Batch replication failed for ${resource}: ${errorMessage}`);
283
+ }
275
284
  this.emit('batch_replicator_error', {
276
285
  replicator: this.name,
277
286
  resource,
@@ -295,6 +304,9 @@ class SqsReplicator extends BaseReplicator {
295
304
  return true;
296
305
  });
297
306
  if (ok) return true;
307
+ if (this.config.verbose) {
308
+ console.warn(`[SqsReplicator] Connection test failed: ${err.message}`);
309
+ }
298
310
  this.emit('connection_error', {
299
311
  replicator: this.name,
300
312
  error: err.message