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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "7.3.5",
3
+ "version": "7.3.8",
4
4
  "description": "Use AWS S3, the world's most reliable document storage, as a database with this ORM.",
5
5
  "main": "dist/s3db.cjs.js",
6
6
  "module": "dist/s3db.es.js",
@@ -10,6 +10,9 @@
10
10
  "jsdelivr": "dist/s3db.iife.min.js",
11
11
  "author": "@stone/martech",
12
12
  "license": "UNLICENSED",
13
+ "bin": {
14
+ "s3db-mcp": "./mcp/server.js"
15
+ },
13
16
  "repository": {
14
17
  "type": "git",
15
18
  "url": "git+https://github.com/forattini-dev/s3db.js.git"
@@ -26,6 +29,9 @@
26
29
  "type": "module",
27
30
  "sideEffects": false,
28
31
  "imports": {
32
+ "#mcp/*": "./mcp/*",
33
+ "#dist/*": "./dist/*",
34
+ "#examples/*": "./examples/*",
29
35
  "#src/*": "./src/*",
30
36
  "#tests/*": "./tests/*"
31
37
  },
@@ -428,6 +428,15 @@ export class FilesystemCache extends Cache {
428
428
 
429
429
  async _clear(prefix) {
430
430
  try {
431
+ // Check if directory exists before trying to read it
432
+ if (!await this._fileExists(this.directory)) {
433
+ // Directory doesn't exist, nothing to clear
434
+ if (this.enableStats) {
435
+ this.stats.clears++;
436
+ }
437
+ return true;
438
+ }
439
+
431
440
  const files = await readdir(this.directory);
432
441
  const cacheFiles = files.filter(file => {
433
442
  if (!file.startsWith(this.prefix)) return false;
@@ -61,19 +61,13 @@ function normalizeResourceName(name) {
61
61
  * 2. Map: source resource → destination resource name:
62
62
  * resources: { users: 'people' }
63
63
  *
64
- * 3. Map: source resource → [destination, transformer]:
65
- * resources: { users: ['people', (el) => ({ ...el, fullName: el.name })] }
64
+ * 3. Map: source resource → { resource, transform }:
65
+ * resources: { users: { resource: 'people', transform: fn } }
66
66
  *
67
- * 4. Map: source resource → { resource, transformer }:
68
- * resources: { users: { resource: 'people', transformer: fn } }
69
- *
70
- * 5. Map: source resource → array of objects (multi-destination):
71
- * resources: { users: [ { resource: 'people', transformer: fn } ] }
72
- *
73
- * 6. Map: source resource → function (transformer only):
67
+ * 4. Map: source resource → function (transformer only):
74
68
  * resources: { users: (el) => ({ ...el, fullName: el.name }) }
75
69
  *
76
- * All forms can be mixed and matched. The transformer is always available (default: identity function).
70
+ * The transform function is optional and applies to data before replication.
77
71
  *
78
72
  * === Example Plugin Configurations ===
79
73
  *
@@ -93,10 +87,10 @@ function normalizeResourceName(name) {
93
87
  * ]
94
88
  * });
95
89
  *
96
- * // Advanced mapping with transformer
90
+ * // Advanced mapping with transform
97
91
  * new ReplicatorPlugin({
98
92
  * replicators: [
99
- * { driver: 's3db', client: dbB, config: { resources: { users: ['people', (el) => ({ ...el, fullName: el.name })] } } }
93
+ * { driver: 's3db', client: dbB, config: { resources: { users: { resource: 'people', transform: (el) => ({ ...el, fullName: el.name }) } } } }
100
94
  * ]
101
95
  * });
102
96
  *
@@ -177,27 +171,42 @@ export class ReplicatorPlugin extends Plugin {
177
171
  }
178
172
 
179
173
  resource.on('insert', async (data) => {
180
- try {
174
+ const [ok, error] = await tryFn(async () => {
181
175
  const completeData = { ...data, createdAt: new Date().toISOString() };
182
176
  await plugin.processReplicatorEvent('insert', resource.name, completeData.id, completeData);
183
- } catch (error) {
177
+ });
178
+
179
+ if (!ok) {
180
+ if (this.config.verbose) {
181
+ console.warn(`[ReplicatorPlugin] Insert event failed for resource ${resource.name}: ${error.message}`);
182
+ }
184
183
  this.emit('error', { operation: 'insert', error: error.message, resource: resource.name });
185
184
  }
186
185
  });
187
186
 
188
187
  resource.on('update', async (data, beforeData) => {
189
- try {
188
+ const [ok, error] = await tryFn(async () => {
190
189
  const completeData = { ...data, updatedAt: new Date().toISOString() };
191
190
  await plugin.processReplicatorEvent('update', resource.name, completeData.id, completeData, beforeData);
192
- } catch (error) {
191
+ });
192
+
193
+ if (!ok) {
194
+ if (this.config.verbose) {
195
+ console.warn(`[ReplicatorPlugin] Update event failed for resource ${resource.name}: ${error.message}`);
196
+ }
193
197
  this.emit('error', { operation: 'update', error: error.message, resource: resource.name });
194
198
  }
195
199
  });
196
200
 
197
201
  resource.on('delete', async (data) => {
198
- try {
202
+ const [ok, error] = await tryFn(async () => {
199
203
  await plugin.processReplicatorEvent('delete', resource.name, data.id, data);
200
- } catch (error) {
204
+ });
205
+
206
+ if (!ok) {
207
+ if (this.config.verbose) {
208
+ console.warn(`[ReplicatorPlugin] Delete event failed for resource ${resource.name}: ${error.message}`);
209
+ }
201
210
  this.emit('error', { operation: 'delete', error: error.message, resource: resource.name });
202
211
  }
203
212
  });
@@ -219,14 +228,19 @@ export class ReplicatorPlugin extends Plugin {
219
228
  async setup(database) {
220
229
  this.database = database;
221
230
 
222
- try {
231
+ const [initOk, initError] = await tryFn(async () => {
223
232
  await this.initializeReplicators(database);
224
- } catch (error) {
225
- this.emit('error', { operation: 'setup', error: error.message });
226
- throw error;
233
+ });
234
+
235
+ if (!initOk) {
236
+ if (this.config.verbose) {
237
+ console.warn(`[ReplicatorPlugin] Replicator initialization failed: ${initError.message}`);
238
+ }
239
+ this.emit('error', { operation: 'setup', error: initError.message });
240
+ throw initError;
227
241
  }
228
242
 
229
- try {
243
+ const [logOk, logError] = await tryFn(async () => {
230
244
  if (this.config.replicatorLogResource) {
231
245
  const logRes = await database.createResource({
232
246
  name: this.config.replicatorLogResource,
@@ -243,8 +257,16 @@ export class ReplicatorPlugin extends Plugin {
243
257
  }
244
258
  });
245
259
  }
246
- } catch (error) {
247
- // Log resource creation failed, continue without it
260
+ });
261
+
262
+ if (!logOk) {
263
+ if (this.config.verbose) {
264
+ console.warn(`[ReplicatorPlugin] Failed to create log resource ${this.config.replicatorLogResource}: ${logError.message}`);
265
+ }
266
+ this.emit('replicator_log_resource_creation_error', {
267
+ resourceName: this.config.replicatorLogResource,
268
+ error: logError.message
269
+ });
248
270
  }
249
271
 
250
272
  await this.uploadMetadataFile(database);
@@ -307,15 +329,24 @@ export class ReplicatorPlugin extends Plugin {
307
329
  async retryWithBackoff(operation, maxRetries = 3) {
308
330
  let lastError;
309
331
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
310
- try {
311
- return await operation();
312
- } catch (error) {
332
+ const [ok, error] = await tryFn(operation);
333
+
334
+ if (ok) {
335
+ return ok;
336
+ } else {
313
337
  lastError = error;
338
+ if (this.config.verbose) {
339
+ console.warn(`[ReplicatorPlugin] Retry attempt ${attempt}/${maxRetries} failed: ${error.message}`);
340
+ }
341
+
314
342
  if (attempt === maxRetries) {
315
343
  throw error;
316
344
  }
317
345
  // Simple backoff: wait 1s, 2s, 4s...
318
346
  const delay = Math.pow(2, attempt - 1) * 1000;
347
+ if (this.config.verbose) {
348
+ console.warn(`[ReplicatorPlugin] Waiting ${delay}ms before retry...`);
349
+ }
319
350
  await new Promise(resolve => setTimeout(resolve, delay));
320
351
  }
321
352
  }
@@ -323,7 +354,7 @@ export class ReplicatorPlugin extends Plugin {
323
354
  }
324
355
 
325
356
  async logError(replicator, resourceName, operation, recordId, data, error) {
326
- try {
357
+ const [ok, logError] = await tryFn(async () => {
327
358
  const logResourceName = this.config.replicatorLogResource;
328
359
  if (this.database && this.database.resources && this.database.resources[logResourceName]) {
329
360
  const logResource = this.database.resources[logResourceName];
@@ -338,8 +369,20 @@ export class ReplicatorPlugin extends Plugin {
338
369
  status: 'error'
339
370
  });
340
371
  }
341
- } catch (logError) {
342
- // Silent log errors
372
+ });
373
+
374
+ if (!ok) {
375
+ if (this.config.verbose) {
376
+ console.warn(`[ReplicatorPlugin] Failed to log error for ${resourceName}: ${logError.message}`);
377
+ }
378
+ this.emit('replicator_log_error', {
379
+ replicator: replicator.name || replicator.id,
380
+ resourceName,
381
+ operation,
382
+ recordId,
383
+ originalError: error.message,
384
+ logError: logError.message
385
+ });
343
386
  }
344
387
  }
345
388
 
@@ -356,7 +399,7 @@ export class ReplicatorPlugin extends Plugin {
356
399
  }
357
400
 
358
401
  const promises = applicableReplicators.map(async (replicator) => {
359
- try {
402
+ const [ok, error, result] = await tryFn(async () => {
360
403
  const result = await this.retryWithBackoff(
361
404
  () => replicator.replicate(resourceName, operation, data, recordId, beforeData),
362
405
  this.config.maxRetries
@@ -372,7 +415,15 @@ export class ReplicatorPlugin extends Plugin {
372
415
  });
373
416
 
374
417
  return result;
375
- } catch (error) {
418
+ });
419
+
420
+ if (ok) {
421
+ return result;
422
+ } else {
423
+ if (this.config.verbose) {
424
+ console.warn(`[ReplicatorPlugin] Replication failed for ${replicator.name || replicator.id} on ${resourceName}: ${error.message}`);
425
+ }
426
+
376
427
  this.emit('replicator_error', {
377
428
  replicator: replicator.name || replicator.id,
378
429
  resourceName,
@@ -403,12 +454,16 @@ export class ReplicatorPlugin extends Plugin {
403
454
  }
404
455
 
405
456
  const promises = applicableReplicators.map(async (replicator) => {
406
- try {
457
+ const [wrapperOk, wrapperError] = await tryFn(async () => {
407
458
  const [ok, err, result] = await tryFn(() =>
408
459
  replicator.replicate(item.resourceName, item.operation, item.data, item.recordId, item.beforeData)
409
460
  );
410
461
 
411
462
  if (!ok) {
463
+ if (this.config.verbose) {
464
+ console.warn(`[ReplicatorPlugin] Replicator item processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${err.message}`);
465
+ }
466
+
412
467
  this.emit('replicator_error', {
413
468
  replicator: replicator.name || replicator.id,
414
469
  resourceName: item.resourceName,
@@ -434,20 +489,28 @@ export class ReplicatorPlugin extends Plugin {
434
489
  });
435
490
 
436
491
  return { success: true, result };
437
- } catch (error) {
492
+ });
493
+
494
+ if (wrapperOk) {
495
+ return wrapperOk;
496
+ } else {
497
+ if (this.config.verbose) {
498
+ console.warn(`[ReplicatorPlugin] Wrapper processing failed for ${replicator.name || replicator.id} on ${item.resourceName}: ${wrapperError.message}`);
499
+ }
500
+
438
501
  this.emit('replicator_error', {
439
502
  replicator: replicator.name || replicator.id,
440
503
  resourceName: item.resourceName,
441
504
  operation: item.operation,
442
505
  recordId: item.recordId,
443
- error: error.message
506
+ error: wrapperError.message
444
507
  });
445
508
 
446
509
  if (this.config.logErrors && this.database) {
447
- await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, error);
510
+ await this.logError(replicator, item.resourceName, item.operation, item.recordId, item.data, wrapperError);
448
511
  }
449
512
 
450
- return { success: false, error: error.message };
513
+ return { success: false, error: wrapperError.message };
451
514
  }
452
515
  });
453
516
 
@@ -474,9 +537,14 @@ export class ReplicatorPlugin extends Plugin {
474
537
  timestamp: typeof item.timestamp === 'number' ? item.timestamp : Date.now(),
475
538
  createdAt: item.createdAt || new Date().toISOString().slice(0, 10),
476
539
  };
477
- try {
540
+ const [ok, err] = await tryFn(async () => {
478
541
  await logRes.insert(logItem);
479
- } catch (err) {
542
+ });
543
+
544
+ if (!ok) {
545
+ if (this.config.verbose) {
546
+ console.warn(`[ReplicatorPlugin] Failed to log replicator item: ${err.message}`);
547
+ }
480
548
  this.emit('replicator.log.failed', { error: err, item });
481
549
  }
482
550
  }
@@ -611,15 +679,24 @@ export class ReplicatorPlugin extends Plugin {
611
679
  }
612
680
 
613
681
  async cleanup() {
614
- try {
682
+ const [ok, error] = await tryFn(async () => {
615
683
  if (this.replicators && this.replicators.length > 0) {
616
684
  const cleanupPromises = this.replicators.map(async (replicator) => {
617
- try {
685
+ const [replicatorOk, replicatorError] = await tryFn(async () => {
618
686
  if (replicator && typeof replicator.cleanup === 'function') {
619
687
  await replicator.cleanup();
620
688
  }
621
- } catch (error) {
622
- // Silent cleanup errors
689
+ });
690
+
691
+ if (!replicatorOk) {
692
+ if (this.config.verbose) {
693
+ console.warn(`[ReplicatorPlugin] Failed to cleanup replicator ${replicator.name || replicator.id}: ${replicatorError.message}`);
694
+ }
695
+ this.emit('replicator_cleanup_error', {
696
+ replicator: replicator.name || replicator.id || 'unknown',
697
+ driver: replicator.driver || 'unknown',
698
+ error: replicatorError.message
699
+ });
623
700
  }
624
701
  });
625
702
 
@@ -631,8 +708,15 @@ export class ReplicatorPlugin extends Plugin {
631
708
  this.eventListenersInstalled.clear();
632
709
 
633
710
  this.removeAllListeners();
634
- } catch (error) {
635
- // Silent cleanup errors
711
+ });
712
+
713
+ if (!ok) {
714
+ if (this.config.verbose) {
715
+ console.warn(`[ReplicatorPlugin] Failed to cleanup plugin: ${error.message}`);
716
+ }
717
+ this.emit('replicator_plugin_cleanup_error', {
718
+ error: error.message
719
+ });
636
720
  }
637
721
  }
638
722
  }
@@ -115,6 +115,9 @@ class BigqueryReplicator extends BaseReplicator {
115
115
  await super.initialize(database);
116
116
  const [ok, err, sdk] = await tryFn(() => import('@google-cloud/bigquery'));
117
117
  if (!ok) {
118
+ if (this.config.verbose) {
119
+ console.warn(`[BigqueryReplicator] Failed to import BigQuery SDK: ${err.message}`);
120
+ }
118
121
  this.emit('initialization_error', { replicator: this.name, error: err.message });
119
122
  throw err;
120
123
  }
@@ -206,21 +209,32 @@ class BigqueryReplicator extends BaseReplicator {
206
209
  let lastError = null;
207
210
 
208
211
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
209
- try {
212
+ const [ok, error] = await tryFn(async () => {
210
213
  const [updateJob] = await this.bigqueryClient.createQueryJob({
211
214
  query,
212
215
  params,
213
216
  location: this.location
214
217
  });
215
218
  await updateJob.getQueryResults();
216
- job = [updateJob];
219
+ return [updateJob];
220
+ });
221
+
222
+ if (ok) {
223
+ job = ok;
217
224
  break;
218
- } catch (error) {
225
+ } else {
219
226
  lastError = error;
220
227
 
228
+ if (this.config.verbose) {
229
+ console.warn(`[BigqueryReplicator] Update attempt ${attempt} failed: ${error.message}`);
230
+ }
231
+
221
232
  // If it's streaming buffer error and not the last attempt
222
233
  if (error?.message?.includes('streaming buffer') && attempt < maxRetries) {
223
234
  const delaySeconds = 30;
235
+ if (this.config.verbose) {
236
+ console.warn(`[BigqueryReplicator] Retrying in ${delaySeconds} seconds due to streaming buffer issue`);
237
+ }
224
238
  await new Promise(resolve => setTimeout(resolve, delaySeconds * 1000));
225
239
  continue;
226
240
  }
@@ -277,6 +291,12 @@ class BigqueryReplicator extends BaseReplicator {
277
291
  }
278
292
 
279
293
  const success = errors.length === 0;
294
+
295
+ // Log errors if any occurred
296
+ if (errors.length > 0) {
297
+ console.warn(`[BigqueryReplicator] Replication completed with errors for ${resourceName}:`, errors);
298
+ }
299
+
280
300
  this.emit('replicated', {
281
301
  replicator: this.name,
282
302
  resourceName,
@@ -298,6 +318,9 @@ class BigqueryReplicator extends BaseReplicator {
298
318
 
299
319
  if (ok) return result;
300
320
 
321
+ if (this.config.verbose) {
322
+ console.warn(`[BigqueryReplicator] Replication failed for ${resourceName}: ${err.message}`);
323
+ }
301
324
  this.emit('replicator_error', {
302
325
  replicator: this.name,
303
326
  resourceName,
@@ -321,8 +344,19 @@ class BigqueryReplicator extends BaseReplicator {
321
344
  record.id,
322
345
  record.beforeData
323
346
  ));
324
- if (ok) results.push(res);
325
- else errors.push({ id: record.id, error: err.message });
347
+ if (ok) {
348
+ results.push(res);
349
+ } else {
350
+ if (this.config.verbose) {
351
+ console.warn(`[BigqueryReplicator] Batch replication failed for record ${record.id}: ${err.message}`);
352
+ }
353
+ errors.push({ id: record.id, error: err.message });
354
+ }
355
+ }
356
+
357
+ // Log errors if any occurred during batch processing
358
+ if (errors.length > 0) {
359
+ console.warn(`[BigqueryReplicator] Batch replication completed with ${errors.length} error(s) for ${resourceName}:`, errors);
326
360
  }
327
361
 
328
362
  return {
@@ -340,6 +374,9 @@ class BigqueryReplicator extends BaseReplicator {
340
374
  return true;
341
375
  });
342
376
  if (ok) return true;
377
+ if (this.config.verbose) {
378
+ console.warn(`[BigqueryReplicator] Connection test failed: ${err.message}`);
379
+ }
343
380
  this.emit('connection_error', { replicator: this.name, error: err.message });
344
381
  return false;
345
382
  }
@@ -113,6 +113,9 @@ class PostgresReplicator extends BaseReplicator {
113
113
  await super.initialize(database);
114
114
  const [ok, err, sdk] = await tryFn(() => import('pg'));
115
115
  if (!ok) {
116
+ if (this.config.verbose) {
117
+ console.warn(`[PostgresReplicator] Failed to import pg SDK: ${err.message}`);
118
+ }
116
119
  this.emit('initialization_error', {
117
120
  replicator: this.name,
118
121
  error: err.message
@@ -258,6 +261,12 @@ class PostgresReplicator extends BaseReplicator {
258
261
  }
259
262
  }
260
263
  const success = errors.length === 0;
264
+
265
+ // Log errors if any occurred
266
+ if (errors.length > 0) {
267
+ console.warn(`[PostgresReplicator] Replication completed with errors for ${resourceName}:`, errors);
268
+ }
269
+
261
270
  this.emit('replicated', {
262
271
  replicator: this.name,
263
272
  resourceName,
@@ -276,6 +285,9 @@ class PostgresReplicator extends BaseReplicator {
276
285
  };
277
286
  });
278
287
  if (ok) return result;
288
+ if (this.config.verbose) {
289
+ console.warn(`[PostgresReplicator] Replication failed for ${resourceName}: ${err.message}`);
290
+ }
279
291
  this.emit('replicator_error', {
280
292
  replicator: this.name,
281
293
  resourceName,
@@ -298,8 +310,19 @@ class PostgresReplicator extends BaseReplicator {
298
310
  record.id,
299
311
  record.beforeData
300
312
  ));
301
- if (ok) results.push(res);
302
- else errors.push({ id: record.id, error: err.message });
313
+ if (ok) {
314
+ results.push(res);
315
+ } else {
316
+ if (this.config.verbose) {
317
+ console.warn(`[PostgresReplicator] Batch replication failed for record ${record.id}: ${err.message}`);
318
+ }
319
+ errors.push({ id: record.id, error: err.message });
320
+ }
321
+ }
322
+
323
+ // Log errors if any occurred during batch processing
324
+ if (errors.length > 0) {
325
+ console.warn(`[PostgresReplicator] Batch replication completed with ${errors.length} error(s) for ${resourceName}:`, errors);
303
326
  }
304
327
 
305
328
  return {
@@ -316,6 +339,9 @@ class PostgresReplicator extends BaseReplicator {
316
339
  return true;
317
340
  });
318
341
  if (ok) return true;
342
+ if (this.config.verbose) {
343
+ console.warn(`[PostgresReplicator] Connection test failed: ${err.message}`);
344
+ }
319
345
  this.emit('connection_error', { replicator: this.name, error: err.message });
320
346
  return false;
321
347
  }