s3db.js 6.2.0 → 7.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/PLUGINS.md +2724 -0
- package/README.md +372 -469
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +30057 -18387
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +30043 -18384
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +29730 -18061
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +44 -69
- package/src/behaviors/body-only.js +110 -0
- package/src/behaviors/body-overflow.js +153 -0
- package/src/behaviors/enforce-limits.js +195 -0
- package/src/behaviors/index.js +39 -0
- package/src/behaviors/truncate-data.js +204 -0
- package/src/behaviors/user-managed.js +147 -0
- package/src/client.class.js +515 -0
- package/src/concerns/base62.js +61 -0
- package/src/concerns/calculator.js +204 -0
- package/src/concerns/crypto.js +142 -0
- package/src/concerns/id.js +8 -0
- package/src/concerns/index.js +5 -0
- package/src/concerns/try-fn.js +151 -0
- package/src/connection-string.class.js +75 -0
- package/src/database.class.js +599 -0
- package/src/errors.js +261 -0
- package/src/index.js +17 -0
- package/src/plugins/audit.plugin.js +442 -0
- package/src/plugins/cache/cache.class.js +53 -0
- package/src/plugins/cache/index.js +6 -0
- package/src/plugins/cache/memory-cache.class.js +164 -0
- package/src/plugins/cache/s3-cache.class.js +189 -0
- package/src/plugins/cache.plugin.js +275 -0
- package/src/plugins/consumers/index.js +24 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
- package/src/plugins/consumers/sqs-consumer.js +102 -0
- package/src/plugins/costs.plugin.js +81 -0
- package/src/plugins/fulltext.plugin.js +473 -0
- package/src/plugins/index.js +12 -0
- package/src/plugins/metrics.plugin.js +603 -0
- package/src/plugins/plugin.class.js +210 -0
- package/src/plugins/plugin.obj.js +13 -0
- package/src/plugins/queue-consumer.plugin.js +134 -0
- package/src/plugins/replicator.plugin.js +769 -0
- package/src/plugins/replicators/base-replicator.class.js +85 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
- package/src/plugins/replicators/index.js +44 -0
- package/src/plugins/replicators/postgres-replicator.class.js +427 -0
- package/src/plugins/replicators/s3db-replicator.class.js +352 -0
- package/src/plugins/replicators/sqs-replicator.class.js +427 -0
- package/src/resource.class.js +2626 -0
- package/src/s3db.d.ts +1263 -0
- package/src/schema.class.js +706 -0
- package/src/stream/index.js +16 -0
- package/src/stream/resource-ids-page-reader.class.js +10 -0
- package/src/stream/resource-ids-reader.class.js +63 -0
- package/src/stream/resource-reader.class.js +81 -0
- package/src/stream/resource-writer.class.js +92 -0
- package/src/validator.class.js +97 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres Replicator Configuration Documentation
|
|
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:
|
|
9
|
+
*
|
|
10
|
+
* ```bash
|
|
11
|
+
* npm install pg
|
|
12
|
+
* # or
|
|
13
|
+
* yarn add pg
|
|
14
|
+
* # or
|
|
15
|
+
* pnpm add pg
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* @typedef {Object} PostgresReplicatorConfig
|
|
19
|
+
* @property {string} database - The name of the PostgreSQL database to connect to
|
|
20
|
+
* @property {string} resourceArn - The ARN of the Aurora Serverless cluster or RDS instance
|
|
21
|
+
* @property {string} secretArn - The ARN of the Secrets Manager secret containing database credentials
|
|
22
|
+
* @property {string} [region='us-east-1'] - AWS region where the database is located
|
|
23
|
+
* @property {Object.<string, string>} [tableMapping] - Maps s3db resource names to PostgreSQL table names
|
|
24
|
+
* - Key: s3db resource name (e.g., 'users', 'orders')
|
|
25
|
+
* - Value: PostgreSQL table name (e.g., 'public.users', 'analytics.orders')
|
|
26
|
+
* - If not provided, resource names are used as table names
|
|
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
|
+
* }
|
|
58
|
+
*
|
|
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
|
+
* @example
|
|
102
|
+
* new PostgresReplicator({
|
|
103
|
+
* connectionString: 'postgresql://user:password@localhost:5432/analytics',
|
|
104
|
+
* ssl: false,
|
|
105
|
+
* logTable: 's3db_replicator_log'
|
|
106
|
+
* }, {
|
|
107
|
+
* users: [
|
|
108
|
+
* { actions: ['insert', 'update', 'delete'], table: 'users_table' },
|
|
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' }
|
|
115
|
+
* })
|
|
116
|
+
*
|
|
117
|
+
* Notes:
|
|
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
|
|
122
|
+
*/
|
|
123
|
+
class PostgresReplicator extends BaseReplicator {
|
|
124
|
+
constructor(config = {}, resources = {}) {
|
|
125
|
+
super(config);
|
|
126
|
+
this.connectionString = config.connectionString;
|
|
127
|
+
this.host = config.host;
|
|
128
|
+
this.port = config.port || 5432;
|
|
129
|
+
this.database = config.database;
|
|
130
|
+
this.user = config.user;
|
|
131
|
+
this.password = config.password;
|
|
132
|
+
this.client = null;
|
|
133
|
+
this.ssl = config.ssl;
|
|
134
|
+
this.logTable = config.logTable;
|
|
135
|
+
|
|
136
|
+
// Parse resources configuration
|
|
137
|
+
this.resources = this.parseResourcesConfig(resources);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
parseResourcesConfig(resources) {
|
|
141
|
+
const parsed = {};
|
|
142
|
+
|
|
143
|
+
for (const [resourceName, config] of Object.entries(resources)) {
|
|
144
|
+
if (typeof config === 'string') {
|
|
145
|
+
// Short form: just table name
|
|
146
|
+
parsed[resourceName] = [{
|
|
147
|
+
table: config,
|
|
148
|
+
actions: ['insert']
|
|
149
|
+
}];
|
|
150
|
+
} else if (Array.isArray(config)) {
|
|
151
|
+
// Array form: multiple table mappings
|
|
152
|
+
parsed[resourceName] = config.map(item => {
|
|
153
|
+
if (typeof item === 'string') {
|
|
154
|
+
return { table: item, actions: ['insert'] };
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
table: item.table,
|
|
158
|
+
actions: item.actions || ['insert']
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
} else if (typeof config === 'object') {
|
|
162
|
+
// Single object form
|
|
163
|
+
parsed[resourceName] = [{
|
|
164
|
+
table: config.table,
|
|
165
|
+
actions: config.actions || ['insert']
|
|
166
|
+
}];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return parsed;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
validateConfig() {
|
|
174
|
+
const errors = [];
|
|
175
|
+
if (!this.connectionString && (!this.host || !this.database)) {
|
|
176
|
+
errors.push('Either connectionString or host+database must be provided');
|
|
177
|
+
}
|
|
178
|
+
if (Object.keys(this.resources).length === 0) {
|
|
179
|
+
errors.push('At least one resource must be configured');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate resource configurations
|
|
183
|
+
for (const [resourceName, tables] of Object.entries(this.resources)) {
|
|
184
|
+
for (const tableConfig of tables) {
|
|
185
|
+
if (!tableConfig.table) {
|
|
186
|
+
errors.push(`Table name is required for resource '${resourceName}'`);
|
|
187
|
+
}
|
|
188
|
+
if (!Array.isArray(tableConfig.actions) || tableConfig.actions.length === 0) {
|
|
189
|
+
errors.push(`Actions array is required for resource '${resourceName}'`);
|
|
190
|
+
}
|
|
191
|
+
const validActions = ['insert', 'update', 'delete'];
|
|
192
|
+
const invalidActions = tableConfig.actions.filter(action => !validActions.includes(action));
|
|
193
|
+
if (invalidActions.length > 0) {
|
|
194
|
+
errors.push(`Invalid actions for resource '${resourceName}': ${invalidActions.join(', ')}. Valid actions: ${validActions.join(', ')}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { isValid: errors.length === 0, errors };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async initialize(database) {
|
|
203
|
+
await super.initialize(database);
|
|
204
|
+
const [ok, err, sdk] = await tryFn(() => import('pg'));
|
|
205
|
+
if (!ok) {
|
|
206
|
+
this.emit('initialization_error', {
|
|
207
|
+
replicator: this.name,
|
|
208
|
+
error: err.message
|
|
209
|
+
});
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
const { Client } = sdk;
|
|
213
|
+
const config = this.connectionString ? {
|
|
214
|
+
connectionString: this.connectionString,
|
|
215
|
+
ssl: this.ssl
|
|
216
|
+
} : {
|
|
217
|
+
host: this.host,
|
|
218
|
+
port: this.port,
|
|
219
|
+
database: this.database,
|
|
220
|
+
user: this.user,
|
|
221
|
+
password: this.password,
|
|
222
|
+
ssl: this.ssl
|
|
223
|
+
};
|
|
224
|
+
this.client = new Client(config);
|
|
225
|
+
await this.client.connect();
|
|
226
|
+
// Create log table if configured
|
|
227
|
+
if (this.logTable) {
|
|
228
|
+
await this.createLogTableIfNotExists();
|
|
229
|
+
}
|
|
230
|
+
this.emit('initialized', {
|
|
231
|
+
replicator: this.name,
|
|
232
|
+
database: this.database || 'postgres',
|
|
233
|
+
resources: Object.keys(this.resources)
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async createLogTableIfNotExists() {
|
|
238
|
+
const createTableQuery = `
|
|
239
|
+
CREATE TABLE IF NOT EXISTS ${this.logTable} (
|
|
240
|
+
id SERIAL PRIMARY KEY,
|
|
241
|
+
resource_name VARCHAR(255) NOT NULL,
|
|
242
|
+
operation VARCHAR(50) NOT NULL,
|
|
243
|
+
record_id VARCHAR(255) NOT NULL,
|
|
244
|
+
data JSONB,
|
|
245
|
+
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
246
|
+
source VARCHAR(100) DEFAULT 's3db-replicator',
|
|
247
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
248
|
+
);
|
|
249
|
+
CREATE INDEX IF NOT EXISTS idx_${this.logTable}_resource_name ON ${this.logTable}(resource_name);
|
|
250
|
+
CREATE INDEX IF NOT EXISTS idx_${this.logTable}_operation ON ${this.logTable}(operation);
|
|
251
|
+
CREATE INDEX IF NOT EXISTS idx_${this.logTable}_record_id ON ${this.logTable}(record_id);
|
|
252
|
+
CREATE INDEX IF NOT EXISTS idx_${this.logTable}_timestamp ON ${this.logTable}(timestamp);
|
|
253
|
+
`;
|
|
254
|
+
await this.client.query(createTableQuery);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
shouldReplicateResource(resourceName) {
|
|
258
|
+
return this.resources.hasOwnProperty(resourceName);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
shouldReplicateAction(resourceName, operation) {
|
|
262
|
+
if (!this.resources[resourceName]) return false;
|
|
263
|
+
|
|
264
|
+
return this.resources[resourceName].some(tableConfig =>
|
|
265
|
+
tableConfig.actions.includes(operation)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getTablesForResource(resourceName, operation) {
|
|
270
|
+
if (!this.resources[resourceName]) return [];
|
|
271
|
+
|
|
272
|
+
return this.resources[resourceName]
|
|
273
|
+
.filter(tableConfig => tableConfig.actions.includes(operation))
|
|
274
|
+
.map(tableConfig => tableConfig.table);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async replicate(resourceName, operation, data, id, beforeData = null) {
|
|
278
|
+
if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
|
|
279
|
+
return { skipped: true, reason: 'resource_not_included' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!this.shouldReplicateAction(resourceName, operation)) {
|
|
283
|
+
return { skipped: true, reason: 'action_not_included' };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const tables = this.getTablesForResource(resourceName, operation);
|
|
287
|
+
if (tables.length === 0) {
|
|
288
|
+
return { skipped: true, reason: 'no_tables_for_action' };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const results = [];
|
|
292
|
+
const errors = [];
|
|
293
|
+
|
|
294
|
+
const [ok, err, result] = await tryFn(async () => {
|
|
295
|
+
// Replicate to all applicable tables
|
|
296
|
+
for (const table of tables) {
|
|
297
|
+
const [okTable, errTable] = await tryFn(async () => {
|
|
298
|
+
let result;
|
|
299
|
+
|
|
300
|
+
if (operation === 'insert') {
|
|
301
|
+
// INSERT INTO table (col1, col2, ...) VALUES (...)
|
|
302
|
+
const keys = Object.keys(data);
|
|
303
|
+
const values = keys.map(k => data[k]);
|
|
304
|
+
const columns = keys.map(k => `"${k}"`).join(', ');
|
|
305
|
+
const params = keys.map((_, i) => `$${i + 1}`).join(', ');
|
|
306
|
+
const sql = `INSERT INTO ${table} (${columns}) VALUES (${params}) ON CONFLICT (id) DO NOTHING RETURNING *`;
|
|
307
|
+
result = await this.client.query(sql, values);
|
|
308
|
+
} else if (operation === 'update') {
|
|
309
|
+
// UPDATE table SET col1=$1, col2=$2 ... WHERE id=$N
|
|
310
|
+
const keys = Object.keys(data).filter(k => k !== 'id');
|
|
311
|
+
const setClause = keys.map((k, i) => `"${k}"=$${i + 1}`).join(', ');
|
|
312
|
+
const values = keys.map(k => data[k]);
|
|
313
|
+
values.push(id);
|
|
314
|
+
const sql = `UPDATE ${table} SET ${setClause} WHERE id=$${keys.length + 1} RETURNING *`;
|
|
315
|
+
result = await this.client.query(sql, values);
|
|
316
|
+
} else if (operation === 'delete') {
|
|
317
|
+
// DELETE FROM table WHERE id=$1
|
|
318
|
+
const sql = `DELETE FROM ${table} WHERE id=$1 RETURNING *`;
|
|
319
|
+
result = await this.client.query(sql, [id]);
|
|
320
|
+
} else {
|
|
321
|
+
throw new Error(`Unsupported operation: ${operation}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
results.push({
|
|
325
|
+
table,
|
|
326
|
+
success: true,
|
|
327
|
+
rows: result.rows,
|
|
328
|
+
rowCount: result.rowCount
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
if (!okTable) {
|
|
332
|
+
errors.push({
|
|
333
|
+
table,
|
|
334
|
+
error: errTable.message
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Log operation if logTable is configured
|
|
339
|
+
if (this.logTable) {
|
|
340
|
+
const [okLog, errLog] = await tryFn(async () => {
|
|
341
|
+
await this.client.query(
|
|
342
|
+
`INSERT INTO ${this.logTable} (resource_name, operation, record_id, data, timestamp, source) VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
343
|
+
[resourceName, operation, id, JSON.stringify(data), new Date().toISOString(), 's3db-replicator']
|
|
344
|
+
);
|
|
345
|
+
});
|
|
346
|
+
if (!okLog) {
|
|
347
|
+
// Don't fail the main operation if logging fails
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const success = errors.length === 0;
|
|
351
|
+
this.emit('replicated', {
|
|
352
|
+
replicator: this.name,
|
|
353
|
+
resourceName,
|
|
354
|
+
operation,
|
|
355
|
+
id,
|
|
356
|
+
tables,
|
|
357
|
+
results,
|
|
358
|
+
errors,
|
|
359
|
+
success
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
success,
|
|
363
|
+
results,
|
|
364
|
+
errors,
|
|
365
|
+
tables
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
if (ok) return result;
|
|
369
|
+
this.emit('replicator_error', {
|
|
370
|
+
replicator: this.name,
|
|
371
|
+
resourceName,
|
|
372
|
+
operation,
|
|
373
|
+
id,
|
|
374
|
+
error: err.message
|
|
375
|
+
});
|
|
376
|
+
return { success: false, error: err.message };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async replicateBatch(resourceName, records) {
|
|
380
|
+
const results = [];
|
|
381
|
+
const errors = [];
|
|
382
|
+
|
|
383
|
+
for (const record of records) {
|
|
384
|
+
const [ok, err, res] = await tryFn(() => this.replicate(
|
|
385
|
+
resourceName,
|
|
386
|
+
record.operation,
|
|
387
|
+
record.data,
|
|
388
|
+
record.id,
|
|
389
|
+
record.beforeData
|
|
390
|
+
));
|
|
391
|
+
if (ok) results.push(res);
|
|
392
|
+
else errors.push({ id: record.id, error: err.message });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
success: errors.length === 0,
|
|
397
|
+
results,
|
|
398
|
+
errors
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async testConnection() {
|
|
403
|
+
const [ok, err] = await tryFn(async () => {
|
|
404
|
+
if (!this.client) await this.initialize();
|
|
405
|
+
await this.client.query('SELECT 1');
|
|
406
|
+
return true;
|
|
407
|
+
});
|
|
408
|
+
if (ok) return true;
|
|
409
|
+
this.emit('connection_error', { replicator: this.name, error: err.message });
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async cleanup() {
|
|
414
|
+
if (this.client) await this.client.end();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
getStatus() {
|
|
418
|
+
return {
|
|
419
|
+
...super.getStatus(),
|
|
420
|
+
database: this.database || 'postgres',
|
|
421
|
+
resources: this.resources,
|
|
422
|
+
logTable: this.logTable
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export default PostgresReplicator;
|