s3db.js 7.3.6 → 7.3.9

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.3.6",
3
+ "version": "7.3.9",
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",
@@ -41,14 +41,14 @@ class BigqueryReplicator extends BaseReplicator {
41
41
  this.credentials = config.credentials;
42
42
  this.location = config.location || 'US';
43
43
  this.logTable = config.logTable;
44
-
44
+
45
45
  // Parse resources configuration
46
46
  this.resources = this.parseResourcesConfig(resources);
47
47
  }
48
48
 
49
49
  parseResourcesConfig(resources) {
50
50
  const parsed = {};
51
-
51
+
52
52
  for (const [resourceName, config] of Object.entries(resources)) {
53
53
  if (typeof config === 'string') {
54
54
  // Short form: just table name
@@ -78,7 +78,7 @@ class BigqueryReplicator extends BaseReplicator {
78
78
  }];
79
79
  }
80
80
  }
81
-
81
+
82
82
  return parsed;
83
83
  }
84
84
 
@@ -87,7 +87,7 @@ class BigqueryReplicator extends BaseReplicator {
87
87
  if (!this.projectId) errors.push('projectId is required');
88
88
  if (!this.datasetId) errors.push('datasetId is required');
89
89
  if (Object.keys(this.resources).length === 0) errors.push('At least one resource must be configured');
90
-
90
+
91
91
  // Validate resource configurations
92
92
  for (const [resourceName, tables] of Object.entries(this.resources)) {
93
93
  for (const tableConfig of tables) {
@@ -107,7 +107,7 @@ class BigqueryReplicator extends BaseReplicator {
107
107
  }
108
108
  }
109
109
  }
110
-
110
+
111
111
  return { isValid: errors.length === 0, errors };
112
112
  }
113
113
 
