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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s3db.js",
3
- "version": "7.2.0",
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
- * Replicates data to Google BigQuery tables, supporting per-resource table mapping and action filtering.
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
- * @config {Object} config - Configuration object for the replicator
20
- * @config {string} config.projectId - (Required) Google Cloud project ID
21
- * @config {string} config.datasetId - (Required) BigQuery dataset ID
22
- * @config {Object} [config.credentials] - (Optional) Google service account credentials object (JSON). If omitted, uses default credentials.
23
- * @config {string} [config.location='US'] - (Optional) BigQuery dataset location/region
24
- * @config {string} [config.logTable] - (Optional) Table name for operation logging. If omitted, no logging is performed.
25
- * @config {Object} resources - Resource configuration mapping
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
- * location: 'US',
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
- * { actions: ['insert', 'update', 'delete'], table: 'users_table' },
41
- * ],
42
- * urls: [
43
- * { actions: ['insert'], table: 'urls_table' },
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
- * Notes:
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 => tableConfig.table);
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 tables = this.getTablesForResource(resourceName, operation);
179
- if (tables.length === 0) {
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 tableId of tables) {
189
+ for (const tableConfig of tableConfigs) {
190
190
  const [okTable, errTable] = await tryFn(async () => {
191
- const table = dataset.table(tableId);
191
+ const table = dataset.table(tableConfig.table);
192
192
  let job;
193
+
193
194
  if (operation === 'insert') {
194
- const row = { ...data };
195
- job = await table.insert([row]);
195
+ const transformedData = this.applyTransform(data, tableConfig.transform);
196
+ job = await table.insert([transformedData]);
196
197
  } else if (operation === 'update') {
197
- const keys = Object.keys(data).filter(k => k !== 'id');
198
- const setClause = keys.map(k => `${k}=@${k}`).join(', ');
199
- const params = { id };
200
- keys.forEach(k => { params[k] = data[k]; });
201
- const query = `UPDATE \`${this.projectId}.${this.datasetId}.${tableId}\` SET ${setClause} WHERE id=@id`;
202
- const [updateJob] = await this.bigqueryClient.createQueryJob({
203
- query,
204
- params
205
- });
206
- await updateJob.getQueryResults();
207
- job = [updateJob];
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}.${tableId}\` WHERE id=@id`;
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: tableId,
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: tableId,
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
- * 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:
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
- * @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
- * }
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
- * ssl: false,
105
- * logTable: 's3db_replicator_log'
25
+ * logTable: 'replication_log'
106
26
  * }, {
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' }
27
+ * users: [{ actions: ['insert', 'update'], table: 'users_table' }],
28
+ * orders: 'orders_table'
115
29
  * })
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
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
- * S3DB Replicator Configuration Documentation
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 - Replicates data to another s3db instance
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) {