s3db.js 11.3.2 → 12.0.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/README.md +102 -8
- package/dist/s3db.cjs.js +36945 -15510
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +66 -1
- package/dist/s3db.es.js +36914 -15534
- 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 +35 -15
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +79 -49
- 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 +97 -47
- 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 +544 -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 +354 -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/replicator.plugin.js +2 -1
- 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 +315 -36
- package/src/s3db.d.ts +66 -1
- 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
|
@@ -0,0 +1,558 @@
|
|
|
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
|
+
import {
|
|
6
|
+
generateMySQLCreateTable,
|
|
7
|
+
getMySQLTableSchema,
|
|
8
|
+
generateMySQLAlterTable
|
|
9
|
+
} from './schema-sync.helper.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* MySQL/MariaDB Replicator - Replicate data to MySQL or MariaDB tables
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ REQUIRED DEPENDENCY: You must install the MySQL client library:
|
|
15
|
+
* ```bash
|
|
16
|
+
* pnpm add mysql2
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Configuration:
|
|
20
|
+
* @param {string} connectionString - MySQL connection string (optional)
|
|
21
|
+
* @param {string} host - Database host (default: localhost)
|
|
22
|
+
* @param {number} port - Database port (default: 3306)
|
|
23
|
+
* @param {string} database - Database name (required)
|
|
24
|
+
* @param {string} user - Database user (required)
|
|
25
|
+
* @param {string} password - Database password (required)
|
|
26
|
+
* @param {Object} ssl - SSL configuration (optional)
|
|
27
|
+
* @param {number} connectionLimit - Max connections in pool (default: 10)
|
|
28
|
+
* @param {string} logTable - Table name for operation logging (optional)
|
|
29
|
+
* @param {Object} schemaSync - Schema synchronization configuration
|
|
30
|
+
* @param {boolean} schemaSync.enabled - Enable automatic schema management (default: false)
|
|
31
|
+
* @param {string} schemaSync.strategy - Sync strategy: 'alter' | 'drop-create' | 'validate-only' (default: 'alter')
|
|
32
|
+
* @param {string} schemaSync.onMismatch - Action on schema mismatch: 'error' | 'warn' | 'ignore' (default: 'error')
|
|
33
|
+
* @param {boolean} schemaSync.autoCreateTable - Auto-create table if not exists (default: true)
|
|
34
|
+
* @param {boolean} schemaSync.autoCreateColumns - Auto-add missing columns (default: true, only with strategy: 'alter')
|
|
35
|
+
* @param {boolean} schemaSync.dropMissingColumns - Remove extra columns (default: false, dangerous!)
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* new MySQLReplicator({
|
|
39
|
+
* host: 'localhost',
|
|
40
|
+
* port: 3306,
|
|
41
|
+
* database: 'analytics',
|
|
42
|
+
* user: 'replicator',
|
|
43
|
+
* password: 'secret',
|
|
44
|
+
* logTable: 'replication_log',
|
|
45
|
+
* schemaSync: {
|
|
46
|
+
* enabled: true,
|
|
47
|
+
* strategy: 'alter',
|
|
48
|
+
* onMismatch: 'error'
|
|
49
|
+
* }
|
|
50
|
+
* }, {
|
|
51
|
+
* users: [{ actions: ['insert', 'update'], table: 'users_table' }],
|
|
52
|
+
* orders: 'orders_table'
|
|
53
|
+
* })
|
|
54
|
+
*
|
|
55
|
+
* See PLUGINS.md for comprehensive configuration documentation.
|
|
56
|
+
*/
|
|
57
|
+
class MySQLReplicator extends BaseReplicator {
|
|
58
|
+
constructor(config = {}, resources = {}) {
|
|
59
|
+
super(config);
|
|
60
|
+
this.connectionString = config.connectionString;
|
|
61
|
+
this.host = config.host || 'localhost';
|
|
62
|
+
this.port = config.port || 3306;
|
|
63
|
+
this.database = config.database;
|
|
64
|
+
this.user = config.user;
|
|
65
|
+
this.password = config.password;
|
|
66
|
+
this.pool = null;
|
|
67
|
+
this.ssl = config.ssl;
|
|
68
|
+
this.connectionLimit = config.connectionLimit || 10;
|
|
69
|
+
this.logTable = config.logTable;
|
|
70
|
+
|
|
71
|
+
// Schema sync configuration
|
|
72
|
+
this.schemaSync = {
|
|
73
|
+
enabled: config.schemaSync?.enabled || false,
|
|
74
|
+
strategy: config.schemaSync?.strategy || 'alter',
|
|
75
|
+
onMismatch: config.schemaSync?.onMismatch || 'error',
|
|
76
|
+
autoCreateTable: config.schemaSync?.autoCreateTable !== false,
|
|
77
|
+
autoCreateColumns: config.schemaSync?.autoCreateColumns !== false,
|
|
78
|
+
dropMissingColumns: config.schemaSync?.dropMissingColumns || false
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Parse resources configuration
|
|
82
|
+
this.resources = this.parseResourcesConfig(resources);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
parseResourcesConfig(resources) {
|
|
86
|
+
const parsed = {};
|
|
87
|
+
|
|
88
|
+
for (const [resourceName, config] of Object.entries(resources)) {
|
|
89
|
+
if (typeof config === 'string') {
|
|
90
|
+
// Short form: just table name
|
|
91
|
+
parsed[resourceName] = [{
|
|
92
|
+
table: config,
|
|
93
|
+
actions: ['insert']
|
|
94
|
+
}];
|
|
95
|
+
} else if (Array.isArray(config)) {
|
|
96
|
+
// Array form: multiple table mappings
|
|
97
|
+
parsed[resourceName] = config.map(item => {
|
|
98
|
+
if (typeof item === 'string') {
|
|
99
|
+
return { table: item, actions: ['insert'] };
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
table: item.table,
|
|
103
|
+
actions: item.actions || ['insert']
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
} else if (typeof config === 'object') {
|
|
107
|
+
// Single object form
|
|
108
|
+
parsed[resourceName] = [{
|
|
109
|
+
table: config.table,
|
|
110
|
+
actions: config.actions || ['insert']
|
|
111
|
+
}];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
validateConfig() {
|
|
119
|
+
const errors = [];
|
|
120
|
+
if (!this.database) {
|
|
121
|
+
errors.push('Database name is required');
|
|
122
|
+
}
|
|
123
|
+
if (!this.user) {
|
|
124
|
+
errors.push('Database user is required');
|
|
125
|
+
}
|
|
126
|
+
if (!this.password) {
|
|
127
|
+
errors.push('Database password is required');
|
|
128
|
+
}
|
|
129
|
+
if (Object.keys(this.resources).length === 0) {
|
|
130
|
+
errors.push('At least one resource must be configured');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Validate resource configurations
|
|
134
|
+
for (const [resourceName, tables] of Object.entries(this.resources)) {
|
|
135
|
+
for (const tableConfig of tables) {
|
|
136
|
+
if (!tableConfig.table) {
|
|
137
|
+
errors.push(`Table name is required for resource '${resourceName}'`);
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(tableConfig.actions) || tableConfig.actions.length === 0) {
|
|
140
|
+
errors.push(`Actions array is required for resource '${resourceName}'`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { isValid: errors.length === 0, errors };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async initialize(database) {
|
|
149
|
+
await super.initialize(database);
|
|
150
|
+
|
|
151
|
+
// Load mysql2 dependency
|
|
152
|
+
const mysql = requirePluginDependency('mysql2', 'MySQLReplicator');
|
|
153
|
+
|
|
154
|
+
// Create connection pool
|
|
155
|
+
const [ok, err] = await tryFn(async () => {
|
|
156
|
+
const poolConfig = {
|
|
157
|
+
host: this.host,
|
|
158
|
+
port: this.port,
|
|
159
|
+
user: this.user,
|
|
160
|
+
password: this.password,
|
|
161
|
+
database: this.database,
|
|
162
|
+
connectionLimit: this.connectionLimit,
|
|
163
|
+
waitForConnections: true,
|
|
164
|
+
queueLimit: 0
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
if (this.ssl) {
|
|
168
|
+
poolConfig.ssl = this.ssl;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.pool = mysql.createPool(poolConfig);
|
|
172
|
+
|
|
173
|
+
// Test connection
|
|
174
|
+
const connection = await this.pool.promise().getConnection();
|
|
175
|
+
await connection.ping();
|
|
176
|
+
connection.release();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!ok) {
|
|
180
|
+
throw new ReplicationError('Failed to connect to MySQL database', {
|
|
181
|
+
operation: 'initialize',
|
|
182
|
+
replicatorClass: 'MySQLReplicator',
|
|
183
|
+
host: this.host,
|
|
184
|
+
port: this.port,
|
|
185
|
+
database: this.database,
|
|
186
|
+
original: err,
|
|
187
|
+
suggestion: 'Check MySQL connection credentials and ensure database is accessible'
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create log table if configured
|
|
192
|
+
if (this.logTable) {
|
|
193
|
+
await this._createLogTable();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Sync schemas if enabled
|
|
197
|
+
if (this.schemaSync.enabled) {
|
|
198
|
+
await this.syncSchemas(database);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.emit('connected', {
|
|
202
|
+
replicator: 'MySQLReplicator',
|
|
203
|
+
host: this.host,
|
|
204
|
+
database: this.database
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Sync table schemas based on S3DB resource definitions
|
|
210
|
+
*/
|
|
211
|
+
async syncSchemas(database) {
|
|
212
|
+
for (const [resourceName, tableConfigs] of Object.entries(this.resources)) {
|
|
213
|
+
const [okRes, errRes, resource] = await tryFn(async () => {
|
|
214
|
+
return await database.getResource(resourceName);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!okRes) {
|
|
218
|
+
if (this.config.verbose) {
|
|
219
|
+
console.warn(`[MySQLReplicator] Could not get resource ${resourceName} for schema sync: ${errRes.message}`);
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const attributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
225
|
+
|
|
226
|
+
for (const tableConfig of tableConfigs) {
|
|
227
|
+
const tableName = tableConfig.table;
|
|
228
|
+
|
|
229
|
+
const [okSync, errSync] = await tryFn(async () => {
|
|
230
|
+
await this.syncTableSchema(tableName, attributes);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (!okSync) {
|
|
234
|
+
const message = `Schema sync failed for table ${tableName}: ${errSync.message}`;
|
|
235
|
+
|
|
236
|
+
if (this.schemaSync.onMismatch === 'error') {
|
|
237
|
+
throw new Error(message);
|
|
238
|
+
} else if (this.schemaSync.onMismatch === 'warn') {
|
|
239
|
+
console.warn(`[MySQLReplicator] ${message}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.emit('schema_sync_completed', {
|
|
246
|
+
replicator: this.name,
|
|
247
|
+
resources: Object.keys(this.resources)
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Sync a single table schema
|
|
253
|
+
*/
|
|
254
|
+
async syncTableSchema(tableName, attributes) {
|
|
255
|
+
const connection = await this.pool.promise().getConnection();
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
// Check if table exists
|
|
259
|
+
const existingSchema = await getMySQLTableSchema(connection, tableName);
|
|
260
|
+
|
|
261
|
+
if (!existingSchema) {
|
|
262
|
+
if (!this.schemaSync.autoCreateTable) {
|
|
263
|
+
throw new Error(`Table ${tableName} does not exist and autoCreateTable is disabled`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (this.schemaSync.strategy === 'validate-only') {
|
|
267
|
+
throw new Error(`Table ${tableName} does not exist (validate-only mode)`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Create table
|
|
271
|
+
const createSQL = generateMySQLCreateTable(tableName, attributes);
|
|
272
|
+
|
|
273
|
+
if (this.config.verbose) {
|
|
274
|
+
console.log(`[MySQLReplicator] Creating table ${tableName}:\n${createSQL}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await connection.query(createSQL);
|
|
278
|
+
|
|
279
|
+
this.emit('table_created', {
|
|
280
|
+
replicator: this.name,
|
|
281
|
+
tableName,
|
|
282
|
+
attributes: Object.keys(attributes)
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Table exists - check for schema changes
|
|
289
|
+
if (this.schemaSync.strategy === 'drop-create') {
|
|
290
|
+
if (this.config.verbose) {
|
|
291
|
+
console.warn(`[MySQLReplicator] Dropping and recreating table ${tableName}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
await connection.query(`DROP TABLE IF EXISTS ${tableName}`);
|
|
295
|
+
const createSQL = generateMySQLCreateTable(tableName, attributes);
|
|
296
|
+
await connection.query(createSQL);
|
|
297
|
+
|
|
298
|
+
this.emit('table_recreated', {
|
|
299
|
+
replicator: this.name,
|
|
300
|
+
tableName,
|
|
301
|
+
attributes: Object.keys(attributes)
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (this.schemaSync.strategy === 'alter' && this.schemaSync.autoCreateColumns) {
|
|
308
|
+
const alterStatements = generateMySQLAlterTable(tableName, attributes, existingSchema);
|
|
309
|
+
|
|
310
|
+
if (alterStatements.length > 0) {
|
|
311
|
+
if (this.config.verbose) {
|
|
312
|
+
console.log(`[MySQLReplicator] Altering table ${tableName}:`, alterStatements);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const stmt of alterStatements) {
|
|
316
|
+
await connection.query(stmt);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
this.emit('table_altered', {
|
|
320
|
+
replicator: this.name,
|
|
321
|
+
tableName,
|
|
322
|
+
addedColumns: alterStatements.length
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (this.schemaSync.strategy === 'validate-only') {
|
|
328
|
+
const alterStatements = generateMySQLAlterTable(tableName, attributes, existingSchema);
|
|
329
|
+
|
|
330
|
+
if (alterStatements.length > 0) {
|
|
331
|
+
throw new Error(`Table ${tableName} schema mismatch. Missing columns: ${alterStatements.length}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} finally {
|
|
335
|
+
connection.release();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
shouldReplicateResource(resourceName) {
|
|
340
|
+
return this.resources.hasOwnProperty(resourceName);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async _createLogTable() {
|
|
344
|
+
const mysql = requirePluginDependency('mysql2', 'MySQLReplicator');
|
|
345
|
+
|
|
346
|
+
const [ok] = await tryFn(async () => {
|
|
347
|
+
await this.pool.promise().query(`
|
|
348
|
+
CREATE TABLE IF NOT EXISTS ${mysql.escapeId(this.logTable)} (
|
|
349
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
350
|
+
resource_name VARCHAR(255) NOT NULL,
|
|
351
|
+
operation VARCHAR(50) NOT NULL,
|
|
352
|
+
record_id VARCHAR(255),
|
|
353
|
+
data JSON,
|
|
354
|
+
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
355
|
+
INDEX idx_resource (resource_name),
|
|
356
|
+
INDEX idx_timestamp (timestamp)
|
|
357
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
358
|
+
`);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
if (!ok && this.config.verbose) {
|
|
362
|
+
console.warn('[MySQLReplicator] Failed to create log table');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async replicate(resourceName, operation, data, id) {
|
|
367
|
+
if (!this.resources[resourceName]) {
|
|
368
|
+
throw new ReplicationError('Resource not configured for replication', {
|
|
369
|
+
operation: 'replicate',
|
|
370
|
+
replicatorClass: 'MySQLReplicator',
|
|
371
|
+
resourceName,
|
|
372
|
+
configuredResources: Object.keys(this.resources),
|
|
373
|
+
suggestion: 'Add resource to replicator resources configuration'
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const results = [];
|
|
378
|
+
|
|
379
|
+
for (const tableConfig of this.resources[resourceName]) {
|
|
380
|
+
if (!tableConfig.actions.includes(operation)) {
|
|
381
|
+
continue; // Skip if operation not allowed for this table
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const [ok, error, result] = await tryFn(async () => {
|
|
385
|
+
switch (operation) {
|
|
386
|
+
case 'insert':
|
|
387
|
+
return await this._insertRecord(tableConfig.table, data);
|
|
388
|
+
case 'update':
|
|
389
|
+
return await this._updateRecord(tableConfig.table, id, data);
|
|
390
|
+
case 'delete':
|
|
391
|
+
return await this._deleteRecord(tableConfig.table, id);
|
|
392
|
+
default:
|
|
393
|
+
throw new ReplicationError(`Unsupported operation: ${operation}`, {
|
|
394
|
+
operation: 'replicate',
|
|
395
|
+
replicatorClass: 'MySQLReplicator',
|
|
396
|
+
invalidOperation: operation,
|
|
397
|
+
supportedOperations: ['insert', 'update', 'delete']
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (ok) {
|
|
403
|
+
results.push(result);
|
|
404
|
+
|
|
405
|
+
// Log to replication log table if configured
|
|
406
|
+
if (this.logTable) {
|
|
407
|
+
await this._logOperation(resourceName, operation, id, data);
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
this.emit('replication_error', {
|
|
411
|
+
resource: resourceName,
|
|
412
|
+
operation,
|
|
413
|
+
table: tableConfig.table,
|
|
414
|
+
error: error.message
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (this.config.verbose) {
|
|
418
|
+
console.error(`[MySQLReplicator] Failed to replicate ${operation} for ${resourceName}:`, error);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return results.length > 0 ? results[0] : null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async _insertRecord(table, data) {
|
|
427
|
+
const mysql = requirePluginDependency('mysql2', 'MySQLReplicator');
|
|
428
|
+
const cleanData = this._cleanInternalFields(data);
|
|
429
|
+
|
|
430
|
+
const columns = Object.keys(cleanData);
|
|
431
|
+
const values = Object.values(cleanData);
|
|
432
|
+
const placeholders = values.map(() => '?').join(', ');
|
|
433
|
+
|
|
434
|
+
const query = `INSERT INTO ${mysql.escapeId(table)} (${columns.map(c => mysql.escapeId(c)).join(', ')}) VALUES (${placeholders})`;
|
|
435
|
+
|
|
436
|
+
const [result] = await this.pool.promise().query(query, values);
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async _updateRecord(table, id, data) {
|
|
441
|
+
const mysql = requirePluginDependency('mysql2', 'MySQLReplicator');
|
|
442
|
+
const cleanData = this._cleanInternalFields(data);
|
|
443
|
+
|
|
444
|
+
const updates = Object.keys(cleanData)
|
|
445
|
+
.map(col => `${mysql.escapeId(col)} = ?`)
|
|
446
|
+
.join(', ');
|
|
447
|
+
|
|
448
|
+
const values = [...Object.values(cleanData), id];
|
|
449
|
+
|
|
450
|
+
const query = `UPDATE ${mysql.escapeId(table)} SET ${updates} WHERE id = ?`;
|
|
451
|
+
|
|
452
|
+
const [result] = await this.pool.promise().query(query, values);
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async _deleteRecord(table, id) {
|
|
457
|
+
const mysql = requirePluginDependency('mysql2', 'MySQLReplicator');
|
|
458
|
+
const query = `DELETE FROM ${mysql.escapeId(table)} WHERE id = ?`;
|
|
459
|
+
|
|
460
|
+
const [result] = await this.pool.promise().query(query, [id]);
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async _logOperation(resourceName, operation, id, data) {
|
|
465
|
+
const mysql = requirePluginDependency('mysql2', 'MySQLReplicator');
|
|
466
|
+
|
|
467
|
+
const [ok] = await tryFn(async () => {
|
|
468
|
+
const query = `INSERT INTO ${mysql.escapeId(this.logTable)} (resource_name, operation, record_id, data) VALUES (?, ?, ?, ?)`;
|
|
469
|
+
await this.pool.promise().query(query, [resourceName, operation, id, JSON.stringify(data)]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
if (!ok && this.config.verbose) {
|
|
473
|
+
console.warn('[MySQLReplicator] Failed to log operation');
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
_cleanInternalFields(data) {
|
|
478
|
+
if (!data || typeof data !== 'object') return data;
|
|
479
|
+
|
|
480
|
+
const cleanData = { ...data };
|
|
481
|
+
|
|
482
|
+
// Remove internal s3db fields
|
|
483
|
+
Object.keys(cleanData).forEach(key => {
|
|
484
|
+
if (key.startsWith('$') || key.startsWith('_')) {
|
|
485
|
+
delete cleanData[key];
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return cleanData;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
async replicateBatch(resourceName, records) {
|
|
493
|
+
const results = [];
|
|
494
|
+
const errors = [];
|
|
495
|
+
|
|
496
|
+
for (const record of records) {
|
|
497
|
+
const [ok, err, result] = await tryFn(() =>
|
|
498
|
+
this.replicate(resourceName, record.operation, record.data, record.id)
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
if (ok) {
|
|
502
|
+
results.push(result);
|
|
503
|
+
} else {
|
|
504
|
+
errors.push({ id: record.id, error: err.message });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
success: errors.length === 0,
|
|
510
|
+
results,
|
|
511
|
+
errors,
|
|
512
|
+
total: records.length
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async testConnection() {
|
|
517
|
+
const [ok, err] = await tryFn(async () => {
|
|
518
|
+
if (!this.pool) {
|
|
519
|
+
throw new Error('Pool not initialized');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const connection = await this.pool.promise().getConnection();
|
|
523
|
+
await connection.ping();
|
|
524
|
+
connection.release();
|
|
525
|
+
return true;
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
if (!ok) {
|
|
529
|
+
this.emit('connection_error', { replicator: 'MySQLReplicator', error: err.message });
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async getStatus() {
|
|
537
|
+
const baseStatus = await super.getStatus();
|
|
538
|
+
return {
|
|
539
|
+
...baseStatus,
|
|
540
|
+
connected: !!this.pool,
|
|
541
|
+
host: this.host,
|
|
542
|
+
database: this.database,
|
|
543
|
+
resources: Object.keys(this.resources),
|
|
544
|
+
poolConnections: this.pool ? this.pool.pool.allConnections.length : 0,
|
|
545
|
+
schemaSync: this.schemaSync
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async cleanup() {
|
|
550
|
+
if (this.pool) {
|
|
551
|
+
await this.pool.end();
|
|
552
|
+
this.pool = null;
|
|
553
|
+
}
|
|
554
|
+
await super.cleanup();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
export default MySQLReplicator;
|