s3db.js 11.3.2 → 12.0.0
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/README.md +102 -8
- package/dist/s3db.cjs.js +36664 -15480
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +57 -0
- package/dist/s3db.es.js +36661 -15531
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +27 -6
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +41 -46
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +39 -19
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +539 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +350 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +14 -10
- package/src/s3db.d.ts +57 -0
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- package/src/partition-drivers/sync-partition-driver.js +0 -38
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
import tryFn from "#src/concerns/try-fn.js";
|
|
2
|
-
|
|
2
|
+
import requirePluginDependency from "#src/plugins/concerns/plugin-dependencies.js";
|
|
3
3
|
import BaseReplicator from './base-replicator.class.js';
|
|
4
|
+
import {
|
|
5
|
+
generateBigQuerySchema,
|
|
6
|
+
getBigQueryTableSchema,
|
|
7
|
+
generateBigQuerySchemaUpdate
|
|
8
|
+
} from './schema-sync.helper.js';
|
|
4
9
|
|
|
5
10
|
/**
|
|
6
11
|
* BigQuery Replicator - Replicate data to Google BigQuery tables
|
|
7
|
-
*
|
|
12
|
+
*
|
|
8
13
|
* ⚠️ REQUIRED DEPENDENCY: You must install the Google Cloud BigQuery SDK:
|
|
9
14
|
* ```bash
|
|
10
15
|
* pnpm add @google-cloud/bigquery
|
|
11
16
|
* ```
|
|
12
|
-
*
|
|
17
|
+
*
|
|
13
18
|
* Configuration:
|
|
14
19
|
* @param {string} projectId - Google Cloud project ID (required)
|
|
15
|
-
* @param {string} datasetId - BigQuery dataset ID (required)
|
|
20
|
+
* @param {string} datasetId - BigQuery dataset ID (required)
|
|
16
21
|
* @param {Object} credentials - Service account credentials object (optional)
|
|
17
22
|
* @param {string} location - BigQuery dataset location/region (default: 'US')
|
|
18
23
|
* @param {string} logTable - Table name for operation logging (optional)
|
|
19
|
-
*
|
|
24
|
+
* @param {Object} schemaSync - Schema synchronization configuration
|
|
25
|
+
* @param {boolean} schemaSync.enabled - Enable automatic schema management (default: false)
|
|
26
|
+
* @param {string} schemaSync.strategy - Sync strategy: 'alter' | 'drop-create' | 'validate-only' (default: 'alter')
|
|
27
|
+
* @param {string} schemaSync.onMismatch - Action on schema mismatch: 'error' | 'warn' | 'ignore' (default: 'error')
|
|
28
|
+
* @param {boolean} schemaSync.autoCreateTable - Auto-create table if not exists (default: true)
|
|
29
|
+
* @param {boolean} schemaSync.autoCreateColumns - Auto-add missing columns (default: true, only with strategy: 'alter')
|
|
30
|
+
*
|
|
20
31
|
* @example
|
|
21
32
|
* new BigqueryReplicator({
|
|
22
33
|
* projectId: 'my-gcp-project',
|
|
23
34
|
* datasetId: 'analytics',
|
|
24
|
-
* credentials: JSON.parse(Buffer.from(GOOGLE_CREDENTIALS, 'base64').toString())
|
|
35
|
+
* credentials: JSON.parse(Buffer.from(GOOGLE_CREDENTIALS, 'base64').toString()),
|
|
36
|
+
* schemaSync: {
|
|
37
|
+
* enabled: true,
|
|
38
|
+
* strategy: 'alter',
|
|
39
|
+
* onMismatch: 'error'
|
|
40
|
+
* }
|
|
25
41
|
* }, {
|
|
26
42
|
* users: {
|
|
27
43
|
* table: 'users_table',
|
|
@@ -29,7 +45,7 @@ import BaseReplicator from './base-replicator.class.js';
|
|
|
29
45
|
* },
|
|
30
46
|
* orders: 'orders_table'
|
|
31
47
|
* })
|
|
32
|
-
*
|
|
48
|
+
*
|
|
33
49
|
* See PLUGINS.md for comprehensive configuration documentation.
|
|
34
50
|
*/
|
|
35
51
|
class BigqueryReplicator extends BaseReplicator {
|
|
@@ -42,6 +58,15 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
42
58
|
this.location = config.location || 'US';
|
|
43
59
|
this.logTable = config.logTable;
|
|
44
60
|
|
|
61
|
+
// Schema sync configuration
|
|
62
|
+
this.schemaSync = {
|
|
63
|
+
enabled: config.schemaSync?.enabled || false,
|
|
64
|
+
strategy: config.schemaSync?.strategy || 'alter',
|
|
65
|
+
onMismatch: config.schemaSync?.onMismatch || 'error',
|
|
66
|
+
autoCreateTable: config.schemaSync?.autoCreateTable !== false,
|
|
67
|
+
autoCreateColumns: config.schemaSync?.autoCreateColumns !== false
|
|
68
|
+
};
|
|
69
|
+
|
|
45
70
|
// Parse resources configuration
|
|
46
71
|
this.resources = this.parseResourcesConfig(resources);
|
|
47
72
|
}
|
|
@@ -113,6 +138,10 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
113
138
|
|
|
114
139
|
async initialize(database) {
|
|
115
140
|
await super.initialize(database);
|
|
141
|
+
|
|
142
|
+
// Validate plugin dependencies are installed
|
|
143
|
+
await requirePluginDependency('bigquery-replicator');
|
|
144
|
+
|
|
116
145
|
const [ok, err, sdk] = await tryFn(() => import('@google-cloud/bigquery'));
|
|
117
146
|
if (!ok) {
|
|
118
147
|
if (this.config.verbose) {
|
|
@@ -127,6 +156,12 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
127
156
|
credentials: this.credentials,
|
|
128
157
|
location: this.location
|
|
129
158
|
});
|
|
159
|
+
|
|
160
|
+
// Sync schemas if enabled
|
|
161
|
+
if (this.schemaSync.enabled) {
|
|
162
|
+
await this.syncSchemas(database);
|
|
163
|
+
}
|
|
164
|
+
|
|
130
165
|
this.emit('initialized', {
|
|
131
166
|
replicator: this.name,
|
|
132
167
|
projectId: this.projectId,
|
|
@@ -135,6 +170,142 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
135
170
|
});
|
|
136
171
|
}
|
|
137
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Sync table schemas based on S3DB resource definitions
|
|
175
|
+
*/
|
|
176
|
+
async syncSchemas(database) {
|
|
177
|
+
for (const [resourceName, tableConfigs] of Object.entries(this.resources)) {
|
|
178
|
+
const [okRes, errRes, resource] = await tryFn(async () => {
|
|
179
|
+
return await database.getResource(resourceName);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (!okRes) {
|
|
183
|
+
if (this.config.verbose) {
|
|
184
|
+
console.warn(`[BigQueryReplicator] Could not get resource ${resourceName} for schema sync: ${errRes.message}`);
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
190
|
+
|
|
191
|
+
for (const tableConfig of tableConfigs) {
|
|
192
|
+
const tableName = tableConfig.table;
|
|
193
|
+
|
|
194
|
+
const [okSync, errSync] = await tryFn(async () => {
|
|
195
|
+
await this.syncTableSchema(tableName, attributes);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!okSync) {
|
|
199
|
+
const message = `Schema sync failed for table ${tableName}: ${errSync.message}`;
|
|
200
|
+
|
|
201
|
+
if (this.schemaSync.onMismatch === 'error') {
|
|
202
|
+
throw new Error(message);
|
|
203
|
+
} else if (this.schemaSync.onMismatch === 'warn') {
|
|
204
|
+
console.warn(`[BigQueryReplicator] ${message}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
this.emit('schema_sync_completed', {
|
|
211
|
+
replicator: this.name,
|
|
212
|
+
resources: Object.keys(this.resources)
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Sync a single table schema in BigQuery
|
|
218
|
+
*/
|
|
219
|
+
async syncTableSchema(tableName, attributes) {
|
|
220
|
+
const dataset = this.bigqueryClient.dataset(this.datasetId);
|
|
221
|
+
const table = dataset.table(tableName);
|
|
222
|
+
|
|
223
|
+
// Check if table exists
|
|
224
|
+
const [exists] = await table.exists();
|
|
225
|
+
|
|
226
|
+
if (!exists) {
|
|
227
|
+
if (!this.schemaSync.autoCreateTable) {
|
|
228
|
+
throw new Error(`Table ${tableName} does not exist and autoCreateTable is disabled`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (this.schemaSync.strategy === 'validate-only') {
|
|
232
|
+
throw new Error(`Table ${tableName} does not exist (validate-only mode)`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Create table with schema
|
|
236
|
+
const schema = generateBigQuerySchema(attributes);
|
|
237
|
+
|
|
238
|
+
if (this.config.verbose) {
|
|
239
|
+
console.log(`[BigQueryReplicator] Creating table ${tableName} with schema:`, schema);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
await dataset.createTable(tableName, { schema });
|
|
243
|
+
|
|
244
|
+
this.emit('table_created', {
|
|
245
|
+
replicator: this.name,
|
|
246
|
+
tableName,
|
|
247
|
+
attributes: Object.keys(attributes)
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Table exists - check for schema changes
|
|
254
|
+
if (this.schemaSync.strategy === 'drop-create') {
|
|
255
|
+
if (this.config.verbose) {
|
|
256
|
+
console.warn(`[BigQueryReplicator] Dropping and recreating table ${tableName}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
await table.delete();
|
|
260
|
+
const schema = generateBigQuerySchema(attributes);
|
|
261
|
+
await dataset.createTable(tableName, { schema });
|
|
262
|
+
|
|
263
|
+
this.emit('table_recreated', {
|
|
264
|
+
replicator: this.name,
|
|
265
|
+
tableName,
|
|
266
|
+
attributes: Object.keys(attributes)
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (this.schemaSync.strategy === 'alter' && this.schemaSync.autoCreateColumns) {
|
|
273
|
+
const existingSchema = await getBigQueryTableSchema(this.bigqueryClient, this.datasetId, tableName);
|
|
274
|
+
const newFields = generateBigQuerySchemaUpdate(attributes, existingSchema);
|
|
275
|
+
|
|
276
|
+
if (newFields.length > 0) {
|
|
277
|
+
if (this.config.verbose) {
|
|
278
|
+
console.log(`[BigQueryReplicator] Adding ${newFields.length} field(s) to table ${tableName}:`, newFields);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Get current schema
|
|
282
|
+
const [metadata] = await table.getMetadata();
|
|
283
|
+
const currentSchema = metadata.schema.fields;
|
|
284
|
+
|
|
285
|
+
// Add new fields to existing schema
|
|
286
|
+
const updatedSchema = [...currentSchema, ...newFields];
|
|
287
|
+
|
|
288
|
+
// Update table schema
|
|
289
|
+
await table.setMetadata({ schema: updatedSchema });
|
|
290
|
+
|
|
291
|
+
this.emit('table_altered', {
|
|
292
|
+
replicator: this.name,
|
|
293
|
+
tableName,
|
|
294
|
+
addedColumns: newFields.length
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (this.schemaSync.strategy === 'validate-only') {
|
|
300
|
+
const existingSchema = await getBigQueryTableSchema(this.bigqueryClient, this.datasetId, tableName);
|
|
301
|
+
const newFields = generateBigQuerySchemaUpdate(attributes, existingSchema);
|
|
302
|
+
|
|
303
|
+
if (newFields.length > 0) {
|
|
304
|
+
throw new Error(`Table ${tableName} schema mismatch. Missing columns: ${newFields.length}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
138
309
|
shouldReplicateResource(resourceName) {
|
|
139
310
|
return this.resources.hasOwnProperty(resourceName);
|
|
140
311
|
}
|
|
@@ -434,7 +605,8 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
434
605
|
projectId: this.projectId,
|
|
435
606
|
datasetId: this.datasetId,
|
|
436
607
|
resources: this.resources,
|
|
437
|
-
logTable: this.logTable
|
|
608
|
+
logTable: this.logTable,
|
|
609
|
+
schemaSync: this.schemaSync
|
|
438
610
|
};
|
|
439
611
|
}
|
|
440
612
|
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import tryFn from "#src/concerns/try-fn.js";
|
|
2
|
+
import requirePluginDependency from "#src/plugins/concerns/plugin-dependencies.js";
|
|
3
|
+
import BaseReplicator from './base-replicator.class.js';
|
|
4
|
+
import { ReplicationError } from '../replicator.errors.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DynamoDB Replicator - Replicate data to AWS DynamoDB tables
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ REQUIRED DEPENDENCY: You must install the AWS SDK:
|
|
10
|
+
* ```bash
|
|
11
|
+
* pnpm add @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Configuration:
|
|
15
|
+
* @param {string} region - AWS region (required)
|
|
16
|
+
* @param {string} accessKeyId - AWS access key (optional, uses AWS SDK default chain)
|
|
17
|
+
* @param {string} secretAccessKey - AWS secret key (optional)
|
|
18
|
+
* @param {string} endpoint - Custom endpoint for DynamoDB Local (optional)
|
|
19
|
+
* @param {Object} credentials - AWS credentials object (optional)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* new DynamoDBReplicator({
|
|
23
|
+
* region: 'us-east-1',
|
|
24
|
+
* accessKeyId: 'YOUR_ACCESS_KEY',
|
|
25
|
+
* secretAccessKey: 'YOUR_SECRET_KEY'
|
|
26
|
+
* }, {
|
|
27
|
+
* users: [{ actions: ['insert', 'update', 'delete'], table: 'UsersTable' }],
|
|
28
|
+
* orders: 'OrdersTable'
|
|
29
|
+
* })
|
|
30
|
+
*
|
|
31
|
+
* // DynamoDB Local example
|
|
32
|
+
* new DynamoDBReplicator({
|
|
33
|
+
* region: 'us-east-1',
|
|
34
|
+
* endpoint: 'http://localhost:8000'
|
|
35
|
+
* }, {
|
|
36
|
+
* users: 'Users'
|
|
37
|
+
* })
|
|
38
|
+
*
|
|
39
|
+
* See PLUGINS.md for comprehensive configuration documentation.
|
|
40
|
+
*/
|
|
41
|
+
class DynamoDBReplicator extends BaseReplicator {
|
|
42
|
+
constructor(config = {}, resources = {}) {
|
|
43
|
+
super(config);
|
|
44
|
+
this.region = config.region || 'us-east-1';
|
|
45
|
+
this.accessKeyId = config.accessKeyId;
|
|
46
|
+
this.secretAccessKey = config.secretAccessKey;
|
|
47
|
+
this.endpoint = config.endpoint;
|
|
48
|
+
this.credentials = config.credentials;
|
|
49
|
+
this.client = null;
|
|
50
|
+
this.docClient = null;
|
|
51
|
+
|
|
52
|
+
// Parse resources configuration
|
|
53
|
+
this.resources = this.parseResourcesConfig(resources);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
parseResourcesConfig(resources) {
|
|
57
|
+
const parsed = {};
|
|
58
|
+
|
|
59
|
+
for (const [resourceName, config] of Object.entries(resources)) {
|
|
60
|
+
if (typeof config === 'string') {
|
|
61
|
+
// Short form: just table name
|
|
62
|
+
parsed[resourceName] = [{
|
|
63
|
+
table: config,
|
|
64
|
+
actions: ['insert'],
|
|
65
|
+
primaryKey: 'id' // Default primary key
|
|
66
|
+
}];
|
|
67
|
+
} else if (Array.isArray(config)) {
|
|
68
|
+
// Array form: multiple table mappings
|
|
69
|
+
parsed[resourceName] = config.map(item => {
|
|
70
|
+
if (typeof item === 'string') {
|
|
71
|
+
return { table: item, actions: ['insert'], primaryKey: 'id' };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
table: item.table,
|
|
75
|
+
actions: item.actions || ['insert'],
|
|
76
|
+
primaryKey: item.primaryKey || 'id',
|
|
77
|
+
sortKey: item.sortKey // Optional sort key
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
} else if (typeof config === 'object') {
|
|
81
|
+
// Single object form
|
|
82
|
+
parsed[resourceName] = [{
|
|
83
|
+
table: config.table,
|
|
84
|
+
actions: config.actions || ['insert'],
|
|
85
|
+
primaryKey: config.primaryKey || 'id',
|
|
86
|
+
sortKey: config.sortKey
|
|
87
|
+
}];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return parsed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
validateConfig() {
|
|
95
|
+
const errors = [];
|
|
96
|
+
// Region defaults to us-east-1, so it's always set
|
|
97
|
+
// Only validate if explicitly set to empty string
|
|
98
|
+
if (this.region === '') {
|
|
99
|
+
errors.push('AWS region is required');
|
|
100
|
+
}
|
|
101
|
+
if (Object.keys(this.resources).length === 0) {
|
|
102
|
+
errors.push('At least one resource must be configured');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate resource configurations
|
|
106
|
+
for (const [resourceName, tables] of Object.entries(this.resources)) {
|
|
107
|
+
for (const tableConfig of tables) {
|
|
108
|
+
if (!tableConfig.table) {
|
|
109
|
+
errors.push(`Table name is required for resource '${resourceName}'`);
|
|
110
|
+
}
|
|
111
|
+
if (!Array.isArray(tableConfig.actions) || tableConfig.actions.length === 0) {
|
|
112
|
+
errors.push(`Actions array is required for resource '${resourceName}'`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { isValid: errors.length === 0, errors };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async initialize(database) {
|
|
121
|
+
await super.initialize(database);
|
|
122
|
+
|
|
123
|
+
// Load AWS SDK dependencies
|
|
124
|
+
const { DynamoDBClient } = requirePluginDependency('@aws-sdk/client-dynamodb', 'DynamoDBReplicator');
|
|
125
|
+
const { DynamoDBDocumentClient, PutCommand, UpdateCommand, DeleteCommand } = requirePluginDependency('@aws-sdk/lib-dynamodb', 'DynamoDBReplicator');
|
|
126
|
+
|
|
127
|
+
// Store command constructors for later use
|
|
128
|
+
this.PutCommand = PutCommand;
|
|
129
|
+
this.UpdateCommand = UpdateCommand;
|
|
130
|
+
this.DeleteCommand = DeleteCommand;
|
|
131
|
+
|
|
132
|
+
const [ok, err] = await tryFn(async () => {
|
|
133
|
+
const clientConfig = {
|
|
134
|
+
region: this.region
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (this.endpoint) {
|
|
138
|
+
clientConfig.endpoint = this.endpoint;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (this.credentials) {
|
|
142
|
+
clientConfig.credentials = this.credentials;
|
|
143
|
+
} else if (this.accessKeyId && this.secretAccessKey) {
|
|
144
|
+
clientConfig.credentials = {
|
|
145
|
+
accessKeyId: this.accessKeyId,
|
|
146
|
+
secretAccessKey: this.secretAccessKey
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.client = new DynamoDBClient(clientConfig);
|
|
151
|
+
this.docClient = DynamoDBDocumentClient.from(this.client);
|
|
152
|
+
|
|
153
|
+
// Test connection by listing tables
|
|
154
|
+
const { ListTablesCommand } = requirePluginDependency('@aws-sdk/client-dynamodb', 'DynamoDBReplicator');
|
|
155
|
+
await this.client.send(new ListTablesCommand({ Limit: 1 }));
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!ok) {
|
|
159
|
+
throw new ReplicationError('Failed to connect to DynamoDB', {
|
|
160
|
+
operation: 'initialize',
|
|
161
|
+
replicatorClass: 'DynamoDBReplicator',
|
|
162
|
+
region: this.region,
|
|
163
|
+
endpoint: this.endpoint,
|
|
164
|
+
original: err,
|
|
165
|
+
suggestion: 'Check AWS credentials and ensure DynamoDB is accessible'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.emit('connected', {
|
|
170
|
+
replicator: 'DynamoDBReplicator',
|
|
171
|
+
region: this.region,
|
|
172
|
+
endpoint: this.endpoint || 'default'
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
shouldReplicateResource(resourceName) {
|
|
177
|
+
return this.resources.hasOwnProperty(resourceName);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async replicate(resourceName, operation, data, id) {
|
|
181
|
+
if (!this.resources[resourceName]) {
|
|
182
|
+
throw new ReplicationError('Resource not configured for replication', {
|
|
183
|
+
operation: 'replicate',
|
|
184
|
+
replicatorClass: 'DynamoDBReplicator',
|
|
185
|
+
resourceName,
|
|
186
|
+
configuredResources: Object.keys(this.resources),
|
|
187
|
+
suggestion: 'Add resource to replicator resources configuration'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const results = [];
|
|
192
|
+
|
|
193
|
+
for (const tableConfig of this.resources[resourceName]) {
|
|
194
|
+
if (!tableConfig.actions.includes(operation)) {
|
|
195
|
+
continue; // Skip if operation not allowed for this table
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const [ok, error, result] = await tryFn(async () => {
|
|
199
|
+
switch (operation) {
|
|
200
|
+
case 'insert':
|
|
201
|
+
return await this._putItem(tableConfig.table, data);
|
|
202
|
+
case 'update':
|
|
203
|
+
return await this._updateItem(tableConfig.table, id, data, tableConfig);
|
|
204
|
+
case 'delete':
|
|
205
|
+
return await this._deleteItem(tableConfig.table, id, tableConfig);
|
|
206
|
+
default:
|
|
207
|
+
throw new ReplicationError(`Unsupported operation: ${operation}`, {
|
|
208
|
+
operation: 'replicate',
|
|
209
|
+
replicatorClass: 'DynamoDBReplicator',
|
|
210
|
+
invalidOperation: operation,
|
|
211
|
+
supportedOperations: ['insert', 'update', 'delete']
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (ok) {
|
|
217
|
+
results.push(result);
|
|
218
|
+
} else {
|
|
219
|
+
this.emit('replication_error', {
|
|
220
|
+
resource: resourceName,
|
|
221
|
+
operation,
|
|
222
|
+
table: tableConfig.table,
|
|
223
|
+
error: error.message
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
if (this.config.verbose) {
|
|
227
|
+
console.error(`[DynamoDBReplicator] Failed to replicate ${operation} for ${resourceName}:`, error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return results.length > 0 ? results[0] : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async _putItem(table, data) {
|
|
236
|
+
const cleanData = this._cleanInternalFields(data);
|
|
237
|
+
|
|
238
|
+
const command = new this.PutCommand({
|
|
239
|
+
TableName: table,
|
|
240
|
+
Item: cleanData
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const result = await this.docClient.send(command);
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async _updateItem(table, id, data, tableConfig) {
|
|
248
|
+
const cleanData = this._cleanInternalFields(data);
|
|
249
|
+
|
|
250
|
+
// Build update expression
|
|
251
|
+
const updateExpressions = [];
|
|
252
|
+
const expressionAttributeNames = {};
|
|
253
|
+
const expressionAttributeValues = {};
|
|
254
|
+
|
|
255
|
+
let index = 0;
|
|
256
|
+
for (const [key, value] of Object.entries(cleanData)) {
|
|
257
|
+
// Skip primary key and sort key
|
|
258
|
+
if (key === tableConfig.primaryKey || key === tableConfig.sortKey) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const attrName = `#attr${index}`;
|
|
263
|
+
const attrValue = `:val${index}`;
|
|
264
|
+
|
|
265
|
+
expressionAttributeNames[attrName] = key;
|
|
266
|
+
expressionAttributeValues[attrValue] = value;
|
|
267
|
+
updateExpressions.push(`${attrName} = ${attrValue}`);
|
|
268
|
+
index++;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Build key
|
|
272
|
+
const key = { [tableConfig.primaryKey]: id };
|
|
273
|
+
if (tableConfig.sortKey && cleanData[tableConfig.sortKey]) {
|
|
274
|
+
key[tableConfig.sortKey] = cleanData[tableConfig.sortKey];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const command = new this.UpdateCommand({
|
|
278
|
+
TableName: table,
|
|
279
|
+
Key: key,
|
|
280
|
+
UpdateExpression: `SET ${updateExpressions.join(', ')}`,
|
|
281
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
282
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
|
283
|
+
ReturnValues: 'ALL_NEW'
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const result = await this.docClient.send(command);
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async _deleteItem(table, id, tableConfig) {
|
|
291
|
+
const key = { [tableConfig.primaryKey]: id };
|
|
292
|
+
|
|
293
|
+
const command = new this.DeleteCommand({
|
|
294
|
+
TableName: table,
|
|
295
|
+
Key: key
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const result = await this.docClient.send(command);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
_cleanInternalFields(data) {
|
|
303
|
+
if (!data || typeof data !== 'object') return data;
|
|
304
|
+
|
|
305
|
+
const cleanData = { ...data };
|
|
306
|
+
|
|
307
|
+
// Remove internal s3db fields
|
|
308
|
+
Object.keys(cleanData).forEach(key => {
|
|
309
|
+
if (key.startsWith('$') || key.startsWith('_')) {
|
|
310
|
+
delete cleanData[key];
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return cleanData;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async replicateBatch(resourceName, records) {
|
|
318
|
+
const results = [];
|
|
319
|
+
const errors = [];
|
|
320
|
+
|
|
321
|
+
// DynamoDB batch operations (up to 25 items)
|
|
322
|
+
// For now, process sequentially (can be optimized with BatchWriteItem)
|
|
323
|
+
for (const record of records) {
|
|
324
|
+
const [ok, err, result] = await tryFn(() =>
|
|
325
|
+
this.replicate(resourceName, record.operation, record.data, record.id)
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
if (ok) {
|
|
329
|
+
results.push(result);
|
|
330
|
+
} else {
|
|
331
|
+
errors.push({ id: record.id, error: err.message });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
success: errors.length === 0,
|
|
337
|
+
results,
|
|
338
|
+
errors,
|
|
339
|
+
total: records.length
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async testConnection() {
|
|
344
|
+
const [ok, err] = await tryFn(async () => {
|
|
345
|
+
if (!this.client) {
|
|
346
|
+
throw new Error('Client not initialized');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const { ListTablesCommand } = requirePluginDependency('@aws-sdk/client-dynamodb', 'DynamoDBReplicator');
|
|
350
|
+
await this.client.send(new ListTablesCommand({ Limit: 1 }));
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (!ok) {
|
|
355
|
+
this.emit('connection_error', { replicator: 'DynamoDBReplicator', error: err.message });
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async getStatus() {
|
|
363
|
+
const baseStatus = await super.getStatus();
|
|
364
|
+
return {
|
|
365
|
+
...baseStatus,
|
|
366
|
+
connected: !!this.client,
|
|
367
|
+
region: this.region,
|
|
368
|
+
endpoint: this.endpoint || 'default',
|
|
369
|
+
resources: Object.keys(this.resources)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async cleanup() {
|
|
374
|
+
if (this.client) {
|
|
375
|
+
this.client.destroy();
|
|
376
|
+
this.client = null;
|
|
377
|
+
this.docClient = null;
|
|
378
|
+
}
|
|
379
|
+
await super.cleanup();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export default DynamoDBReplicator;
|
|
@@ -1,11 +1,29 @@
|
|
|
1
1
|
import BaseReplicator from './base-replicator.class.js';
|
|
2
2
|
import BigqueryReplicator from './bigquery-replicator.class.js';
|
|
3
|
+
import DynamoDBReplicator from './dynamodb-replicator.class.js';
|
|
4
|
+
import MongoDBReplicator from './mongodb-replicator.class.js';
|
|
5
|
+
import MySQLReplicator from './mysql-replicator.class.js';
|
|
6
|
+
import PlanetScaleReplicator from './planetscale-replicator.class.js';
|
|
3
7
|
import PostgresReplicator from './postgres-replicator.class.js';
|
|
4
8
|
import S3dbReplicator from './s3db-replicator.class.js';
|
|
5
9
|
import SqsReplicator from './sqs-replicator.class.js';
|
|
10
|
+
import TursoReplicator from './turso-replicator.class.js';
|
|
11
|
+
import WebhookReplicator from './webhook-replicator.class.js';
|
|
6
12
|
import { ReplicationError } from '../replicator.errors.js';
|
|
7
13
|
|
|
8
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
BaseReplicator,
|
|
16
|
+
BigqueryReplicator,
|
|
17
|
+
DynamoDBReplicator,
|
|
18
|
+
MongoDBReplicator,
|
|
19
|
+
MySQLReplicator,
|
|
20
|
+
PlanetScaleReplicator,
|
|
21
|
+
PostgresReplicator,
|
|
22
|
+
S3dbReplicator,
|
|
23
|
+
SqsReplicator,
|
|
24
|
+
TursoReplicator,
|
|
25
|
+
WebhookReplicator
|
|
26
|
+
};
|
|
9
27
|
|
|
10
28
|
/**
|
|
11
29
|
* Available replicator drivers
|
|
@@ -14,12 +32,19 @@ export const REPLICATOR_DRIVERS = {
|
|
|
14
32
|
s3db: S3dbReplicator,
|
|
15
33
|
sqs: SqsReplicator,
|
|
16
34
|
bigquery: BigqueryReplicator,
|
|
17
|
-
postgres: PostgresReplicator
|
|
35
|
+
postgres: PostgresReplicator,
|
|
36
|
+
mysql: MySQLReplicator,
|
|
37
|
+
mariadb: MySQLReplicator, // MariaDB uses the same driver as MySQL
|
|
38
|
+
planetscale: PlanetScaleReplicator,
|
|
39
|
+
turso: TursoReplicator,
|
|
40
|
+
dynamodb: DynamoDBReplicator,
|
|
41
|
+
mongodb: MongoDBReplicator,
|
|
42
|
+
webhook: WebhookReplicator
|
|
18
43
|
};
|
|
19
44
|
|
|
20
45
|
/**
|
|
21
46
|
* Create a replicator instance based on driver type
|
|
22
|
-
* @param {string} driver - Driver type (s3db, sqs, bigquery, postgres)
|
|
47
|
+
* @param {string} driver - Driver type (s3db, sqs, bigquery, postgres, mysql, mariadb, planetscale, turso, dynamodb, mongodb, webhook)
|
|
23
48
|
* @param {Object} config - Replicator configuration
|
|
24
49
|
* @returns {BaseReplicator} Replicator instance
|
|
25
50
|
*/
|