s3db.js 7.1.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/PLUGINS.md +200 -10
- package/dist/s3db.cjs.js +62 -31
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +14 -15
- package/dist/s3db.es.js +62 -31
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +62 -31
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +5 -1
- package/src/plugins/replicators/bigquery-replicator.class.js +102 -68
- package/src/plugins/replicators/postgres-replicator.class.js +19 -109
- package/src/plugins/replicators/s3db-replicator.class.js +21 -49
- package/src/plugins/replicators/sqs-replicator.class.js +17 -116
- package/src/s3db.d.ts +14 -15
package/PLUGINS.md
CHANGED
|
@@ -1608,18 +1608,147 @@ await users.insert({ name: 'John', email: 'john@example.com' });
|
|
|
1608
1608
|
|
|
1609
1609
|
#### S3DB Replicator
|
|
1610
1610
|
|
|
1611
|
-
Replicate to another S3DB instance:
|
|
1611
|
+
Replicate to another S3DB instance with flexible resource mapping and transformation capabilities:
|
|
1612
1612
|
|
|
1613
1613
|
```javascript
|
|
1614
1614
|
{
|
|
1615
1615
|
driver: 's3db',
|
|
1616
|
-
resources: ['users', 'products'],
|
|
1617
1616
|
config: {
|
|
1618
1617
|
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
1618
|
+
},
|
|
1619
|
+
resources: {
|
|
1620
|
+
// Simple resource mapping (replicate to same name)
|
|
1621
|
+
users: 'users',
|
|
1622
|
+
|
|
1623
|
+
// Map source → destination resource name
|
|
1624
|
+
products: 'backup_products',
|
|
1625
|
+
|
|
1626
|
+
// Advanced mapping with transformer
|
|
1627
|
+
orders: {
|
|
1628
|
+
resource: 'order_backup',
|
|
1629
|
+
transformer: (data) => ({
|
|
1630
|
+
...data,
|
|
1631
|
+
backup_timestamp: new Date().toISOString(),
|
|
1632
|
+
original_source: 'production'
|
|
1633
|
+
})
|
|
1634
|
+
},
|
|
1635
|
+
|
|
1636
|
+
// Multi-destination replication
|
|
1637
|
+
analytics: [
|
|
1638
|
+
'analytics_backup',
|
|
1639
|
+
{
|
|
1640
|
+
resource: 'analytics_processed',
|
|
1641
|
+
transformer: (data) => ({
|
|
1642
|
+
...data,
|
|
1643
|
+
processed_date: data.createdAt?.split('T')[0],
|
|
1644
|
+
data_source: 'analytics'
|
|
1645
|
+
})
|
|
1646
|
+
}
|
|
1647
|
+
]
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
**Resource Configuration Syntaxes:**
|
|
1653
|
+
|
|
1654
|
+
The S3DB replicator supports highly flexible resource mapping configurations:
|
|
1655
|
+
|
|
1656
|
+
**1. Array of resource names** (replicate to same name):
|
|
1657
|
+
```javascript
|
|
1658
|
+
resources: ['users', 'products', 'orders']
|
|
1659
|
+
// Replicates each resource to itself in the destination database
|
|
1660
|
+
```
|
|
1661
|
+
|
|
1662
|
+
**2. Object mapping** (source → destination):
|
|
1663
|
+
```javascript
|
|
1664
|
+
resources: {
|
|
1665
|
+
users: 'people', // users → people
|
|
1666
|
+
products: 'items', // products → items
|
|
1667
|
+
orders: 'order_history' // orders → order_history
|
|
1668
|
+
}
|
|
1669
|
+
```
|
|
1670
|
+
|
|
1671
|
+
**3. Array with transformer** (resource + transformation):
|
|
1672
|
+
```javascript
|
|
1673
|
+
resources: {
|
|
1674
|
+
users: ['people', (data) => ({ ...data, fullName: `${data.firstName} ${data.lastName}` })]
|
|
1675
|
+
// Replicates 'users' to 'people' with transformation
|
|
1676
|
+
}
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
**4. Object with resource and transformer**:
|
|
1680
|
+
```javascript
|
|
1681
|
+
resources: {
|
|
1682
|
+
users: {
|
|
1683
|
+
resource: 'people',
|
|
1684
|
+
transformer: (data) => ({
|
|
1685
|
+
...data,
|
|
1686
|
+
fullName: `${data.firstName} ${data.lastName}`,
|
|
1687
|
+
migrated_at: new Date().toISOString()
|
|
1688
|
+
})
|
|
1619
1689
|
}
|
|
1620
1690
|
}
|
|
1621
1691
|
```
|
|
1622
1692
|
|
|
1693
|
+
**5. Multi-destination arrays**:
|
|
1694
|
+
```javascript
|
|
1695
|
+
resources: {
|
|
1696
|
+
users: [
|
|
1697
|
+
'people', // Simple copy
|
|
1698
|
+
{
|
|
1699
|
+
resource: 'user_analytics',
|
|
1700
|
+
transformer: (data) => ({
|
|
1701
|
+
id: data.id,
|
|
1702
|
+
signup_date: data.createdAt,
|
|
1703
|
+
user_type: data.role || 'standard'
|
|
1704
|
+
})
|
|
1705
|
+
}
|
|
1706
|
+
]
|
|
1707
|
+
}
|
|
1708
|
+
```
|
|
1709
|
+
|
|
1710
|
+
**6. Function-only transformation** (transform to same resource):
|
|
1711
|
+
```javascript
|
|
1712
|
+
resources: {
|
|
1713
|
+
users: (data) => ({
|
|
1714
|
+
...data,
|
|
1715
|
+
processed: true,
|
|
1716
|
+
backup_date: new Date().toISOString()
|
|
1717
|
+
})
|
|
1718
|
+
}
|
|
1719
|
+
```
|
|
1720
|
+
|
|
1721
|
+
**Mixed Configuration Example:**
|
|
1722
|
+
```javascript
|
|
1723
|
+
resources: {
|
|
1724
|
+
users: [
|
|
1725
|
+
'people', // Simple copy
|
|
1726
|
+
{
|
|
1727
|
+
resource: 'user_profiles',
|
|
1728
|
+
transformer: (data) => ({
|
|
1729
|
+
...data,
|
|
1730
|
+
profile_complete: !!(data.name && data.email)
|
|
1731
|
+
})
|
|
1732
|
+
}
|
|
1733
|
+
],
|
|
1734
|
+
orders: 'order_backup', // Rename only
|
|
1735
|
+
products: { resource: 'product_catalog' }, // Object form
|
|
1736
|
+
analytics: (data) => ({ ...data, processed: true }) // Transform only
|
|
1737
|
+
}
|
|
1738
|
+
```
|
|
1739
|
+
|
|
1740
|
+
**Configuration Options:**
|
|
1741
|
+
- `connectionString`: S3DB connection string for destination database (required)
|
|
1742
|
+
- `client`: Pre-configured S3DB client instance (alternative to connectionString)
|
|
1743
|
+
- `resources`: Resource mapping configuration (see syntaxes above)
|
|
1744
|
+
|
|
1745
|
+
**Transformer Features:**
|
|
1746
|
+
- **Data Transformation**: Apply custom logic before replication
|
|
1747
|
+
- **Field Mapping**: Rename, combine, or derive new fields
|
|
1748
|
+
- **Data Enrichment**: Add metadata, timestamps, or computed values
|
|
1749
|
+
- **Conditional Logic**: Apply transformations based on data content
|
|
1750
|
+
- **Multi-destination**: Send different transformed versions to multiple targets
|
|
1751
|
+
|
|
1623
1752
|
#### SQS Replicator
|
|
1624
1753
|
|
|
1625
1754
|
Send changes to AWS SQS queues:
|
|
@@ -1639,23 +1768,84 @@ Send changes to AWS SQS queues:
|
|
|
1639
1768
|
|
|
1640
1769
|
#### BigQuery Replicator
|
|
1641
1770
|
|
|
1642
|
-
Replicate to Google BigQuery:
|
|
1771
|
+
Replicate to Google BigQuery with advanced data transformation capabilities:
|
|
1643
1772
|
|
|
1644
1773
|
```javascript
|
|
1645
1774
|
{
|
|
1646
1775
|
driver: 'bigquery',
|
|
1647
|
-
resources: {
|
|
1648
|
-
users: [{ actions: ['insert', 'update'], table: 'users_table' }],
|
|
1649
|
-
orders: 'orders_table'
|
|
1650
|
-
},
|
|
1651
1776
|
config: {
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1777
|
+
location: 'US',
|
|
1778
|
+
projectId: 'my-gcp-project',
|
|
1779
|
+
datasetId: 'analytics',
|
|
1780
|
+
credentials: JSON.parse(Buffer.from(GOOGLE_CREDENTIALS, 'base64').toString()),
|
|
1781
|
+
logTable: 'replication_log' // Optional: table for operation logging
|
|
1782
|
+
},
|
|
1783
|
+
resources: {
|
|
1784
|
+
// Simple table mapping
|
|
1785
|
+
clicks: 'mrt-shortner__clicks',
|
|
1786
|
+
fingerprints: 'mrt-shortner__fingerprints',
|
|
1787
|
+
scans: 'mrt-shortner__scans',
|
|
1788
|
+
sessions: 'mrt-shortner__sessions',
|
|
1789
|
+
shares: 'mrt-shortner__shares',
|
|
1790
|
+
urls: 'mrt-shortner__urls',
|
|
1791
|
+
views: 'mrt-shortner__views',
|
|
1792
|
+
|
|
1793
|
+
// Advanced configuration with transform functions
|
|
1794
|
+
users: {
|
|
1795
|
+
table: 'mrt-shortner__users',
|
|
1796
|
+
actions: ['insert', 'update'],
|
|
1797
|
+
transform: (data) => {
|
|
1798
|
+
return {
|
|
1799
|
+
...data,
|
|
1800
|
+
ip: data.ip || 'unknown',
|
|
1801
|
+
userIp: data.userIp || 'unknown',
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
},
|
|
1805
|
+
|
|
1806
|
+
// Multiple destinations for a single resource
|
|
1807
|
+
orders: [
|
|
1808
|
+
{ actions: ['insert'], table: 'fact_orders' },
|
|
1809
|
+
{
|
|
1810
|
+
actions: ['insert'],
|
|
1811
|
+
table: 'daily_revenue',
|
|
1812
|
+
transform: (data) => ({
|
|
1813
|
+
date: data.createdAt?.split('T')[0],
|
|
1814
|
+
revenue: data.amount,
|
|
1815
|
+
customer_id: data.userId,
|
|
1816
|
+
order_count: 1
|
|
1817
|
+
})
|
|
1818
|
+
}
|
|
1819
|
+
]
|
|
1655
1820
|
}
|
|
1656
1821
|
}
|
|
1657
1822
|
```
|
|
1658
1823
|
|
|
1824
|
+
**Transform Function Features:**
|
|
1825
|
+
- **Data Transformation**: Apply custom logic before sending to BigQuery
|
|
1826
|
+
- **Field Mapping**: Rename, combine, or derive new fields
|
|
1827
|
+
- **Data Enrichment**: Add computed fields, defaults, or metadata
|
|
1828
|
+
- **Format Conversion**: Convert data types or formats for BigQuery compatibility
|
|
1829
|
+
- **Multiple Destinations**: Send transformed data to different tables
|
|
1830
|
+
|
|
1831
|
+
**Configuration Options:**
|
|
1832
|
+
- `projectId`: Google Cloud project ID (required)
|
|
1833
|
+
- `datasetId`: BigQuery dataset ID (required)
|
|
1834
|
+
- `credentials`: Service account credentials object (optional, uses default if omitted)
|
|
1835
|
+
- `location`: BigQuery dataset location/region (default: 'US')
|
|
1836
|
+
- `logTable`: Table name for operation logging (optional)
|
|
1837
|
+
|
|
1838
|
+
**Resource Configuration:**
|
|
1839
|
+
- **String**: Simple table mapping (e.g., `'table_name'`)
|
|
1840
|
+
- **Object**: Advanced configuration with actions and transforms
|
|
1841
|
+
- **Array**: Multiple destination tables with different configurations
|
|
1842
|
+
|
|
1843
|
+
**Automatic Features:**
|
|
1844
|
+
- **Retry Logic**: Handles BigQuery streaming buffer limitations with 30-second retry delays
|
|
1845
|
+
- **Error Handling**: Graceful handling of schema mismatches and quota limits
|
|
1846
|
+
- **Operation Logging**: Optional audit trail of all replication operations
|
|
1847
|
+
- **Schema Compatibility**: Automatic handling of missing fields
|
|
1848
|
+
|
|
1659
1849
|
#### PostgreSQL Replicator
|
|
1660
1850
|
|
|
1661
1851
|
Replicate to PostgreSQL database:
|
package/dist/s3db.cjs.js
CHANGED
|
@@ -8006,22 +8006,25 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
8006
8006
|
if (typeof config === "string") {
|
|
8007
8007
|
parsed[resourceName] = [{
|
|
8008
8008
|
table: config,
|
|
8009
|
-
actions: ["insert"]
|
|
8009
|
+
actions: ["insert"],
|
|
8010
|
+
transform: null
|
|
8010
8011
|
}];
|
|
8011
8012
|
} else if (Array.isArray(config)) {
|
|
8012
8013
|
parsed[resourceName] = config.map((item) => {
|
|
8013
8014
|
if (typeof item === "string") {
|
|
8014
|
-
return { table: item, actions: ["insert"] };
|
|
8015
|
+
return { table: item, actions: ["insert"], transform: null };
|
|
8015
8016
|
}
|
|
8016
8017
|
return {
|
|
8017
8018
|
table: item.table,
|
|
8018
|
-
actions: item.actions || ["insert"]
|
|
8019
|
+
actions: item.actions || ["insert"],
|
|
8020
|
+
transform: item.transform || null
|
|
8019
8021
|
};
|
|
8020
8022
|
});
|
|
8021
8023
|
} else if (typeof config === "object") {
|
|
8022
8024
|
parsed[resourceName] = [{
|
|
8023
8025
|
table: config.table,
|
|
8024
|
-
actions: config.actions || ["insert"]
|
|
8026
|
+
actions: config.actions || ["insert"],
|
|
8027
|
+
transform: config.transform || null
|
|
8025
8028
|
}];
|
|
8026
8029
|
}
|
|
8027
8030
|
}
|
|
@@ -8045,6 +8048,9 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
8045
8048
|
if (invalidActions.length > 0) {
|
|
8046
8049
|
errors.push(`Invalid actions for resource '${resourceName}': ${invalidActions.join(", ")}. Valid actions: ${validActions.join(", ")}`);
|
|
8047
8050
|
}
|
|
8051
|
+
if (tableConfig.transform && typeof tableConfig.transform !== "function") {
|
|
8052
|
+
errors.push(`Transform must be a function for resource '${resourceName}'`);
|
|
8053
|
+
}
|
|
8048
8054
|
}
|
|
8049
8055
|
}
|
|
8050
8056
|
return { isValid: errors.length === 0, errors };
|
|
@@ -8080,7 +8086,16 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
8080
8086
|
}
|
|
8081
8087
|
getTablesForResource(resourceName, operation) {
|
|
8082
8088
|
if (!this.resources[resourceName]) return [];
|
|
8083
|
-
return this.resources[resourceName].filter((tableConfig) => tableConfig.actions.includes(operation)).map((tableConfig) =>
|
|
8089
|
+
return this.resources[resourceName].filter((tableConfig) => tableConfig.actions.includes(operation)).map((tableConfig) => ({
|
|
8090
|
+
table: tableConfig.table,
|
|
8091
|
+
transform: tableConfig.transform
|
|
8092
|
+
}));
|
|
8093
|
+
}
|
|
8094
|
+
applyTransform(data, transformFn) {
|
|
8095
|
+
if (!transformFn) return data;
|
|
8096
|
+
let transformedData = JSON.parse(JSON.stringify(data));
|
|
8097
|
+
if (transformedData._length) delete transformedData._length;
|
|
8098
|
+
return transformFn(transformedData);
|
|
8084
8099
|
}
|
|
8085
8100
|
async replicate(resourceName, operation, data, id, beforeData = null) {
|
|
8086
8101
|
if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
|
|
@@ -8089,40 +8104,56 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
8089
8104
|
if (!this.shouldReplicateAction(resourceName, operation)) {
|
|
8090
8105
|
return { skipped: true, reason: "action_not_included" };
|
|
8091
8106
|
}
|
|
8092
|
-
const
|
|
8093
|
-
if (
|
|
8107
|
+
const tableConfigs = this.getTablesForResource(resourceName, operation);
|
|
8108
|
+
if (tableConfigs.length === 0) {
|
|
8094
8109
|
return { skipped: true, reason: "no_tables_for_action" };
|
|
8095
8110
|
}
|
|
8096
8111
|
const results = [];
|
|
8097
8112
|
const errors = [];
|
|
8098
8113
|
const [ok, err, result] = await try_fn_default(async () => {
|
|
8099
8114
|
const dataset = this.bigqueryClient.dataset(this.datasetId);
|
|
8100
|
-
for (const
|
|
8115
|
+
for (const tableConfig of tableConfigs) {
|
|
8101
8116
|
const [okTable, errTable] = await try_fn_default(async () => {
|
|
8102
|
-
const table = dataset.table(
|
|
8117
|
+
const table = dataset.table(tableConfig.table);
|
|
8103
8118
|
let job;
|
|
8104
8119
|
if (operation === "insert") {
|
|
8105
|
-
const
|
|
8106
|
-
job = await table.insert([
|
|
8120
|
+
const transformedData = this.applyTransform(data, tableConfig.transform);
|
|
8121
|
+
job = await table.insert([transformedData]);
|
|
8107
8122
|
} else if (operation === "update") {
|
|
8108
|
-
const
|
|
8109
|
-
const
|
|
8110
|
-
const
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
|
|
8118
|
-
|
|
8119
|
-
|
|
8120
|
-
|
|
8123
|
+
const transformedData = this.applyTransform(data, tableConfig.transform);
|
|
8124
|
+
const keys = Object.keys(transformedData).filter((k) => k !== "id");
|
|
8125
|
+
const setClause = keys.map((k) => `${k} = @${k}`).join(", ");
|
|
8126
|
+
const params = { id, ...transformedData };
|
|
8127
|
+
const query = `UPDATE \`${this.projectId}.${this.datasetId}.${tableConfig.table}\` SET ${setClause} WHERE id = @id`;
|
|
8128
|
+
const maxRetries = 2;
|
|
8129
|
+
let lastError = null;
|
|
8130
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
8131
|
+
try {
|
|
8132
|
+
const [updateJob] = await this.bigqueryClient.createQueryJob({
|
|
8133
|
+
query,
|
|
8134
|
+
params,
|
|
8135
|
+
location: this.location
|
|
8136
|
+
});
|
|
8137
|
+
await updateJob.getQueryResults();
|
|
8138
|
+
job = [updateJob];
|
|
8139
|
+
break;
|
|
8140
|
+
} catch (error) {
|
|
8141
|
+
lastError = error;
|
|
8142
|
+
if (error?.message?.includes("streaming buffer") && attempt < maxRetries) {
|
|
8143
|
+
const delaySeconds = 30;
|
|
8144
|
+
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1e3));
|
|
8145
|
+
continue;
|
|
8146
|
+
}
|
|
8147
|
+
throw error;
|
|
8148
|
+
}
|
|
8149
|
+
}
|
|
8150
|
+
if (!job) throw lastError;
|
|
8121
8151
|
} else if (operation === "delete") {
|
|
8122
|
-
const query = `DELETE FROM \`${this.projectId}.${this.datasetId}.${
|
|
8152
|
+
const query = `DELETE FROM \`${this.projectId}.${this.datasetId}.${tableConfig.table}\` WHERE id = @id`;
|
|
8123
8153
|
const [deleteJob] = await this.bigqueryClient.createQueryJob({
|
|
8124
8154
|
query,
|
|
8125
|
-
params: { id }
|
|
8155
|
+
params: { id },
|
|
8156
|
+
location: this.location
|
|
8126
8157
|
});
|
|
8127
8158
|
await deleteJob.getQueryResults();
|
|
8128
8159
|
job = [deleteJob];
|
|
@@ -8130,14 +8161,14 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
8130
8161
|
throw new Error(`Unsupported operation: ${operation}`);
|
|
8131
8162
|
}
|
|
8132
8163
|
results.push({
|
|
8133
|
-
table:
|
|
8164
|
+
table: tableConfig.table,
|
|
8134
8165
|
success: true,
|
|
8135
8166
|
jobId: job[0]?.id
|
|
8136
8167
|
});
|
|
8137
8168
|
});
|
|
8138
8169
|
if (!okTable) {
|
|
8139
8170
|
errors.push({
|
|
8140
|
-
table:
|
|
8171
|
+
table: tableConfig.table,
|
|
8141
8172
|
error: errTable.message
|
|
8142
8173
|
});
|
|
8143
8174
|
}
|
|
@@ -8163,7 +8194,7 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
8163
8194
|
resourceName,
|
|
8164
8195
|
operation,
|
|
8165
8196
|
id,
|
|
8166
|
-
tables,
|
|
8197
|
+
tables: tableConfigs.map((t) => t.table),
|
|
8167
8198
|
results,
|
|
8168
8199
|
errors,
|
|
8169
8200
|
success
|
|
@@ -8172,7 +8203,7 @@ class BigqueryReplicator extends base_replicator_class_default {
|
|
|
8172
8203
|
success,
|
|
8173
8204
|
results,
|
|
8174
8205
|
errors,
|
|
8175
|
-
tables
|
|
8206
|
+
tables: tableConfigs.map((t) => t.table)
|
|
8176
8207
|
};
|
|
8177
8208
|
});
|
|
8178
8209
|
if (ok) return result;
|
|
@@ -12211,7 +12242,7 @@ class Database extends EventEmitter {
|
|
|
12211
12242
|
super();
|
|
12212
12243
|
this.version = "1";
|
|
12213
12244
|
this.s3dbVersion = (() => {
|
|
12214
|
-
const [ok, err, version] = try_fn_default(() => true ? "7.
|
|
12245
|
+
const [ok, err, version] = try_fn_default(() => true ? "7.2.0" : "latest");
|
|
12215
12246
|
return ok ? version : "latest";
|
|
12216
12247
|
})();
|
|
12217
12248
|
this.resources = {};
|