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/PLUGINS.md +1285 -157
- package/dist/s3db.cjs.js +314 -83
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.es.js +314 -83
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +314 -83
- package/dist/s3db.iife.min.js +1 -1
- package/mcp/server.js +1410 -0
- package/package.json +7 -1
- package/src/plugins/cache/filesystem-cache.class.js +9 -0
- package/src/plugins/replicator.plugin.js +130 -46
- package/src/plugins/replicators/bigquery-replicator.class.js +42 -5
- package/src/plugins/replicators/postgres-replicator.class.js +28 -2
- package/src/plugins/replicators/s3db-replicator.class.js +177 -68
- package/src/plugins/replicators/sqs-replicator.class.js +18 -1
|
@@ -48,18 +48,16 @@ class S3dbReplicator extends BaseReplicator {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
_normalizeResources(resources) {
|
|
51
|
-
// Supports
|
|
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
|
-
//
|
|
60
|
-
map[normalizeResourceName(res.resource)] =
|
|
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
|
|
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
|
-
//
|
|
78
|
-
return
|
|
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
|
-
//
|
|
85
|
-
map[normSrc] =
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
123
|
+
|
|
124
|
+
this.emit('connected', {
|
|
125
|
+
replicator: this.name,
|
|
126
|
+
target: this.connectionString || 'client-provided'
|
|
127
|
+
});
|
|
132
128
|
});
|
|
133
|
-
|
|
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
|
|
158
|
-
const destResourceObj = this._getDestResourceObj(destResource);
|
|
157
|
+
const entry = this.resourcesMap[normResource];
|
|
159
158
|
|
|
160
|
-
|
|
161
|
-
|
|
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 (
|
|
238
|
+
if (operation === 'insert') {
|
|
165
239
|
result = await destResourceObj.insert(transformedData);
|
|
166
|
-
} else if (
|
|
167
|
-
result = await destResourceObj.update(
|
|
168
|
-
} else if (
|
|
169
|
-
result = await destResourceObj.delete(
|
|
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: ${
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
-
|
|
294
|
+
|
|
295
|
+
// Array of multiple destinations - use first resource found
|
|
205
296
|
if (Array.isArray(entry)) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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)
|
|
247
|
-
|
|
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
|
-
|
|
270
|
-
}
|
|
371
|
+
if (!this.targetDatabase) throw new Error('No target database configured');
|
|
372
|
+
|
|
271
373
|
// Try to list resources to test connection
|
|
272
|
-
|
|
374
|
+
if (typeof this.targetDatabase.connect === 'function') {
|
|
375
|
+
await this.targetDatabase.connect();
|
|
376
|
+
}
|
|
377
|
+
|
|
273
378
|
return true;
|
|
274
379
|
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
//
|
|
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'
|
|
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
|
-
//
|
|
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
|