s3db.js 12.2.4 → 12.4.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 +117 -0
- package/dist/s3db.cjs.js +1596 -167
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +1499 -73
- package/dist/s3db.es.js.map +1 -1
- package/package.json +2 -2
- package/src/behaviors/body-only.js +15 -5
- package/src/behaviors/body-overflow.js +9 -0
- package/src/behaviors/user-managed.js +8 -1
- package/src/clients/index.js +14 -0
- package/src/clients/memory-client.class.js +883 -0
- package/src/clients/memory-client.md +917 -0
- package/src/clients/memory-storage.class.js +504 -0
- package/src/{client.class.js → clients/s3-client.class.js} +11 -10
- package/src/concerns/typescript-generator.js +12 -2
- package/src/database.class.js +2 -2
- package/src/index.js +2 -1
- package/src/plugins/api/utils/openapi-generator.js +21 -2
- package/src/plugins/replicators/bigquery-replicator.class.js +109 -21
- package/src/plugins/replicators/mysql-replicator.class.js +9 -1
- package/src/plugins/replicators/planetscale-replicator.class.js +9 -1
- package/src/plugins/replicators/postgres-replicator.class.js +9 -1
- package/src/plugins/replicators/schema-sync.helper.js +53 -2
- package/src/plugins/replicators/turso-replicator.class.js +9 -1
- package/src/plugins/tfstate/s3-driver.js +3 -3
- package/src/plugins/vector.plugin.js +3 -3
- package/src/resource.class.js +203 -4
- package/src/schema.class.js +223 -33
|
@@ -27,12 +27,17 @@ import {
|
|
|
27
27
|
* @param {string} schemaSync.onMismatch - Action on schema mismatch: 'error' | 'warn' | 'ignore' (default: 'error')
|
|
28
28
|
* @param {boolean} schemaSync.autoCreateTable - Auto-create table if not exists (default: true)
|
|
29
29
|
* @param {boolean} schemaSync.autoCreateColumns - Auto-add missing columns (default: true, only with strategy: 'alter')
|
|
30
|
+
* @param {string} mutability - Global mutability mode: 'append-only' | 'mutable' | 'immutable' (default: 'append-only')
|
|
31
|
+
* - 'append-only': Updates/deletes become inserts with _operation_type and _operation_timestamp (most performant, no streaming buffer issues)
|
|
32
|
+
* - 'mutable': Traditional UPDATE/DELETE queries with streaming buffer retry logic
|
|
33
|
+
* - 'immutable': Full audit trail with _operation_type, _operation_timestamp, _is_deleted, _version fields
|
|
30
34
|
*
|
|
31
35
|
* @example
|
|
32
36
|
* new BigqueryReplicator({
|
|
33
37
|
* projectId: 'my-gcp-project',
|
|
34
38
|
* datasetId: 'analytics',
|
|
35
39
|
* credentials: JSON.parse(Buffer.from(GOOGLE_CREDENTIALS, 'base64').toString()),
|
|
40
|
+
* mutability: 'append-only', // Global default
|
|
36
41
|
* schemaSync: {
|
|
37
42
|
* enabled: true,
|
|
38
43
|
* strategy: 'alter',
|
|
@@ -41,6 +46,7 @@ import {
|
|
|
41
46
|
* }, {
|
|
42
47
|
* users: {
|
|
43
48
|
* table: 'users_table',
|
|
49
|
+
* mutability: 'immutable', // Override for audit trail
|
|
44
50
|
* transform: (data) => ({ ...data, ip: data.ip || 'unknown' })
|
|
45
51
|
* },
|
|
46
52
|
* orders: 'orders_table'
|
|
@@ -58,6 +64,10 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
58
64
|
this.location = config.location || 'US';
|
|
59
65
|
this.logTable = config.logTable;
|
|
60
66
|
|
|
67
|
+
// Mutability configuration
|
|
68
|
+
this.mutability = config.mutability || 'append-only';
|
|
69
|
+
this._validateMutability(this.mutability);
|
|
70
|
+
|
|
61
71
|
// Schema sync configuration
|
|
62
72
|
this.schemaSync = {
|
|
63
73
|
enabled: config.schemaSync?.enabled || false,
|
|
@@ -69,6 +79,16 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
69
79
|
|
|
70
80
|
// Parse resources configuration
|
|
71
81
|
this.resources = this.parseResourcesConfig(resources);
|
|
82
|
+
|
|
83
|
+
// Version tracking for immutable mode
|
|
84
|
+
this.versionCounters = new Map();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_validateMutability(mutability) {
|
|
88
|
+
const validModes = ['append-only', 'mutable', 'immutable'];
|
|
89
|
+
if (!validModes.includes(mutability)) {
|
|
90
|
+
throw new Error(`Invalid mutability mode: ${mutability}. Must be one of: ${validModes.join(', ')}`);
|
|
91
|
+
}
|
|
72
92
|
}
|
|
73
93
|
|
|
74
94
|
parseResourcesConfig(resources) {
|
|
@@ -80,26 +100,33 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
80
100
|
parsed[resourceName] = [{
|
|
81
101
|
table: config,
|
|
82
102
|
actions: ['insert'],
|
|
83
|
-
transform: null
|
|
103
|
+
transform: null,
|
|
104
|
+
mutability: this.mutability
|
|
84
105
|
}];
|
|
85
106
|
} else if (Array.isArray(config)) {
|
|
86
107
|
// Array form: multiple table mappings
|
|
87
108
|
parsed[resourceName] = config.map(item => {
|
|
88
109
|
if (typeof item === 'string') {
|
|
89
|
-
return { table: item, actions: ['insert'], transform: null };
|
|
110
|
+
return { table: item, actions: ['insert'], transform: null, mutability: this.mutability };
|
|
90
111
|
}
|
|
112
|
+
const itemMutability = item.mutability || this.mutability;
|
|
113
|
+
this._validateMutability(itemMutability);
|
|
91
114
|
return {
|
|
92
115
|
table: item.table,
|
|
93
116
|
actions: item.actions || ['insert'],
|
|
94
|
-
transform: item.transform || null
|
|
117
|
+
transform: item.transform || null,
|
|
118
|
+
mutability: itemMutability
|
|
95
119
|
};
|
|
96
120
|
});
|
|
97
121
|
} else if (typeof config === 'object') {
|
|
98
122
|
// Single object form
|
|
123
|
+
const configMutability = config.mutability || this.mutability;
|
|
124
|
+
this._validateMutability(configMutability);
|
|
99
125
|
parsed[resourceName] = [{
|
|
100
126
|
table: config.table,
|
|
101
127
|
actions: config.actions || ['insert'],
|
|
102
|
-
transform: config.transform || null
|
|
128
|
+
transform: config.transform || null,
|
|
129
|
+
mutability: configMutability
|
|
103
130
|
}];
|
|
104
131
|
}
|
|
105
132
|
}
|
|
@@ -186,13 +213,22 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
186
213
|
continue;
|
|
187
214
|
}
|
|
188
215
|
|
|
189
|
-
const
|
|
216
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
217
|
+
|
|
218
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
219
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
220
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
221
|
+
: [];
|
|
222
|
+
const attributes = Object.fromEntries(
|
|
223
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
224
|
+
);
|
|
190
225
|
|
|
191
226
|
for (const tableConfig of tableConfigs) {
|
|
192
227
|
const tableName = tableConfig.table;
|
|
228
|
+
const mutability = tableConfig.mutability;
|
|
193
229
|
|
|
194
230
|
const [okSync, errSync] = await tryFn(async () => {
|
|
195
|
-
await this.syncTableSchema(tableName, attributes);
|
|
231
|
+
await this.syncTableSchema(tableName, attributes, mutability);
|
|
196
232
|
});
|
|
197
233
|
|
|
198
234
|
if (!okSync) {
|
|
@@ -216,7 +252,7 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
216
252
|
/**
|
|
217
253
|
* Sync a single table schema in BigQuery
|
|
218
254
|
*/
|
|
219
|
-
async syncTableSchema(tableName, attributes) {
|
|
255
|
+
async syncTableSchema(tableName, attributes, mutability = 'append-only') {
|
|
220
256
|
const dataset = this.bigqueryClient.dataset(this.datasetId);
|
|
221
257
|
const table = dataset.table(tableName);
|
|
222
258
|
|
|
@@ -232,11 +268,11 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
232
268
|
throw new Error(`Table ${tableName} does not exist (validate-only mode)`);
|
|
233
269
|
}
|
|
234
270
|
|
|
235
|
-
// Create table with schema
|
|
236
|
-
const schema = generateBigQuerySchema(attributes);
|
|
271
|
+
// Create table with schema (including tracking fields based on mutability)
|
|
272
|
+
const schema = generateBigQuerySchema(attributes, mutability);
|
|
237
273
|
|
|
238
274
|
if (this.config.verbose) {
|
|
239
|
-
console.log(`[BigQueryReplicator] Creating table ${tableName} with schema:`, schema);
|
|
275
|
+
console.log(`[BigQueryReplicator] Creating table ${tableName} with schema (mutability: ${mutability}):`, schema);
|
|
240
276
|
}
|
|
241
277
|
|
|
242
278
|
await dataset.createTable(tableName, { schema });
|
|
@@ -244,7 +280,8 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
244
280
|
this.emit('table_created', {
|
|
245
281
|
replicator: this.name,
|
|
246
282
|
tableName,
|
|
247
|
-
attributes: Object.keys(attributes)
|
|
283
|
+
attributes: Object.keys(attributes),
|
|
284
|
+
mutability
|
|
248
285
|
});
|
|
249
286
|
|
|
250
287
|
return;
|
|
@@ -257,13 +294,14 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
257
294
|
}
|
|
258
295
|
|
|
259
296
|
await table.delete();
|
|
260
|
-
const schema = generateBigQuerySchema(attributes);
|
|
297
|
+
const schema = generateBigQuerySchema(attributes, mutability);
|
|
261
298
|
await dataset.createTable(tableName, { schema });
|
|
262
299
|
|
|
263
300
|
this.emit('table_recreated', {
|
|
264
301
|
replicator: this.name,
|
|
265
302
|
tableName,
|
|
266
|
-
attributes: Object.keys(attributes)
|
|
303
|
+
attributes: Object.keys(attributes),
|
|
304
|
+
mutability
|
|
267
305
|
});
|
|
268
306
|
|
|
269
307
|
return;
|
|
@@ -271,7 +309,7 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
271
309
|
|
|
272
310
|
if (this.schemaSync.strategy === 'alter' && this.schemaSync.autoCreateColumns) {
|
|
273
311
|
const existingSchema = await getBigQueryTableSchema(this.bigqueryClient, this.datasetId, tableName);
|
|
274
|
-
const newFields = generateBigQuerySchemaUpdate(attributes, existingSchema);
|
|
312
|
+
const newFields = generateBigQuerySchemaUpdate(attributes, existingSchema, mutability);
|
|
275
313
|
|
|
276
314
|
if (newFields.length > 0) {
|
|
277
315
|
if (this.config.verbose) {
|
|
@@ -298,7 +336,7 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
298
336
|
|
|
299
337
|
if (this.schemaSync.strategy === 'validate-only') {
|
|
300
338
|
const existingSchema = await getBigQueryTableSchema(this.bigqueryClient, this.datasetId, tableName);
|
|
301
|
-
const newFields = generateBigQuerySchemaUpdate(attributes, existingSchema);
|
|
339
|
+
const newFields = generateBigQuerySchemaUpdate(attributes, existingSchema, mutability);
|
|
302
340
|
|
|
303
341
|
if (newFields.length > 0) {
|
|
304
342
|
throw new Error(`Table ${tableName} schema mismatch. Missing columns: ${newFields.length}`);
|
|
@@ -325,7 +363,8 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
325
363
|
.filter(tableConfig => tableConfig.actions.includes(operation))
|
|
326
364
|
.map(tableConfig => ({
|
|
327
365
|
table: tableConfig.table,
|
|
328
|
-
transform: tableConfig.transform
|
|
366
|
+
transform: tableConfig.transform,
|
|
367
|
+
mutability: tableConfig.mutability
|
|
329
368
|
}));
|
|
330
369
|
}
|
|
331
370
|
|
|
@@ -354,6 +393,39 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
354
393
|
return cleanData;
|
|
355
394
|
}
|
|
356
395
|
|
|
396
|
+
/**
|
|
397
|
+
* Add tracking fields for append-only and immutable modes
|
|
398
|
+
* @private
|
|
399
|
+
*/
|
|
400
|
+
_addTrackingFields(data, operation, mutability, id) {
|
|
401
|
+
const tracked = { ...data };
|
|
402
|
+
|
|
403
|
+
// Add operation tracking for append-only and immutable modes
|
|
404
|
+
if (mutability === 'append-only' || mutability === 'immutable') {
|
|
405
|
+
tracked._operation_type = operation;
|
|
406
|
+
tracked._operation_timestamp = new Date().toISOString();
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Add additional fields for immutable mode
|
|
410
|
+
if (mutability === 'immutable') {
|
|
411
|
+
tracked._is_deleted = operation === 'delete';
|
|
412
|
+
tracked._version = this._getNextVersion(id);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return tracked;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get next version number for immutable mode
|
|
420
|
+
* @private
|
|
421
|
+
*/
|
|
422
|
+
_getNextVersion(id) {
|
|
423
|
+
const current = this.versionCounters.get(id) || 0;
|
|
424
|
+
const next = current + 1;
|
|
425
|
+
this.versionCounters.set(id, next);
|
|
426
|
+
return next;
|
|
427
|
+
}
|
|
428
|
+
|
|
357
429
|
async replicate(resourceName, operation, data, id, beforeData = null) {
|
|
358
430
|
|
|
359
431
|
if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
|
|
@@ -379,10 +451,23 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
379
451
|
for (const tableConfig of tableConfigs) {
|
|
380
452
|
const [okTable, errTable] = await tryFn(async () => {
|
|
381
453
|
const table = dataset.table(tableConfig.table);
|
|
454
|
+
const mutability = tableConfig.mutability;
|
|
382
455
|
let job;
|
|
383
456
|
|
|
384
|
-
|
|
385
|
-
|
|
457
|
+
// For append-only and immutable modes, convert update/delete to insert
|
|
458
|
+
const shouldConvertToInsert =
|
|
459
|
+
(mutability === 'append-only' || mutability === 'immutable') &&
|
|
460
|
+
(operation === 'update' || operation === 'delete');
|
|
461
|
+
|
|
462
|
+
if (operation === 'insert' || shouldConvertToInsert) {
|
|
463
|
+
// Apply transform first
|
|
464
|
+
let transformedData = this.applyTransform(data, tableConfig.transform);
|
|
465
|
+
|
|
466
|
+
// Add tracking fields if needed
|
|
467
|
+
if (shouldConvertToInsert) {
|
|
468
|
+
transformedData = this._addTrackingFields(transformedData, operation, mutability, id);
|
|
469
|
+
}
|
|
470
|
+
|
|
386
471
|
try {
|
|
387
472
|
job = await table.insert([transformedData]);
|
|
388
473
|
} catch (error) {
|
|
@@ -395,7 +480,8 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
395
480
|
}
|
|
396
481
|
throw error;
|
|
397
482
|
}
|
|
398
|
-
} else if (operation === 'update') {
|
|
483
|
+
} else if (operation === 'update' && mutability === 'mutable') {
|
|
484
|
+
// Traditional UPDATE for mutable mode
|
|
399
485
|
const transformedData = this.applyTransform(data, tableConfig.transform);
|
|
400
486
|
const keys = Object.keys(transformedData).filter(k => k !== 'id');
|
|
401
487
|
const setClause = keys.map(k => `${k} = @${k}`).join(', ');
|
|
@@ -447,7 +533,8 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
447
533
|
}
|
|
448
534
|
|
|
449
535
|
if (!job) throw lastError;
|
|
450
|
-
} else if (operation === 'delete') {
|
|
536
|
+
} else if (operation === 'delete' && mutability === 'mutable') {
|
|
537
|
+
// Traditional DELETE for mutable mode
|
|
451
538
|
const query = `DELETE FROM \`${this.projectId}.${this.datasetId}.${tableConfig.table}\` WHERE id = @id`;
|
|
452
539
|
try {
|
|
453
540
|
const [deleteJob] = await this.bigqueryClient.createQueryJob({
|
|
@@ -606,7 +693,8 @@ class BigqueryReplicator extends BaseReplicator {
|
|
|
606
693
|
datasetId: this.datasetId,
|
|
607
694
|
resources: this.resources,
|
|
608
695
|
logTable: this.logTable,
|
|
609
|
-
schemaSync: this.schemaSync
|
|
696
|
+
schemaSync: this.schemaSync,
|
|
697
|
+
mutability: this.mutability
|
|
610
698
|
};
|
|
611
699
|
}
|
|
612
700
|
}
|
|
@@ -221,7 +221,15 @@ class MySQLReplicator extends BaseReplicator {
|
|
|
221
221
|
continue;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
const
|
|
224
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
225
|
+
|
|
226
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
227
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
228
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
229
|
+
: [];
|
|
230
|
+
const attributes = Object.fromEntries(
|
|
231
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
232
|
+
);
|
|
225
233
|
|
|
226
234
|
for (const tableConfig of tableConfigs) {
|
|
227
235
|
const tableName = tableConfig.table;
|
|
@@ -183,7 +183,15 @@ class PlanetScaleReplicator extends BaseReplicator {
|
|
|
183
183
|
continue;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
const
|
|
186
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
187
|
+
|
|
188
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
189
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
190
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
191
|
+
: [];
|
|
192
|
+
const attributes = Object.fromEntries(
|
|
193
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
194
|
+
);
|
|
187
195
|
|
|
188
196
|
for (const tableConfig of tableConfigs) {
|
|
189
197
|
const tableName = tableConfig.table;
|
|
@@ -225,7 +225,15 @@ class PostgresReplicator extends BaseReplicator {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// Get resource attributes from current version
|
|
228
|
-
const
|
|
228
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
229
|
+
|
|
230
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
231
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
232
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
233
|
+
: [];
|
|
234
|
+
const attributes = Object.fromEntries(
|
|
235
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
236
|
+
);
|
|
229
237
|
|
|
230
238
|
// Sync each table configured for this resource
|
|
231
239
|
for (const tableConfig of tableConfigs) {
|
|
@@ -7,6 +7,25 @@
|
|
|
7
7
|
|
|
8
8
|
import tryFn from "#src/concerns/try-fn.js";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Filter out plugin attributes from attributes object
|
|
12
|
+
* Plugin attributes are internal implementation details and should not be replicated
|
|
13
|
+
* @param {Object} attributes - All attributes including plugin attributes
|
|
14
|
+
* @param {Object} resource - Resource instance with schema._pluginAttributes
|
|
15
|
+
* @returns {Object} Filtered attributes (user attributes only)
|
|
16
|
+
*/
|
|
17
|
+
function filterPluginAttributes(attributes, resource) {
|
|
18
|
+
if (!resource?.schema?._pluginAttributes) {
|
|
19
|
+
return attributes;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pluginAttrNames = Object.values(resource.schema._pluginAttributes).flat();
|
|
23
|
+
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
Object.entries(attributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
/**
|
|
11
30
|
* Parse s3db field type notation (e.g., 'string|required|maxlength:50')
|
|
12
31
|
*/
|
|
@@ -394,7 +413,7 @@ export function generateMySQLAlterTable(tableName, attributes, existingSchema) {
|
|
|
394
413
|
/**
|
|
395
414
|
* Generate BigQuery table schema from S3DB resource schema
|
|
396
415
|
*/
|
|
397
|
-
export function generateBigQuerySchema(attributes) {
|
|
416
|
+
export function generateBigQuerySchema(attributes, mutability = 'append-only') {
|
|
398
417
|
const fields = [];
|
|
399
418
|
|
|
400
419
|
// Always add id field
|
|
@@ -427,6 +446,18 @@ export function generateBigQuerySchema(attributes) {
|
|
|
427
446
|
fields.push({ name: 'updated_at', type: 'TIMESTAMP', mode: 'NULLABLE' });
|
|
428
447
|
}
|
|
429
448
|
|
|
449
|
+
// Add tracking fields for append-only and immutable modes
|
|
450
|
+
if (mutability === 'append-only' || mutability === 'immutable') {
|
|
451
|
+
fields.push({ name: '_operation_type', type: 'STRING', mode: 'NULLABLE' });
|
|
452
|
+
fields.push({ name: '_operation_timestamp', type: 'TIMESTAMP', mode: 'NULLABLE' });
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Add additional fields for immutable mode
|
|
456
|
+
if (mutability === 'immutable') {
|
|
457
|
+
fields.push({ name: '_is_deleted', type: 'BOOL', mode: 'NULLABLE' });
|
|
458
|
+
fields.push({ name: '_version', type: 'INT64', mode: 'NULLABLE' });
|
|
459
|
+
}
|
|
460
|
+
|
|
430
461
|
return fields;
|
|
431
462
|
}
|
|
432
463
|
|
|
@@ -459,7 +490,7 @@ export async function getBigQueryTableSchema(bigqueryClient, datasetId, tableId)
|
|
|
459
490
|
/**
|
|
460
491
|
* Generate BigQuery schema update (add missing fields)
|
|
461
492
|
*/
|
|
462
|
-
export function generateBigQuerySchemaUpdate(attributes, existingSchema) {
|
|
493
|
+
export function generateBigQuerySchemaUpdate(attributes, existingSchema, mutability = 'append-only') {
|
|
463
494
|
const newFields = [];
|
|
464
495
|
|
|
465
496
|
for (const [fieldName, fieldConfig] of Object.entries(attributes)) {
|
|
@@ -477,6 +508,26 @@ export function generateBigQuerySchemaUpdate(attributes, existingSchema) {
|
|
|
477
508
|
});
|
|
478
509
|
}
|
|
479
510
|
|
|
511
|
+
// Add tracking fields for append-only and immutable modes if they don't exist
|
|
512
|
+
if (mutability === 'append-only' || mutability === 'immutable') {
|
|
513
|
+
if (!existingSchema['_operation_type']) {
|
|
514
|
+
newFields.push({ name: '_operation_type', type: 'STRING', mode: 'NULLABLE' });
|
|
515
|
+
}
|
|
516
|
+
if (!existingSchema['_operation_timestamp']) {
|
|
517
|
+
newFields.push({ name: '_operation_timestamp', type: 'TIMESTAMP', mode: 'NULLABLE' });
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Add additional fields for immutable mode if they don't exist
|
|
522
|
+
if (mutability === 'immutable') {
|
|
523
|
+
if (!existingSchema['_is_deleted']) {
|
|
524
|
+
newFields.push({ name: '_is_deleted', type: 'BOOL', mode: 'NULLABLE' });
|
|
525
|
+
}
|
|
526
|
+
if (!existingSchema['_version']) {
|
|
527
|
+
newFields.push({ name: '_version', type: 'INT64', mode: 'NULLABLE' });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
480
531
|
return newFields;
|
|
481
532
|
}
|
|
482
533
|
|
|
@@ -177,7 +177,15 @@ class TursoReplicator extends BaseReplicator {
|
|
|
177
177
|
continue;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
const
|
|
180
|
+
const allAttributes = resource.config.versions[resource.config.currentVersion]?.attributes || {};
|
|
181
|
+
|
|
182
|
+
// Filter out plugin attributes - they are internal and should not be replicated
|
|
183
|
+
const pluginAttrNames = resource.schema?._pluginAttributes
|
|
184
|
+
? Object.values(resource.schema._pluginAttributes).flat()
|
|
185
|
+
: [];
|
|
186
|
+
const attributes = Object.fromEntries(
|
|
187
|
+
Object.entries(allAttributes).filter(([name]) => !pluginAttrNames.includes(name))
|
|
188
|
+
);
|
|
181
189
|
|
|
182
190
|
for (const tableConfig of tableConfigs) {
|
|
183
191
|
const tableName = tableConfig.table;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Reads Terraform/OpenTofu state files from S3 buckets
|
|
5
5
|
*/
|
|
6
6
|
import { TfStateDriver } from './base-driver.js';
|
|
7
|
-
import {
|
|
7
|
+
import { S3Client } from '../../clients/s3-client.class.js';
|
|
8
8
|
import tryFn from '../../concerns/try-fn.js';
|
|
9
9
|
|
|
10
10
|
export class S3TfStateDriver extends TfStateDriver {
|
|
@@ -71,8 +71,8 @@ export class S3TfStateDriver extends TfStateDriver {
|
|
|
71
71
|
async initialize() {
|
|
72
72
|
const { bucket, credentials, region } = this.connectionConfig;
|
|
73
73
|
|
|
74
|
-
// Create S3 client using s3db's
|
|
75
|
-
this.client = new
|
|
74
|
+
// Create S3 client using s3db's S3Client class
|
|
75
|
+
this.client = new S3Client({
|
|
76
76
|
bucketName: bucket,
|
|
77
77
|
credentials,
|
|
78
78
|
region
|
|
@@ -183,13 +183,13 @@ export class VectorPlugin extends Plugin {
|
|
|
183
183
|
}
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
// Add tracking field to schema if not present
|
|
186
|
+
// Add tracking field to schema if not present using plugin API
|
|
187
187
|
if (!resource.schema.attributes[trackingFieldName]) {
|
|
188
|
-
resource.
|
|
188
|
+
resource.addPluginAttribute(trackingFieldName, {
|
|
189
189
|
type: 'boolean',
|
|
190
190
|
optional: true,
|
|
191
191
|
default: false
|
|
192
|
-
};
|
|
192
|
+
}, 'VectorPlugin');
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// Emit event
|