@@ -141,15 +141,15 @@ class BigqueryReplicator extends BaseReplicator {
141
141
 
142
142
  shouldReplicateAction(resourceName, operation) {
143
143
  if (!this.resources[resourceName]) return false;
144
-
145
- return this.resources[resourceName].some(tableConfig =>
144
+
145
+ return this.resources[resourceName].some(tableConfig =>
146
146
  tableConfig.actions.includes(operation)
147
147
  );
148
148
  }
149
149
 
150
150
  getTablesForResource(resourceName, operation) {
151
151
  if (!this.resources[resourceName]) return [];
152
-
152
+
153
153
  return this.resources[resourceName]
154
154
  .filter(tableConfig => tableConfig.actions.includes(operation))
155
155
  .map(tableConfig => ({
@@ -159,16 +159,32 @@ class BigqueryReplicator extends BaseReplicator {
159
159
  }
160
160
 
161
161
  applyTransform(data, transformFn) {
162
- if (!transformFn) return data;
163
-
164
- let transformedData = JSON.parse(JSON.stringify(data));
165
- if (transformedData._length) delete transformedData._length;
166
-
162
+ // First, clean internal fields that shouldn't go to BigQuery
163
+ let cleanData = this._cleanInternalFields(data);
164
+
165
+ if (!transformFn) return cleanData;
166
+
167
+ let transformedData = JSON.parse(JSON.stringify(cleanData));
167
168
  return transformFn(transformedData);
168
169
  }
169
170
 
171
+ _cleanInternalFields(data) {
172
+ if (!data || typeof data !== 'object') return data;
173
+
174
+ const cleanData = { ...data };
175
+
176
+ // Remove internal fields that start with $ or _
177
+ Object.keys(cleanData).forEach(key => {
178
+ if (key.startsWith('$') || key.startsWith('_')) {
179
+ delete cleanData[key];
180
+ }
181
+ });
182
+
183
+ return cleanData;
184
+ }
185
+
170
186
  async replicate(resourceName, operation, data, id, beforeData = null) {
171
-
187
+
172
188
  if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
173
189
  return { skipped: true, reason: 'resource_not_included' };
174
190
  }
@@ -187,27 +203,38 @@ class BigqueryReplicator extends BaseReplicator {
187
203
 
188
204
  const [ok, err, result] = await tryFn(async () => {
189
205
  const dataset = this.bigqueryClient.dataset(this.datasetId);
190
-
206
+
191
207
  // Replicate to all applicable tables
192
208
  for (const tableConfig of tableConfigs) {
193
209
  const [okTable, errTable] = await tryFn(async () => {
194
210
  const table = dataset.table(tableConfig.table);
195
211
  let job;
196
-
212
+
197
213
  if (operation === 'insert') {
198
214
  const transformedData = this.applyTransform(data, tableConfig.transform);
199
- job = await table.insert([transformedData]);
215
+ try {
216
+ job = await table.insert([transformedData]);
217
+ } catch (error) {
218
+ // Extract detailed BigQuery error information
219
+ const { errors, response } = error;
220
+ if (this.config.verbose) {
221
+ console.error('[BigqueryReplicator] BigQuery insert error details:');
222
+ if (errors) console.error('Errors:', JSON.stringify(errors, null, 2));
223
+ if (response) console.error('Response:', JSON.stringify(response, null, 2));
224
+ }
225
+ throw error;
226
+ }
200
227
  } else if (operation === 'update') {
201
228
  const transformedData = this.applyTransform(data, tableConfig.transform);
202
229
  const keys = Object.keys(transformedData).filter(k => k !== 'id');
203
230
  const setClause = keys.map(k => `${k} = @${k}`).join(', ');
204
231
  const params = { id, ...transformedData };
205
232
  const query = `UPDATE \`${this.projectId}.${this.datasetId}.${tableConfig.table}\` SET ${setClause} WHERE id = @id`;
206
-
233
+
207
234
  // Retry logic for streaming buffer issues
208
235
  const maxRetries = 2;
209
236
  let lastError = null;
210
-
237
+
211
238
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
212
239
  const [ok, error] = await tryFn(async () => {
213
240
  const [updateJob] = await this.bigqueryClient.createQueryJob({
@@ -218,17 +245,22 @@ class BigqueryReplicator extends BaseReplicator {
218
245
  await updateJob.getQueryResults();
219
246
  return [updateJob];
220
247
  });
221
-
248
+
222
249
  if (ok) {
223
250
  job = ok;
224
251
  break;
225
252
  } else {
226
253
  lastError = error;
227
-
254
+
255
+ // Enhanced error logging for BigQuery update operations
228
256
  if (this.config.verbose) {
229
257
  console.warn(`[BigqueryReplicator] Update attempt ${attempt} failed: ${error.message}`);
258
+ if (error.errors) {
259
+ console.error('[BigqueryReplicator] BigQuery update error details:');
260
+ console.error('Errors:', JSON.stringify(error.errors, null, 2));
261
+ }
230
262
  }
231
-
263
+
232
264
  // If it's streaming buffer error and not the last attempt
233
265
  if (error?.message?.includes('streaming buffer') && attempt < maxRetries) {
234
266
  const delaySeconds = 30;
@@ -238,32 +270,43 @@ class BigqueryReplicator extends BaseReplicator {
238
270
  await new Promise(resolve => setTimeout(resolve, delaySeconds * 1000));
239
271
  continue;
240
272
  }
241
-
273
+
242
274
  throw error;
243
275
  }
244
276
  }
245
-
277
+
246
278
  if (!job) throw lastError;
247
279
  } else if (operation === 'delete') {
248
280
  const query = `DELETE FROM \`${this.projectId}.${this.datasetId}.${tableConfig.table}\` WHERE id = @id`;
249
- const [deleteJob] = await this.bigqueryClient.createQueryJob({
250
- query,
251
- params: { id },
252
- location: this.location
253
- });
254
- await deleteJob.getQueryResults();
255
- job = [deleteJob];
281
+ try {
282
+ const [deleteJob] = await this.bigqueryClient.createQueryJob({
283
+ query,
284
+ params: { id },
285
+ location: this.location
286
+ });
287
+ await deleteJob.getQueryResults();
288
+ job = [deleteJob];
289
+ } catch (error) {
290
+ // Enhanced error logging for BigQuery delete operations
291
+ if (this.config.verbose) {
292
+ console.error('[BigqueryReplicator] BigQuery delete error details:');
293
+ console.error('Query:', query);
294
+ if (error.errors) console.error('Errors:', JSON.stringify(error.errors, null, 2));
295
+ if (error.response) console.error('Response:', JSON.stringify(error.response, null, 2));
296
+ }
297
+ throw error;
298
+ }
256
299
  } else {
257
300
  throw new Error(`Unsupported operation: ${operation}`);
258
301
  }
259
-
302
+
260
303
  results.push({
261
304
  table: tableConfig.table,
262
305
  success: true,
263
306
  jobId: job[0]?.id
264
307
  });
265
308
  });
266
-
309
+
267
310
  if (!okTable) {
268
311
  errors.push({
269
312
  table: tableConfig.table,
@@ -271,7 +314,7 @@ class BigqueryReplicator extends BaseReplicator {
271
314
  });
272
315
  }
273
316
  }
274
-
317
+
275
318
  // Log operation if logTable is configured
276
319
  if (this.logTable) {
277
320
  const [okLog, errLog] = await tryFn(async () => {
@@ -289,8 +332,14 @@ class BigqueryReplicator extends BaseReplicator {
289
332
  // Don't fail the main operation if logging fails
290
333
  }
291
334
  }
292
-
335
+
293
336
  const success = errors.length === 0;
337
+
338
+ // Log errors if any occurred
339
+ if (errors.length > 0) {
340
+ console.warn(`[BigqueryReplicator] Replication completed with errors for ${resourceName}:`, errors);
341
+ }
342
+
294
343
  this.emit('replicated', {
295
344
  replicator: this.name,
296
345
  resourceName,
@@ -301,17 +350,17 @@ class BigqueryReplicator extends BaseReplicator {
301
350
  errors,
302
351
  success
303
352
  });
304
-
305
- return {
306
- success,
307
- results,
353
+
354
+ return {
355
+ success,
356
+ results,
308
357
  errors,
309
358
  tables: tableConfigs.map(t => t.table)
310
359
  };
311
360
  });
312
-
361
+
313
362
  if (ok) return result;
314
-
363
+
315
364
  if (this.config.verbose) {
316
365
  console.warn(`[BigqueryReplicator] Replication failed for ${resourceName}: ${err.message}`);
317
366
  }
@@ -322,20 +371,20 @@ class BigqueryReplicator extends BaseReplicator {
322
371
  id,
323
372
  error: err.message
324
373
  });
325
-
374
+
326
375
  return { success: false, error: err.message };
327
376
  }
328
377
 
329
378
  async replicateBatch(resourceName, records) {
330
379
  const results = [];
331
380
  const errors = [];
332
-
381
+
333
382
  for (const record of records) {
334
383
  const [ok, err, res] = await tryFn(() => this.replicate(
335
- resourceName,
336
- record.operation,
337
- record.data,
338
- record.id,
384
+ resourceName,
385
+ record.operation,
386
+ record.data,
387
+ record.id,
339
388
  record.beforeData
340
389
  ));
341
390
  if (ok) {
@@ -347,11 +396,16 @@ class BigqueryReplicator extends BaseReplicator {
347
396
  errors.push({ id: record.id, error: err.message });
348
397
  }
349
398
  }
350
-
351
- return {
352
- success: errors.length === 0,
353
- results,
354
- errors
399
+
400
+ // Log errors if any occurred during batch processing
401
+ if (errors.length > 0) {
402
+ console.warn(`[BigqueryReplicator] Batch replication completed with ${errors.length} error(s) for ${resourceName}:`, errors);
403
+ }
404
+
405
+ return {
406
+ success: errors.length === 0,
407
+ results,
408
+ errors
355
409
  };
356
410
  }
357
411
 
@@ -211,18 +211,22 @@ class PostgresReplicator extends BaseReplicator {
211
211
  let result;
212
212
 
213
213
  if (operation === 'insert') {
214
+ // Clean internal fields before processing
215
+ const cleanData = this._cleanInternalFields(data);
214
216
  // INSERT INTO table (col1, col2, ...) VALUES (...)
215
- const keys = Object.keys(data);
216
- const values = keys.map(k => data[k]);
217
+ const keys = Object.keys(cleanData);
218
+ const values = keys.map(k => cleanData[k]);
217
219
  const columns = keys.map(k => `"${k}"`).join(', ');
218
220
  const params = keys.map((_, i) => `$${i + 1}`).join(', ');
219
221
  const sql = `INSERT INTO ${table} (${columns}) VALUES (${params}) ON CONFLICT (id) DO NOTHING RETURNING *`;
220
222
  result = await this.client.query(sql, values);
221
223
  } else if (operation === 'update') {
224
+ // Clean internal fields before processing
225
+ const cleanData = this._cleanInternalFields(data);
222
226
  // UPDATE table SET col1=$1, col2=$2 ... WHERE id=$N
223
- const keys = Object.keys(data).filter(k => k !== 'id');
227
+ const keys = Object.keys(cleanData).filter(k => k !== 'id');
224
228
  const setClause = keys.map((k, i) => `"${k}"=$${i + 1}`).join(', ');
225
- const values = keys.map(k => data[k]);
229
+ const values = keys.map(k => cleanData[k]);
226
230
  values.push(id);
227
231
  const sql = `UPDATE ${table} SET ${setClause} WHERE id=$${keys.length + 1} RETURNING *`;
228
232
  result = await this.client.query(sql, values);
@@ -261,6 +265,12 @@ class PostgresReplicator extends BaseReplicator {
261
265
  }
262
266
  }
263
267
  const success = errors.length === 0;
268
+
269
+ // Log errors if any occurred
270
+ if (errors.length > 0) {
271
+ console.warn(`[PostgresReplicator] Replication completed with errors for ${resourceName}:`, errors);
272
+ }
273
+
264
274
  this.emit('replicated', {
265
275
  replicator: this.name,
266
276
  resourceName,
@@ -314,6 +324,11 @@ class PostgresReplicator extends BaseReplicator {
314
324
  }
315
325
  }
316
326
 
327
+ // Log errors if any occurred during batch processing
328
+ if (errors.length > 0) {
329
+ console.warn(`[PostgresReplicator] Batch replication completed with ${errors.length} error(s) for ${resourceName}:`, errors);
330
+ }
331
+
317
332
  return {
318
333
  success: errors.length === 0,
319
334
  results,
@@ -335,6 +350,21 @@ class PostgresReplicator extends BaseReplicator {
335
350
  return false;
336
351
  }
337
352
 
353
+ _cleanInternalFields(data) {
354
+ if (!data || typeof data !== 'object') return data;
355
+
356
+ const cleanData = { ...data };
357
+
358
+ // Remove internal fields that start with $ or _
359
+ Object.keys(cleanData).forEach(key => {
360
+ if (key.startsWith('$') || key.startsWith('_')) {
361
+ delete cleanData[key];
362
+ }
363
+ });
364
+
365
+ return cleanData;
366
+ }
367
+
338
368
  async cleanup() {
339
369
  if (this.client) await this.client.end();
340
370
  }
@@ -249,44 +249,62 @@ class S3dbReplicator extends BaseReplicator {
249
249
  }
250
250
 
251
251
  _applyTransformer(resource, data) {
252
+ // First, clean internal fields that shouldn't go to target S3DB
253
+ let cleanData = this._cleanInternalFields(data);
254
+
252
255
  const normResource = normalizeResourceName(resource);
253
256
  const entry = this.resourcesMap[normResource];
254
257
  let result;
255
- if (!entry) return data;
258
+ if (!entry) return cleanData;
256
259
 
257
260
  // Array of multiple destinations - use first transform found
258
261
  if (Array.isArray(entry)) {
259
262
  for (const item of entry) {
260
263
  if (typeof item === 'object' && item.transform && typeof item.transform === 'function') {
261
- result = item.transform(data);
264
+ result = item.transform(cleanData);
262
265
  break;
263
266
  } else if (typeof item === 'object' && item.transformer && typeof item.transformer === 'function') {
264
- result = item.transformer(data);
267
+ result = item.transformer(cleanData);
265
268
  break;
266
269
  }
267
270
  }
268
- if (!result) result = data;
271
+ if (!result) result = cleanData;
269
272
  } else if (typeof entry === 'object') {
270
273
  // Prefer transform, fallback to transformer for backwards compatibility
271
274
  if (typeof entry.transform === 'function') {
272
- result = entry.transform(data);
275
+ result = entry.transform(cleanData);
273
276
  } else if (typeof entry.transformer === 'function') {
274
- result = entry.transformer(data);
277
+ result = entry.transformer(cleanData);
275
278
  }
276
279
  } else if (typeof entry === 'function') {
277
280
  // Function directly as transformer
278
- result = entry(data);
281
+ result = entry(cleanData);
279
282
  } else {
280
- result = data;
283
+ result = cleanData;
281
284
  }
282
285
 
283
286
  // Ensure that id is always present
284
- if (result && data && data.id && !result.id) result.id = data.id;
285
- // Fallback: if transformer returns undefined/null, use original data
286
- if (!result && data) result = data;
287
+ if (result && cleanData && cleanData.id && !result.id) result.id = cleanData.id;
288
+ // Fallback: if transformer returns undefined/null, use original clean data
289
+ if (!result && cleanData) result = cleanData;
287
290
  return result;
288
291
  }
289
292
 
293
+ _cleanInternalFields(data) {
294
+ if (!data || typeof data !== 'object') return data;
295
+
296
+ const cleanData = { ...data };
297
+
298
+ // Remove internal fields that start with $ or _
299
+ Object.keys(cleanData).forEach(key => {
300
+ if (key.startsWith('$') || key.startsWith('_')) {
301
+ delete cleanData[key];
302
+ }
303
+ });
304
+
305
+ return cleanData;
306
+ }
307
+
290
308
  _resolveDestResource(resource, data) {
291
309
  const normResource = normalizeResourceName(resource);
292
310
  const entry = this.resourcesMap[normResource];
@@ -345,6 +363,11 @@ class S3dbReplicator extends BaseReplicator {
345
363
  }
346
364
  }
347
365
 
366
+ // Log errors if any occurred during batch processing
367
+ if (errors.length > 0) {
368
+ console.warn(`[S3dbReplicator] Batch replication completed with ${errors.length} error(s) for ${resourceName}:`, errors);
369
+ }
370
+
348
371
  this.emit('batch_replicated', {
349
372
  replicator: this.name,
350
373
  resourceName,
@@ -90,19 +90,37 @@ class SqsReplicator extends BaseReplicator {
90
90
  }
91
91
 
92
92
  _applyTransformer(resource, data) {
93
+ // First, clean internal fields that shouldn't go to SQS
94
+ let cleanData = this._cleanInternalFields(data);
95
+
93
96
  const entry = this.resources[resource];
94
- let result = data;
97
+ let result = cleanData;
95
98
 
96
- if (!entry) return data;
99
+ if (!entry) return cleanData;
97
100
 
98
101
  // Support both transform and transformer (backwards compatibility)
99
102
  if (typeof entry.transform === 'function') {
100
- result = entry.transform(data);
103
+ result = entry.transform(cleanData);
101
104
  } else if (typeof entry.transformer === 'function') {
102
- result = entry.transformer(data);
105
+ result = entry.transformer(cleanData);
103
106
  }
104
107
 
105
- return result || data;
108
+ return result || cleanData;
109
+ }
110
+
111
+ _cleanInternalFields(data) {
112
+ if (!data || typeof data !== 'object') return data;
113
+
114
+ const cleanData = { ...data };
115
+
116
+ // Remove internal fields that start with $ or _
117
+ Object.keys(cleanData).forEach(key => {
118
+ if (key.startsWith('$') || key.startsWith('_')) {
119
+ delete cleanData[key];
120
+ }
121
+ });
122
+
123
+ return cleanData;
106
124
  }
107
125
 
108
126
  /**
@@ -260,6 +278,11 @@ class SqsReplicator extends BaseReplicator {
260
278
  }
261
279
  }
262
280
  }
281
+ // Log errors if any occurred during batch processing
282
+ if (errors.length > 0) {
283
+ console.warn(`[SqsReplicator] Batch replication completed with ${errors.length} error(s) for ${resource}:`, errors);
284
+ }
285
+
263
286
  this.emit('batch_replicated', {
264
287
  replicator: this.name,
265
288
  resource,