s3db.js 7.2.0 → 7.2.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 +200 -10
- package/dist/s3db.cjs.js +61 -30
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +14 -15
- package/dist/s3db.es.js +61 -30
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +61 -30
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +5 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +102 -68
- package/src/plugins/replicators/postgres-replicator.class.js +19 -109
- package/src/plugins/replicators/s3db-replicator.class.js +21 -49
- package/src/plugins/replicators/sqs-replicator.class.js +17 -116
- package/src/s3db.d.ts +14 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "s3db.js",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.1",
|
|
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",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
],
|
|
26
26
|
"type": "module",
|
|
27
27
|
"sideEffects": false,
|
|
28
|
+
"imports": {
|
|
29
|
+
"#src/*": "./src/*",
|
|
30
|
+
"#tests/*": "./tests/*"
|
|
31
|
+
},
|
|
28
32
|
"exports": {
|
|
29
33
|
".": {
|
|
30
34
|
"import": "./dist/s3db.es.js",
|
|
@@ -1,55 +1,36 @@
|
|
|
1
|
+
import tryFn from "#src/concerns/try-fn.js";
|
|
2
|
+
|
|
1
3
|
import BaseReplicator from './base-replicator.class.js';
|
|
2
|
-
import tryFn from "../../concerns/try-fn.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* BigQuery Replicator
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* ⚠️ REQUIRED DEPENDENCY: You must install the Google Cloud BigQuery SDK to use this replicator:
|
|
10
|
-
*
|
|
6
|
+
* BigQuery Replicator - Replicate data to Google BigQuery tables
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ REQUIRED DEPENDENCY: You must install the Google Cloud BigQuery SDK:
|
|
11
9
|
* ```bash
|
|
12
|
-
* npm install @google-cloud/bigquery
|
|
13
|
-
* # or
|
|
14
|
-
* yarn add @google-cloud/bigquery
|
|
15
|
-
* # or
|
|
16
10
|
* pnpm add @google-cloud/bigquery
|
|
17
11
|
* ```
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* @
|
|
21
|
-
* @
|
|
22
|
-
* @
|
|
23
|
-
* @
|
|
24
|
-
* @
|
|
25
|
-
*
|
|
26
|
-
* @config {Object|string} resources[resourceName] - Resource configuration
|
|
27
|
-
* @config {string} resources[resourceName].table - Table name for this resource
|
|
28
|
-
* @config {Array} resources[resourceName].actions - Array of actions to replicate (insert, update, delete)
|
|
29
|
-
* @config {string} resources[resourceName] - Short form: just the table name (equivalent to { actions: ['insert'], table: tableName })
|
|
30
|
-
*
|
|
12
|
+
*
|
|
13
|
+
* Configuration:
|
|
14
|
+
* @param {string} projectId - Google Cloud project ID (required)
|
|
15
|
+
* @param {string} datasetId - BigQuery dataset ID (required)
|
|
16
|
+
* @param {Object} credentials - Service account credentials object (optional)
|
|
17
|
+
* @param {string} location - BigQuery dataset location/region (default: 'US')
|
|
18
|
+
* @param {string} logTable - Table name for operation logging (optional)
|
|
19
|
+
*
|
|
31
20
|
* @example
|
|
32
21
|
* new BigqueryReplicator({
|
|
33
22
|
* projectId: 'my-gcp-project',
|
|
34
23
|
* datasetId: 'analytics',
|
|
35
|
-
*
|
|
36
|
-
* credentials: require('./gcp-service-account.json'),
|
|
37
|
-
* logTable: 's3db_replicator_log'
|
|
24
|
+
* credentials: JSON.parse(Buffer.from(GOOGLE_CREDENTIALS, 'base64').toString())
|
|
38
25
|
* }, {
|
|
39
|
-
* users:
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* { actions: ['insert'], table: 'urls_table_v2' },
|
|
45
|
-
* ],
|
|
46
|
-
* clicks: 'clicks_table' // equivalent to { actions: ['insert'], table: 'clicks_table' }
|
|
26
|
+
* users: {
|
|
27
|
+
* table: 'users_table',
|
|
28
|
+
* transform: (data) => ({ ...data, ip: data.ip || 'unknown' })
|
|
29
|
+
* },
|
|
30
|
+
* orders: 'orders_table'
|
|
47
31
|
* })
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* - The target tables must exist and have columns matching the resource attributes (id is required as primary key)
|
|
51
|
-
* - The log table must have columns: resource_name, operation, record_id, data, timestamp, source
|
|
52
|
-
* - Uses @google-cloud/bigquery SDK
|
|
32
|
+
*
|
|
33
|
+
* See PLUGINS.md for comprehensive configuration documentation.
|
|
53
34
|
*/
|
|
54
35
|
class BigqueryReplicator extends BaseReplicator {
|
|
55
36
|
constructor(config = {}, resources = {}) {
|
|
@@ -73,24 +54,27 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
73
54
|
// Short form: just table name
|
|
74
55
|
parsed[resourceName] = [{
|
|
75
56
|
table: config,
|
|
76
|
-
actions: ['insert']
|
|
57
|
+
actions: ['insert'],
|
|
58
|
+
transform: null
|
|
77
59
|
}];
|
|
78
60
|
} else if (Array.isArray(config)) {
|
|
79
61
|
// Array form: multiple table mappings
|
|
80
62
|
parsed[resourceName] = config.map(item => {
|
|
81
63
|
if (typeof item === 'string') {
|
|
82
|
-
return { table: item, actions: ['insert'] };
|
|
64
|
+
return { table: item, actions: ['insert'], transform: null };
|
|
83
65
|
}
|
|
84
66
|
return {
|
|
85
67
|
table: item.table,
|
|
86
|
-
actions: item.actions || ['insert']
|
|
68
|
+
actions: item.actions || ['insert'],
|
|
69
|
+
transform: item.transform || null
|
|
87
70
|
};
|
|
88
71
|
});
|
|
89
72
|
} else if (typeof config === 'object') {
|
|
90
73
|
// Single object form
|
|
91
74
|
parsed[resourceName] = [{
|
|
92
75
|
table: config.table,
|
|
93
|
-
actions: config.actions || ['insert']
|
|
76
|
+
actions: config.actions || ['insert'],
|
|
77
|
+
transform: config.transform || null
|
|
94
78
|
}];
|
|
95
79
|
}
|
|
96
80
|
}
|
|
@@ -118,6 +102,9 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
118
102
|
if (invalidActions.length > 0) {
|
|
119
103
|
errors.push(`Invalid actions for resource '${resourceName}': ${invalidActions.join(', ')}. Valid actions: ${validActions.join(', ')}`);
|
|
120
104
|
}
|
|
105
|
+
if (tableConfig.transform && typeof tableConfig.transform !== 'function') {
|
|
106
|
+
errors.push(`Transform must be a function for resource '${resourceName}'`);
|
|
107
|
+
}
|
|
121
108
|
}
|
|
122
109
|
}
|
|
123
110
|
|
|
@@ -162,7 +149,19 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
162
149
|
|
|
163
150
|
return this.resources[resourceName]
|
|
164
151
|
.filter(tableConfig => tableConfig.actions.includes(operation))
|
|
165
|
-
.map(tableConfig =>
|
|
152
|
+
.map(tableConfig => ({
|
|
153
|
+
table: tableConfig.table,
|
|
154
|
+
transform: tableConfig.transform
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
applyTransform(data, transformFn) {
|
|
159
|
+
if (!transformFn) return data;
|
|
160
|
+
|
|
161
|
+
let transformedData = JSON.parse(JSON.stringify(data));
|
|
162
|
+
if (transformedData._length) delete transformedData._length;
|
|
163
|
+
|
|
164
|
+
return transformFn(transformedData);
|
|
166
165
|
}
|
|
167
166
|
|
|
168
167
|
async replicate(resourceName, operation, data, id, beforeData = null) {
|
|
@@ -175,8 +174,8 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
175
174
|
return { skipped: true, reason: 'action_not_included' };
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
const
|
|
179
|
-
if (
|
|
177
|
+
const tableConfigs = this.getTablesForResource(resourceName, operation);
|
|
178
|
+
if (tableConfigs.length === 0) {
|
|
180
179
|
return { skipped: true, reason: 'no_tables_for_action' };
|
|
181
180
|
}
|
|
182
181
|
|
|
@@ -185,50 +184,80 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
185
184
|
|
|
186
185
|
const [ok, err, result] = await tryFn(async () => {
|
|
187
186
|
const dataset = this.bigqueryClient.dataset(this.datasetId);
|
|
187
|
+
|
|
188
188
|
// Replicate to all applicable tables
|
|
189
|
-
for (const
|
|
189
|
+
for (const tableConfig of tableConfigs) {
|
|
190
190
|
const [okTable, errTable] = await tryFn(async () => {
|
|
191
|
-
const table = dataset.table(
|
|
191
|
+
const table = dataset.table(tableConfig.table);
|
|
192
192
|
let job;
|
|
193
|
+
|
|
193
194
|
if (operation === 'insert') {
|
|
194
|
-
const
|
|
195
|
-
job = await table.insert([
|
|
195
|
+
const transformedData = this.applyTransform(data, tableConfig.transform);
|
|
196
|
+
job = await table.insert([transformedData]);
|
|
196
197
|
} else if (operation === 'update') {
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
const query = `UPDATE \`${this.projectId}.${this.datasetId}.${
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
198
|
+
const transformedData = this.applyTransform(data, tableConfig.transform);
|
|
199
|
+
const keys = Object.keys(transformedData).filter(k => k !== 'id');
|
|
200
|
+
const setClause = keys.map(k => `${k} = @${k}`).join(', ');
|
|
201
|
+
const params = { id, ...transformedData };
|
|
202
|
+
const query = `UPDATE \`${this.projectId}.${this.datasetId}.${tableConfig.table}\` SET ${setClause} WHERE id = @id`;
|
|
203
|
+
|
|
204
|
+
// Retry logic for streaming buffer issues
|
|
205
|
+
const maxRetries = 2;
|
|
206
|
+
let lastError = null;
|
|
207
|
+
|
|
208
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
209
|
+
try {
|
|
210
|
+
const [updateJob] = await this.bigqueryClient.createQueryJob({
|
|
211
|
+
query,
|
|
212
|
+
params,
|
|
213
|
+
location: this.location
|
|
214
|
+
});
|
|
215
|
+
await updateJob.getQueryResults();
|
|
216
|
+
job = [updateJob];
|
|
217
|
+
break;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
lastError = error;
|
|
220
|
+
|
|
221
|
+
// If it's streaming buffer error and not the last attempt
|
|
222
|
+
if (error?.message?.includes('streaming buffer') && attempt < maxRetries) {
|
|
223
|
+
const delaySeconds = 30;
|
|
224
|
+
await new Promise(resolve => setTimeout(resolve, delaySeconds * 1000));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!job) throw lastError;
|
|
208
233
|
} else if (operation === 'delete') {
|
|
209
|
-
const query = `DELETE FROM \`${this.projectId}.${this.datasetId}.${
|
|
234
|
+
const query = `DELETE FROM \`${this.projectId}.${this.datasetId}.${tableConfig.table}\` WHERE id = @id`;
|
|
210
235
|
const [deleteJob] = await this.bigqueryClient.createQueryJob({
|
|
211
236
|
query,
|
|
212
|
-
params: { id }
|
|
237
|
+
params: { id },
|
|
238
|
+
location: this.location
|
|
213
239
|
});
|
|
214
240
|
await deleteJob.getQueryResults();
|
|
215
241
|
job = [deleteJob];
|
|
216
242
|
} else {
|
|
217
243
|
throw new Error(`Unsupported operation: ${operation}`);
|
|
218
244
|
}
|
|
245
|
+
|
|
219
246
|
results.push({
|
|
220
|
-
table:
|
|
247
|
+
table: tableConfig.table,
|
|
221
248
|
success: true,
|
|
222
249
|
jobId: job[0]?.id
|
|
223
250
|
});
|
|
224
251
|
});
|
|
252
|
+
|
|
225
253
|
if (!okTable) {
|
|
226
254
|
errors.push({
|
|
227
|
-
table:
|
|
255
|
+
table: tableConfig.table,
|
|
228
256
|
error: errTable.message
|
|
229
257
|
});
|
|
230
258
|
}
|
|
231
259
|
}
|
|
260
|
+
|
|
232
261
|
// Log operation if logTable is configured
|
|
233
262
|
if (this.logTable) {
|
|
234
263
|
const [okLog, errLog] = await tryFn(async () => {
|
|
@@ -246,25 +275,29 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
246
275
|
// Don't fail the main operation if logging fails
|
|
247
276
|
}
|
|
248
277
|
}
|
|
278
|
+
|
|
249
279
|
const success = errors.length === 0;
|
|
250
280
|
this.emit('replicated', {
|
|
251
281
|
replicator: this.name,
|
|
252
282
|
resourceName,
|
|
253
283
|
operation,
|
|
254
284
|
id,
|
|
255
|
-
tables,
|
|
285
|
+
tables: tableConfigs.map(t => t.table),
|
|
256
286
|
results,
|
|
257
287
|
errors,
|
|
258
288
|
success
|
|
259
289
|
});
|
|
290
|
+
|
|
260
291
|
return {
|
|
261
292
|
success,
|
|
262
293
|
results,
|
|
263
294
|
errors,
|
|
264
|
-
tables
|
|
295
|
+
tables: tableConfigs.map(t => t.table)
|
|
265
296
|
};
|
|
266
297
|
});
|
|
298
|
+
|
|
267
299
|
if (ok) return result;
|
|
300
|
+
|
|
268
301
|
this.emit('replicator_error', {
|
|
269
302
|
replicator: this.name,
|
|
270
303
|
resourceName,
|
|
@@ -272,6 +305,7 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
272
305
|
id,
|
|
273
306
|
error: err.message
|
|
274
307
|
});
|
|
308
|
+
|
|
275
309
|
return { success: false, error: err.message };
|
|
276
310
|
}
|
|
277
311
|
|
|
@@ -1,124 +1,34 @@
|
|
|
1
|
+
import tryFn from "#src/concerns/try-fn.js";
|
|
2
|
+
import BaseReplicator from './base-replicator.class.js';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* This replicator executes real SQL operations (INSERT, UPDATE, DELETE) on PostgreSQL tables
|
|
5
|
-
* using the official pg (node-postgres) library. It maps s3db resources to database tables
|
|
6
|
-
* and performs actual database operations for each replicator event.
|
|
7
|
-
*
|
|
8
|
-
* ⚠️ REQUIRED DEPENDENCY: You must install the PostgreSQL client library to use this replicator:
|
|
5
|
+
* PostgreSQL Replicator - Replicate data to PostgreSQL tables
|
|
9
6
|
*
|
|
7
|
+
* ⚠️ REQUIRED DEPENDENCY: You must install the PostgreSQL client library:
|
|
10
8
|
* ```bash
|
|
11
|
-
* npm install pg
|
|
12
|
-
* # or
|
|
13
|
-
* yarn add pg
|
|
14
|
-
* # or
|
|
15
9
|
* pnpm add pg
|
|
16
10
|
* ```
|
|
17
11
|
*
|
|
18
|
-
*
|
|
19
|
-
* @
|
|
20
|
-
* @
|
|
21
|
-
* @
|
|
22
|
-
* @
|
|
23
|
-
* @
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @property {boolean} [logOperations=false] - Whether to log SQL operations to console for debugging
|
|
28
|
-
* @property {string} [schema='public'] - Default database schema to use when tableMapping doesn't specify schema
|
|
29
|
-
* @property {number} [maxRetries=3] - Maximum number of retry attempts for failed operations
|
|
30
|
-
* @property {number} [retryDelay=1000] - Delay in milliseconds between retry attempts
|
|
31
|
-
* @property {boolean} [useUpsert=true] - Whether to use UPSERT (INSERT ... ON CONFLICT) for updates
|
|
32
|
-
* @property {string} [conflictColumn='id'] - Column name to use for conflict resolution in UPSERT operations
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* // Basic configuration with table mapping
|
|
36
|
-
* {
|
|
37
|
-
* database: 'analytics_db',
|
|
38
|
-
* resourceArn: 'arn:aws:rds:us-east-1:123456789012:cluster:my-aurora-cluster',
|
|
39
|
-
* secretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:db-credentials',
|
|
40
|
-
* region: 'us-east-1',
|
|
41
|
-
* tableMapping: {
|
|
42
|
-
* 'users': 'public.users',
|
|
43
|
-
* 'orders': 'analytics.orders',
|
|
44
|
-
* 'products': 'inventory.products'
|
|
45
|
-
* },
|
|
46
|
-
* logOperations: true,
|
|
47
|
-
* useUpsert: true,
|
|
48
|
-
* conflictColumn: 'id'
|
|
49
|
-
* }
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* // Minimal configuration using default settings
|
|
53
|
-
* {
|
|
54
|
-
* database: 'my_database',
|
|
55
|
-
* resourceArn: 'arn:aws:rds:us-east-1:123456789012:cluster:my-cluster',
|
|
56
|
-
* secretArn: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:db-secret'
|
|
57
|
-
* }
|
|
12
|
+
* Configuration:
|
|
13
|
+
* @param {string} connectionString - PostgreSQL connection string (required)
|
|
14
|
+
* @param {string} host - Database host (alternative to connectionString)
|
|
15
|
+
* @param {number} port - Database port (default: 5432)
|
|
16
|
+
* @param {string} database - Database name
|
|
17
|
+
* @param {string} user - Database user
|
|
18
|
+
* @param {string} password - Database password
|
|
19
|
+
* @param {Object} ssl - SSL configuration (optional)
|
|
20
|
+
* @param {string} logTable - Table name for operation logging (optional)
|
|
58
21
|
*
|
|
59
|
-
* @notes
|
|
60
|
-
* - Requires AWS credentials with RDS Data Service permissions
|
|
61
|
-
* - Database tables must exist before replicator starts
|
|
62
|
-
* - For UPSERT operations, the conflict column must have a unique constraint
|
|
63
|
-
* - All data is automatically converted to JSON format for storage
|
|
64
|
-
* - Timestamps are stored as ISO strings in the database
|
|
65
|
-
* - Failed operations are retried with exponential backoff
|
|
66
|
-
* - Operations are executed within database transactions for consistency
|
|
67
|
-
*/
|
|
68
|
-
import BaseReplicator from './base-replicator.class.js';
|
|
69
|
-
import tryFn from "../../concerns/try-fn.js";
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* PostgreSQL Replicator
|
|
73
|
-
*
|
|
74
|
-
* Replicates data to PostgreSQL tables, supporting per-resource table mapping and action filtering.
|
|
75
|
-
*
|
|
76
|
-
* ⚠️ REQUIRED DEPENDENCY: You must install the PostgreSQL client library to use this replicator:
|
|
77
|
-
*
|
|
78
|
-
* ```bash
|
|
79
|
-
* npm install pg
|
|
80
|
-
* # or
|
|
81
|
-
* yarn add pg
|
|
82
|
-
* # or
|
|
83
|
-
* pnpm add pg
|
|
84
|
-
* ```
|
|
85
|
-
*
|
|
86
|
-
* @config {Object} config - Configuration object for the replicator
|
|
87
|
-
* @config {string} [config.connectionString] - PostgreSQL connection string (alternative to individual params)
|
|
88
|
-
* @config {string} [config.host] - Database host (required if not using connectionString)
|
|
89
|
-
* @config {number} [config.port=5432] - Database port
|
|
90
|
-
* @config {string} [config.database] - Database name (required if not using connectionString)
|
|
91
|
-
* @config {string} [config.user] - Database user (required if not using connectionString)
|
|
92
|
-
* @config {string} [config.password] - Database password (required if not using connectionString)
|
|
93
|
-
* @config {Object} [config.ssl] - SSL configuration
|
|
94
|
-
* @config {string} [config.logTable] - Table name for operation logging. If omitted, no logging is performed.
|
|
95
|
-
* @config {Object} resources - Resource configuration mapping
|
|
96
|
-
* @config {Object|string} resources[resourceName] - Resource configuration
|
|
97
|
-
* @config {string} resources[resourceName].table - Table name for this resource
|
|
98
|
-
* @config {Array} resources[resourceName].actions - Array of actions to replicate (insert, update, delete)
|
|
99
|
-
* @config {string} resources[resourceName] - Short form: just the table name (equivalent to { actions: ['insert'], table: tableName })
|
|
100
|
-
*
|
|
101
22
|
* @example
|
|
102
23
|
* new PostgresReplicator({
|
|
103
24
|
* connectionString: 'postgresql://user:password@localhost:5432/analytics',
|
|
104
|
-
*
|
|
105
|
-
* logTable: 's3db_replicator_log'
|
|
25
|
+
* logTable: 'replication_log'
|
|
106
26
|
* }, {
|
|
107
|
-
* users: [
|
|
108
|
-
*
|
|
109
|
-
* ],
|
|
110
|
-
* orders: [
|
|
111
|
-
* { actions: ['insert'], table: 'orders_table' },
|
|
112
|
-
* { actions: ['insert'], table: 'orders_analytics' }, // Also replicate to analytics table
|
|
113
|
-
* ],
|
|
114
|
-
* products: 'products_table' // Short form: equivalent to { actions: ['insert'], table: 'products_table' }
|
|
27
|
+
* users: [{ actions: ['insert', 'update'], table: 'users_table' }],
|
|
28
|
+
* orders: 'orders_table'
|
|
115
29
|
* })
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* - The target tables must exist and have columns matching the resource attributes (id is required as primary key)
|
|
119
|
-
* - The log table must have columns: resource_name, operation, record_id, data, timestamp, source
|
|
120
|
-
* - Uses pg (node-postgres) library
|
|
121
|
-
* - Supports UPSERT operations with ON CONFLICT handling
|
|
30
|
+
*
|
|
31
|
+
* See PLUGINS.md for comprehensive configuration documentation.
|
|
122
32
|
*/
|
|
123
33
|
class PostgresReplicator extends BaseReplicator {
|
|
124
34
|
constructor(config = {}, resources = {}) {
|
|
@@ -1,59 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* This replicator supports highly flexible resource mapping and transformer configuration. You can specify the resources to replicate using any of the following syntaxes:
|
|
5
|
-
*
|
|
6
|
-
* 1. Array of resource names (replicate resource to itself):
|
|
7
|
-
* resources: ['users']
|
|
8
|
-
* // Replicates 'users' to 'users' in the destination
|
|
9
|
-
*
|
|
10
|
-
* 2. Map: source resource → destination resource name:
|
|
11
|
-
* resources: { users: 'people' }
|
|
12
|
-
* // Replicates 'users' to 'people' in the destination
|
|
13
|
-
*
|
|
14
|
-
* 3. Map: source resource → array of destination resource names and/or transformers:
|
|
15
|
-
* resources: { users: ['people', (el) => ({ ...el, fullName: el.name })] }
|
|
16
|
-
* // Replicates 'users' to 'people' and also applies the transformer
|
|
17
|
-
*
|
|
18
|
-
* 4. Map: source resource → object with resource and transformer:
|
|
19
|
-
* resources: { users: { resource: 'people', transformer: (el) => ({ ...el, fullName: el.name }) } }
|
|
20
|
-
* // Replicates 'users' to 'people' with a custom transformer
|
|
21
|
-
*
|
|
22
|
-
* 5. Map: source resource → array of objects with resource and transformer (multi-destination):
|
|
23
|
-
* resources: { users: [ { resource: 'people', transformer: (el) => ({ ...el, fullName: el.name }) } ] }
|
|
24
|
-
* // Replicates 'users' to multiple destinations, each with its own transformer
|
|
25
|
-
*
|
|
26
|
-
* 6. Map: source resource → function (rare, but supported):
|
|
27
|
-
* resources: { users: (el) => ... }
|
|
28
|
-
* // Replicates 'users' to 'users' with a custom transformer
|
|
29
|
-
*
|
|
30
|
-
* All forms can be mixed and matched for different resources. The transformer is always available (default: identity function).
|
|
31
|
-
*
|
|
32
|
-
* Example:
|
|
33
|
-
* resources: {
|
|
34
|
-
* users: [
|
|
35
|
-
* 'people',
|
|
36
|
-
* { resource: 'people', transformer: (el) => ({ ...el, fullName: el.name }) },
|
|
37
|
-
* (el) => ({ ...el, fullName: el.name })
|
|
38
|
-
* ],
|
|
39
|
-
* orders: 'orders_copy',
|
|
40
|
-
* products: { resource: 'products_copy' }
|
|
41
|
-
* }
|
|
42
|
-
*
|
|
43
|
-
* The replicator always uses the provided client as the destination.
|
|
44
|
-
*
|
|
45
|
-
* See tests/examples for all supported syntaxes.
|
|
46
|
-
*/
|
|
1
|
+
import tryFn from "#src/concerns/try-fn.js";
|
|
2
|
+
import { S3db } from '#src/database.class.js';
|
|
47
3
|
import BaseReplicator from './base-replicator.class.js';
|
|
48
|
-
import { S3db } from '../../database.class.js';
|
|
49
|
-
import tryFn from "../../concerns/try-fn.js";
|
|
50
4
|
|
|
51
5
|
function normalizeResourceName(name) {
|
|
52
6
|
return typeof name === 'string' ? name.trim().toLowerCase() : name;
|
|
53
7
|
}
|
|
54
8
|
|
|
55
9
|
/**
|
|
56
|
-
* S3DB Replicator -
|
|
10
|
+
* S3DB Replicator - Replicate data to another S3DB instance
|
|
11
|
+
*
|
|
12
|
+
* Configuration:
|
|
13
|
+
* @param {string} connectionString - S3DB connection string for destination database (required)
|
|
14
|
+
* @param {Object} client - Pre-configured S3DB client instance (alternative to connectionString)
|
|
15
|
+
* @param {Object} resources - Resource mapping configuration
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* new S3dbReplicator({
|
|
19
|
+
* connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
20
|
+
* }, {
|
|
21
|
+
* users: 'backup_users',
|
|
22
|
+
* orders: {
|
|
23
|
+
* resource: 'order_backup',
|
|
24
|
+
* transformer: (data) => ({ ...data, backup_timestamp: new Date().toISOString() })
|
|
25
|
+
* }
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* See PLUGINS.md for comprehensive configuration documentation.
|
|
57
29
|
*/
|
|
58
30
|
class S3dbReplicator extends BaseReplicator {
|
|
59
31
|
constructor(config = {}, resources = [], client = null) {
|