s3db.js 7.3.5 โ 7.3.6
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 +1285 -157
- package/dist/s3db.cjs.js +296 -83
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.es.js +296 -83
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +296 -83
- package/dist/s3db.iife.min.js +1 -1
- package/mcp/server.js +1410 -0
- package/package.json +7 -1
- package/src/plugins/cache/filesystem-cache.class.js +9 -0
- package/src/plugins/replicator.plugin.js +130 -46
- package/src/plugins/replicators/bigquery-replicator.class.js +31 -5
- package/src/plugins/replicators/postgres-replicator.class.js +17 -2
- package/src/plugins/replicators/s3db-replicator.class.js +172 -68
- package/src/plugins/replicators/sqs-replicator.class.js +13 -1
package/PLUGINS.md
CHANGED
|
@@ -1561,7 +1561,16 @@ setTimeout(() => {
|
|
|
1561
1561
|
|
|
1562
1562
|
## ๐ Replicator Plugin
|
|
1563
1563
|
|
|
1564
|
-
|
|
1564
|
+
**Enterprise-grade data replication system** that synchronizes your s3db data in real-time to multiple targets including other S3DB instances, SQS queues, BigQuery, PostgreSQL databases, and more. Features robust error handling, advanced transformation capabilities, and comprehensive monitoring.
|
|
1565
|
+
|
|
1566
|
+
### ๐ฏ Key Features
|
|
1567
|
+
|
|
1568
|
+
- **Real-time Replication**: Automatic data synchronization on insert, update, and delete operations
|
|
1569
|
+
- **Multi-Target Support**: Replicate to S3DB, BigQuery, PostgreSQL, SQS, and custom targets
|
|
1570
|
+
- **Advanced Transformations**: Transform data with custom functions before replication
|
|
1571
|
+
- **Error Resilience**: Automatic retries, detailed error reporting, and dead letter queue support
|
|
1572
|
+
- **Performance Monitoring**: Built-in metrics, performance tracking, and health monitoring
|
|
1573
|
+
- **Flexible Configuration**: Support for multiple resource mapping syntaxes and selective replication
|
|
1565
1574
|
|
|
1566
1575
|
### โก Quick Start
|
|
1567
1576
|
|
|
@@ -1571,6 +1580,7 @@ import { S3db, ReplicatorPlugin } from 's3db.js';
|
|
|
1571
1580
|
const s3db = new S3db({
|
|
1572
1581
|
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1573
1582
|
plugins: [new ReplicatorPlugin({
|
|
1583
|
+
verbose: true, // Enable detailed logging for debugging
|
|
1574
1584
|
replicators: [
|
|
1575
1585
|
{
|
|
1576
1586
|
driver: 's3db',
|
|
@@ -1585,7 +1595,7 @@ const s3db = new S3db({
|
|
|
1585
1595
|
|
|
1586
1596
|
await s3db.connect();
|
|
1587
1597
|
|
|
1588
|
-
// Data is automatically replicated
|
|
1598
|
+
// Data is automatically replicated with detailed error reporting
|
|
1589
1599
|
const users = s3db.resource('users');
|
|
1590
1600
|
await users.insert({ name: 'John', email: 'john@example.com' });
|
|
1591
1601
|
// This insert is automatically replicated to the backup database
|
|
@@ -1595,21 +1605,56 @@ await users.insert({ name: 'John', email: 'john@example.com' });
|
|
|
1595
1605
|
|
|
1596
1606
|
| Parameter | Type | Default | Description |
|
|
1597
1607
|
|-----------|------|---------|-------------|
|
|
1598
|
-
| `enabled` | boolean | `true` | Enable/disable replication |
|
|
1599
|
-
| `replicators` | array | `[]` | Array of replicator configurations |
|
|
1600
|
-
| `
|
|
1601
|
-
| `
|
|
1602
|
-
| `
|
|
1603
|
-
| `
|
|
1604
|
-
| `
|
|
1605
|
-
| `
|
|
1608
|
+
| `enabled` | boolean | `true` | Enable/disable replication globally |
|
|
1609
|
+
| `replicators` | array | `[]` | Array of replicator configurations (required) |
|
|
1610
|
+
| `verbose` | boolean | `false` | Enable detailed console logging for debugging |
|
|
1611
|
+
| `persistReplicatorLog` | boolean | `false` | Store replication logs in database resource |
|
|
1612
|
+
| `replicatorLogResource` | string | `'replicator_log'` | Name of log resource for persistence |
|
|
1613
|
+
| `logErrors` | boolean | `true` | Log errors to replication log resource |
|
|
1614
|
+
| `batchSize` | number | `100` | Batch size for bulk replication operations |
|
|
1615
|
+
| `maxRetries` | number | `3` | Maximum retry attempts for failed replications |
|
|
1616
|
+
| `timeout` | number | `30000` | Timeout for replication operations (ms) |
|
|
1617
|
+
|
|
1618
|
+
### ๐๏ธ Event System & Debugging
|
|
1619
|
+
|
|
1620
|
+
The Replicator Plugin emits comprehensive events for monitoring and debugging:
|
|
1621
|
+
|
|
1622
|
+
```javascript
|
|
1623
|
+
const replicatorPlugin = s3db.plugins.find(p => p.constructor.name === 'ReplicatorPlugin');
|
|
1606
1624
|
|
|
1607
|
-
|
|
1625
|
+
// Success events
|
|
1626
|
+
replicatorPlugin.on('replicated', (data) => {
|
|
1627
|
+
console.log(`โ
Replicated: ${data.operation} on ${data.resourceName} to ${data.replicator}`);
|
|
1628
|
+
});
|
|
1608
1629
|
|
|
1609
|
-
|
|
1630
|
+
// Error events
|
|
1631
|
+
replicatorPlugin.on('replicator_error', (data) => {
|
|
1632
|
+
console.error(`โ Replication failed: ${data.error} (${data.resourceName})`);
|
|
1633
|
+
});
|
|
1610
1634
|
|
|
1611
|
-
|
|
1635
|
+
// Log resource errors
|
|
1636
|
+
replicatorPlugin.on('replicator_log_error', (data) => {
|
|
1637
|
+
console.warn(`โ ๏ธ Failed to log replication: ${data.logError}`);
|
|
1638
|
+
});
|
|
1612
1639
|
|
|
1640
|
+
// Setup errors
|
|
1641
|
+
replicatorPlugin.on('replicator_log_resource_creation_error', (data) => {
|
|
1642
|
+
console.error(`๐จ Log resource creation failed: ${data.error}`);
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// Cleanup errors
|
|
1646
|
+
replicatorPlugin.on('replicator_cleanup_error', (data) => {
|
|
1647
|
+
console.warn(`๐งน Cleanup failed for ${data.replicator}: ${data.error}`);
|
|
1648
|
+
});
|
|
1649
|
+
```
|
|
1650
|
+
|
|
1651
|
+
### ๐ง Replicator Drivers
|
|
1652
|
+
|
|
1653
|
+
#### ๐๏ธ S3DB Replicator
|
|
1654
|
+
|
|
1655
|
+
Replicate to another S3DB instance with **advanced resource mapping and transformation capabilities**. Supports multiple configuration syntaxes for maximum flexibility.
|
|
1656
|
+
|
|
1657
|
+
**Basic Configuration:**
|
|
1613
1658
|
```javascript
|
|
1614
1659
|
{
|
|
1615
1660
|
driver: 's3db',
|
|
@@ -1623,43 +1668,34 @@ Replicate to another S3DB instance with flexible resource mapping and transforma
|
|
|
1623
1668
|
// Map source โ destination resource name
|
|
1624
1669
|
products: 'backup_products',
|
|
1625
1670
|
|
|
1626
|
-
// Advanced mapping with
|
|
1671
|
+
// Advanced mapping with transform function
|
|
1627
1672
|
orders: {
|
|
1628
1673
|
resource: 'order_backup',
|
|
1629
|
-
|
|
1674
|
+
transform: (data) => ({
|
|
1630
1675
|
...data,
|
|
1631
1676
|
backup_timestamp: new Date().toISOString(),
|
|
1632
|
-
original_source: 'production'
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
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
|
-
]
|
|
1677
|
+
original_source: 'production',
|
|
1678
|
+
migrated_at: new Date().toISOString()
|
|
1679
|
+
}),
|
|
1680
|
+
actions: ['insert', 'update', 'delete']
|
|
1681
|
+
}
|
|
1648
1682
|
}
|
|
1649
1683
|
}
|
|
1650
1684
|
```
|
|
1651
1685
|
|
|
1652
|
-
|
|
1686
|
+
### ๐ Resource Configuration Syntaxes
|
|
1653
1687
|
|
|
1654
|
-
The S3DB replicator supports
|
|
1688
|
+
The S3DB replicator supports **multiple configuration syntaxes** for maximum flexibility. You can mix and match these formats as needed:
|
|
1655
1689
|
|
|
1656
|
-
|
|
1690
|
+
#### 1. Array of Resource Names
|
|
1691
|
+
**Use case**: Simple backup/clone scenarios
|
|
1657
1692
|
```javascript
|
|
1658
1693
|
resources: ['users', 'products', 'orders']
|
|
1659
1694
|
// Replicates each resource to itself in the destination database
|
|
1660
1695
|
```
|
|
1661
1696
|
|
|
1662
|
-
|
|
1697
|
+
#### 2. Simple Object Mapping
|
|
1698
|
+
**Use case**: Rename resources during replication
|
|
1663
1699
|
```javascript
|
|
1664
1700
|
resources: {
|
|
1665
1701
|
users: 'people', // users โ people
|
|
@@ -1668,230 +1704,1322 @@ resources: {
|
|
|
1668
1704
|
}
|
|
1669
1705
|
```
|
|
1670
1706
|
|
|
1671
|
-
|
|
1707
|
+
#### 3. Object with Transform Function
|
|
1708
|
+
**Use case**: Data transformation during replication โญ **RECOMMENDED**
|
|
1672
1709
|
```javascript
|
|
1673
1710
|
resources: {
|
|
1674
|
-
users:
|
|
1675
|
-
|
|
1711
|
+
users: {
|
|
1712
|
+
resource: 'people', // Destination resource name
|
|
1713
|
+
transform: (data) => ({ // Data transformation function
|
|
1714
|
+
...data,
|
|
1715
|
+
fullName: `${data.firstName} ${data.lastName}`,
|
|
1716
|
+
migrated_at: new Date().toISOString(),
|
|
1717
|
+
source_system: 'production'
|
|
1718
|
+
}),
|
|
1719
|
+
actions: ['insert', 'update', 'delete'] // Optional: which operations to replicate
|
|
1720
|
+
}
|
|
1676
1721
|
}
|
|
1677
1722
|
```
|
|
1678
1723
|
|
|
1679
|
-
|
|
1724
|
+
#### 4. Function-Only Transformation
|
|
1725
|
+
**Use case**: Transform data without changing resource name
|
|
1680
1726
|
```javascript
|
|
1681
1727
|
resources: {
|
|
1682
|
-
users: {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
})
|
|
1689
|
-
}
|
|
1728
|
+
users: (data) => ({
|
|
1729
|
+
...data,
|
|
1730
|
+
processed: true,
|
|
1731
|
+
backup_date: new Date().toISOString(),
|
|
1732
|
+
hash: crypto.createHash('md5').update(JSON.stringify(data)).digest('hex')
|
|
1733
|
+
})
|
|
1690
1734
|
}
|
|
1691
1735
|
```
|
|
1692
1736
|
|
|
1693
|
-
|
|
1737
|
+
#### 5. Multi-Destination Replication
|
|
1738
|
+
**Use case**: Send data to multiple targets with different transformations
|
|
1694
1739
|
```javascript
|
|
1695
1740
|
resources: {
|
|
1696
1741
|
users: [
|
|
1697
|
-
'people', // Simple copy
|
|
1742
|
+
'people', // Simple copy to 'people'
|
|
1698
1743
|
{
|
|
1699
1744
|
resource: 'user_analytics',
|
|
1700
|
-
|
|
1745
|
+
transform: (data) => ({ // Transformed copy to 'user_analytics'
|
|
1701
1746
|
id: data.id,
|
|
1702
1747
|
signup_date: data.createdAt,
|
|
1703
|
-
user_type: data.role || 'standard'
|
|
1748
|
+
user_type: data.role || 'standard',
|
|
1749
|
+
last_activity: new Date().toISOString()
|
|
1750
|
+
})
|
|
1751
|
+
},
|
|
1752
|
+
{
|
|
1753
|
+
resource: 'audit_trail',
|
|
1754
|
+
transform: (data) => ({ // Audit copy to 'audit_trail'
|
|
1755
|
+
user_id: data.id,
|
|
1756
|
+
action: 'user_replicated',
|
|
1757
|
+
timestamp: new Date().toISOString(),
|
|
1758
|
+
data_hash: crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex')
|
|
1704
1759
|
})
|
|
1705
1760
|
}
|
|
1706
1761
|
]
|
|
1707
1762
|
}
|
|
1708
1763
|
```
|
|
1709
1764
|
|
|
1710
|
-
|
|
1765
|
+
#### 6. Advanced Mixed Configuration
|
|
1766
|
+
**Use case**: Complex enterprise scenarios
|
|
1711
1767
|
```javascript
|
|
1712
|
-
resources: {
|
|
1713
|
-
|
|
1768
|
+
resources: {
|
|
1769
|
+
// Simple mappings
|
|
1770
|
+
sessions: 'user_sessions',
|
|
1771
|
+
|
|
1772
|
+
// Transform without renaming
|
|
1773
|
+
products: (data) => ({
|
|
1714
1774
|
...data,
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
})
|
|
1775
|
+
search_keywords: data.name?.toLowerCase().split(' ') || [],
|
|
1776
|
+
price_category: data.price > 100 ? 'premium' : 'standard'
|
|
1777
|
+
}),
|
|
1778
|
+
|
|
1779
|
+
// Complex multi-destination with conditions
|
|
1780
|
+
orders: [
|
|
1781
|
+
'order_backup', // Simple backup
|
|
1782
|
+
{
|
|
1783
|
+
resource: 'order_analytics',
|
|
1784
|
+
transform: (data) => ({
|
|
1785
|
+
order_id: data.id,
|
|
1786
|
+
customer_id: data.userId,
|
|
1787
|
+
amount: data.amount,
|
|
1788
|
+
order_date: data.createdAt?.split('T')[0],
|
|
1789
|
+
status: data.status || 'pending',
|
|
1790
|
+
item_count: data.items?.length || 0,
|
|
1791
|
+
is_large_order: data.amount > 1000
|
|
1792
|
+
}),
|
|
1793
|
+
actions: ['insert', 'update'] // Only replicate inserts and updates, not deletes
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
resource: 'financial_records',
|
|
1797
|
+
transform: (data) => ({
|
|
1798
|
+
transaction_id: data.id,
|
|
1799
|
+
amount: data.amount,
|
|
1800
|
+
currency: data.currency || 'USD',
|
|
1801
|
+
type: 'order_payment',
|
|
1802
|
+
timestamp: new Date().toISOString(),
|
|
1803
|
+
metadata: {
|
|
1804
|
+
customer_id: data.userId,
|
|
1805
|
+
order_items: data.items?.length || 0
|
|
1806
|
+
}
|
|
1807
|
+
}),
|
|
1808
|
+
actions: ['insert'] // Only replicate new orders for financial records
|
|
1809
|
+
}
|
|
1810
|
+
],
|
|
1811
|
+
|
|
1812
|
+
// Conditional replication
|
|
1813
|
+
users: {
|
|
1814
|
+
resource: 'customer_profiles',
|
|
1815
|
+
transform: (data) => {
|
|
1816
|
+
// Only replicate active users
|
|
1817
|
+
if (data.status !== 'active') return null;
|
|
1818
|
+
|
|
1819
|
+
return {
|
|
1820
|
+
...data,
|
|
1821
|
+
fullName: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
|
|
1822
|
+
account_age_days: data.createdAt ?
|
|
1823
|
+
Math.floor((Date.now() - new Date(data.createdAt)) / (1000 * 60 * 60 * 24)) : 0,
|
|
1824
|
+
preferences: data.preferences || {},
|
|
1825
|
+
last_updated: new Date().toISOString()
|
|
1826
|
+
};
|
|
1827
|
+
},
|
|
1828
|
+
actions: ['insert', 'update']
|
|
1829
|
+
}
|
|
1718
1830
|
}
|
|
1719
1831
|
```
|
|
1720
1832
|
|
|
1721
|
-
|
|
1833
|
+
### ๐ง S3DB Replicator Configuration Options
|
|
1834
|
+
|
|
1835
|
+
| Parameter | Type | Required | Description |
|
|
1836
|
+
|-----------|------|----------|-------------|
|
|
1837
|
+
| `connectionString` | string | Yes* | S3DB connection string for destination database |
|
|
1838
|
+
| `client` | S3db | Yes* | Pre-configured S3DB client instance |
|
|
1839
|
+
| `resources` | object/array | Yes | Resource mapping configuration (see syntaxes above) |
|
|
1840
|
+
| `timeout` | number | No | Operation timeout in milliseconds (default: 30000) |
|
|
1841
|
+
| `retryAttempts` | number | No | Number of retry attempts for failed operations (default: 3) |
|
|
1842
|
+
|
|
1843
|
+
*Either `connectionString` or `client` must be provided.
|
|
1844
|
+
|
|
1845
|
+
### ๐ฏ Transform Function Features
|
|
1846
|
+
|
|
1847
|
+
Transform functions provide powerful data manipulation capabilities:
|
|
1848
|
+
|
|
1849
|
+
#### Data Transformation Examples:
|
|
1722
1850
|
```javascript
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1851
|
+
// 1. Field mapping and enrichment
|
|
1852
|
+
transform: (data) => ({
|
|
1853
|
+
id: data.id,
|
|
1854
|
+
customer_name: `${data.firstName} ${data.lastName}`,
|
|
1855
|
+
email_domain: data.email?.split('@')[1] || 'unknown',
|
|
1856
|
+
created_timestamp: Date.now(),
|
|
1857
|
+
source: 'production-db'
|
|
1858
|
+
})
|
|
1859
|
+
|
|
1860
|
+
// 2. Conditional logic
|
|
1861
|
+
transform: (data) => {
|
|
1862
|
+
if (data.type === 'premium') {
|
|
1863
|
+
return { ...data, priority: 'high', sla: '4hours' };
|
|
1864
|
+
}
|
|
1865
|
+
return { ...data, priority: 'normal', sla: '24hours' };
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// 3. Data validation and filtering
|
|
1869
|
+
transform: (data) => {
|
|
1870
|
+
// Skip replication for invalid data
|
|
1871
|
+
if (!data.email || !data.name) return null;
|
|
1872
|
+
|
|
1873
|
+
return {
|
|
1874
|
+
...data,
|
|
1875
|
+
email: data.email.toLowerCase(),
|
|
1876
|
+
name: data.name.trim(),
|
|
1877
|
+
validated: true
|
|
1878
|
+
};
|
|
1737
1879
|
}
|
|
1880
|
+
|
|
1881
|
+
// 4. Computed fields
|
|
1882
|
+
transform: (data) => ({
|
|
1883
|
+
...data,
|
|
1884
|
+
age: data.birthDate ?
|
|
1885
|
+
Math.floor((Date.now() - new Date(data.birthDate)) / (1000 * 60 * 60 * 24 * 365)) : null,
|
|
1886
|
+
account_value: (data.orders || []).reduce((sum, order) => sum + order.amount, 0),
|
|
1887
|
+
last_activity: new Date().toISOString()
|
|
1888
|
+
})
|
|
1889
|
+
|
|
1890
|
+
// 5. Data aggregation
|
|
1891
|
+
transform: (data) => ({
|
|
1892
|
+
user_id: data.id,
|
|
1893
|
+
total_orders: data.orderHistory?.length || 0,
|
|
1894
|
+
total_spent: data.orderHistory?.reduce((sum, order) => sum + order.amount, 0) || 0,
|
|
1895
|
+
favorite_category: data.orderHistory?.map(o => o.category)
|
|
1896
|
+
.sort((a,b) => a.localeCompare(b))
|
|
1897
|
+
.reduce((prev, curr, i, arr) =>
|
|
1898
|
+
arr.filter(v => v === prev).length >= arr.filter(v => v === curr).length ? prev : curr
|
|
1899
|
+
) || null,
|
|
1900
|
+
analysis_date: new Date().toISOString()
|
|
1901
|
+
})
|
|
1738
1902
|
```
|
|
1739
1903
|
|
|
1740
|
-
**
|
|
1741
|
-
- `
|
|
1742
|
-
- `
|
|
1743
|
-
-
|
|
1904
|
+
**Transform Function Best Practices:**
|
|
1905
|
+
- **Return `null`** to skip replication for specific records
|
|
1906
|
+
- **Preserve the `id` field** unless specifically mapping to a different field
|
|
1907
|
+
- **Handle edge cases** like missing fields or null values
|
|
1908
|
+
- **Use immutable operations** to avoid modifying the original data
|
|
1909
|
+
- **Keep transforms lightweight** to maintain replication performance
|
|
1910
|
+
- **Add metadata fields** like timestamps for tracking purposes
|
|
1744
1911
|
|
|
1745
|
-
|
|
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
|
|
1912
|
+
#### ๐ฌ SQS Replicator
|
|
1751
1913
|
|
|
1752
|
-
|
|
1914
|
+
**Real-time event streaming** to AWS SQS queues for microservices integration and event-driven architectures.
|
|
1753
1915
|
|
|
1754
|
-
|
|
1916
|
+
**โ ๏ธ Required Dependency:**
|
|
1917
|
+
```bash
|
|
1918
|
+
pnpm add @aws-sdk/client-sqs
|
|
1919
|
+
```
|
|
1755
1920
|
|
|
1921
|
+
**Basic Configuration:**
|
|
1756
1922
|
```javascript
|
|
1757
1923
|
{
|
|
1758
1924
|
driver: 'sqs',
|
|
1759
|
-
resources: ['orders'],
|
|
1925
|
+
resources: ['orders', 'users'],
|
|
1760
1926
|
config: {
|
|
1761
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue',
|
|
1762
1927
|
region: 'us-east-1',
|
|
1763
|
-
|
|
1928
|
+
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/events.fifo',
|
|
1929
|
+
messageGroupId: 's3db-events',
|
|
1764
1930
|
deduplicationId: true
|
|
1765
1931
|
}
|
|
1766
1932
|
}
|
|
1767
1933
|
```
|
|
1768
1934
|
|
|
1769
|
-
|
|
1935
|
+
**Advanced Configuration with Resource-Specific Queues:**
|
|
1936
|
+
```javascript
|
|
1937
|
+
{
|
|
1938
|
+
driver: 'sqs',
|
|
1939
|
+
config: {
|
|
1940
|
+
region: 'us-east-1',
|
|
1941
|
+
credentials: {
|
|
1942
|
+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
1943
|
+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
1944
|
+
},
|
|
1945
|
+
|
|
1946
|
+
// Resource-specific queue URLs
|
|
1947
|
+
queues: {
|
|
1948
|
+
orders: 'https://sqs.us-east-1.amazonaws.com/123456789012/order-events.fifo',
|
|
1949
|
+
users: 'https://sqs.us-east-1.amazonaws.com/123456789012/user-events.fifo',
|
|
1950
|
+
payments: 'https://sqs.us-east-1.amazonaws.com/123456789012/payment-events.fifo'
|
|
1951
|
+
},
|
|
1952
|
+
|
|
1953
|
+
// Default queue for resources not specifically mapped
|
|
1954
|
+
defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/general-events.fifo',
|
|
1955
|
+
|
|
1956
|
+
// FIFO queue settings
|
|
1957
|
+
messageGroupId: 's3db-replicator',
|
|
1958
|
+
deduplicationId: true,
|
|
1959
|
+
|
|
1960
|
+
// Message attributes (applied to all messages)
|
|
1961
|
+
messageAttributes: {
|
|
1962
|
+
source: { StringValue: 'production-db', DataType: 'String' },
|
|
1963
|
+
version: { StringValue: '1.0', DataType: 'String' },
|
|
1964
|
+
environment: { StringValue: process.env.NODE_ENV || 'development', DataType: 'String' }
|
|
1965
|
+
}
|
|
1966
|
+
},
|
|
1967
|
+
resources: {
|
|
1968
|
+
// Simple resource mapping
|
|
1969
|
+
orders: true,
|
|
1970
|
+
users: true,
|
|
1971
|
+
|
|
1972
|
+
// Resource with transformation
|
|
1973
|
+
payments: {
|
|
1974
|
+
transform: (data) => ({
|
|
1975
|
+
payment_id: data.id,
|
|
1976
|
+
amount: data.amount,
|
|
1977
|
+
currency: data.currency || 'USD',
|
|
1978
|
+
customer_id: data.userId,
|
|
1979
|
+
payment_method: data.method,
|
|
1980
|
+
status: data.status,
|
|
1981
|
+
timestamp: new Date().toISOString(),
|
|
1982
|
+
|
|
1983
|
+
// Add computed fields
|
|
1984
|
+
amount_usd: data.currency === 'USD' ? data.amount : data.amount * (data.exchange_rate || 1),
|
|
1985
|
+
is_large_payment: data.amount > 1000,
|
|
1986
|
+
risk_score: data.amount > 5000 ? 'high' : data.amount > 1000 ? 'medium' : 'low'
|
|
1987
|
+
})
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
```
|
|
1770
1992
|
|
|
1771
|
-
|
|
1993
|
+
**SQS Message Format:**
|
|
1994
|
+
```javascript
|
|
1995
|
+
{
|
|
1996
|
+
// Message Body (JSON string)
|
|
1997
|
+
MessageBody: JSON.stringify({
|
|
1998
|
+
resource: 'orders', // Source resource name
|
|
1999
|
+
operation: 'insert', // Operation: insert, update, delete
|
|
2000
|
+
id: 'order-123', // Record ID
|
|
2001
|
+
data: { // Transformed data payload
|
|
2002
|
+
id: 'order-123',
|
|
2003
|
+
userId: 'user-456',
|
|
2004
|
+
amount: 99.99,
|
|
2005
|
+
status: 'pending',
|
|
2006
|
+
timestamp: '2024-01-15T10:30:00.000Z'
|
|
2007
|
+
},
|
|
2008
|
+
beforeData: null, // Previous data for updates
|
|
2009
|
+
metadata: {
|
|
2010
|
+
source: 'production-db',
|
|
2011
|
+
replicator: 'sqs-replicator',
|
|
2012
|
+
timestamp: '2024-01-15T10:30:00.000Z'
|
|
2013
|
+
}
|
|
2014
|
+
}),
|
|
2015
|
+
|
|
2016
|
+
// Message Attributes
|
|
2017
|
+
MessageAttributes: {
|
|
2018
|
+
source: { StringValue: 'production-db', DataType: 'String' },
|
|
2019
|
+
resource: { StringValue: 'orders', DataType: 'String' },
|
|
2020
|
+
operation: { StringValue: 'insert', DataType: 'String' }
|
|
2021
|
+
},
|
|
2022
|
+
|
|
2023
|
+
// FIFO Queue Settings
|
|
2024
|
+
MessageGroupId: 's3db-events',
|
|
2025
|
+
MessageDeduplicationId: 'orders:insert:order-123'
|
|
2026
|
+
}
|
|
2027
|
+
```
|
|
1772
2028
|
|
|
2029
|
+
**Configuration Options:**
|
|
2030
|
+
|
|
2031
|
+
| Parameter | Type | Required | Description |
|
|
2032
|
+
|-----------|------|----------|-------------|
|
|
2033
|
+
| `region` | string | Yes | AWS region for SQS service |
|
|
2034
|
+
| `queueUrl` | string | Yes* | Single queue URL for all resources |
|
|
2035
|
+
| `queues` | object | Yes* | Resource-specific queue mapping |
|
|
2036
|
+
| `defaultQueueUrl` | string | No | Fallback queue for unmapped resources |
|
|
2037
|
+
| `credentials` | object | No | AWS credentials (uses default chain if omitted) |
|
|
2038
|
+
| `messageGroupId` | string | No | Message group ID for FIFO queues |
|
|
2039
|
+
| `deduplicationId` | boolean | No | Enable message deduplication for FIFO queues |
|
|
2040
|
+
| `messageAttributes` | object | No | Custom message attributes applied to all messages |
|
|
2041
|
+
|
|
2042
|
+
*Either `queueUrl` or `queues` must be provided.
|
|
2043
|
+
|
|
2044
|
+
#### ๐ BigQuery Replicator
|
|
2045
|
+
|
|
2046
|
+
**Data warehouse integration** for Google BigQuery with advanced transformation capabilities, multi-table support, and automatic retry logic for streaming buffer limitations.
|
|
2047
|
+
|
|
2048
|
+
**โ ๏ธ Required Dependency:**
|
|
2049
|
+
```bash
|
|
2050
|
+
pnpm add @google-cloud/bigquery
|
|
2051
|
+
```
|
|
2052
|
+
|
|
2053
|
+
**Basic Configuration:**
|
|
1773
2054
|
```javascript
|
|
1774
2055
|
{
|
|
1775
2056
|
driver: 'bigquery',
|
|
1776
2057
|
config: {
|
|
2058
|
+
projectId: 'my-analytics-project',
|
|
2059
|
+
datasetId: 'production_data',
|
|
1777
2060
|
location: 'US',
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
2061
|
+
credentials: {
|
|
2062
|
+
client_email: 'service-account@project.iam.gserviceaccount.com',
|
|
2063
|
+
private_key: process.env.BIGQUERY_PRIVATE_KEY,
|
|
2064
|
+
project_id: 'my-analytics-project'
|
|
2065
|
+
}
|
|
1782
2066
|
},
|
|
1783
2067
|
resources: {
|
|
1784
2068
|
// Simple table mapping
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2069
|
+
users: 'users_table',
|
|
2070
|
+
orders: 'orders_table',
|
|
2071
|
+
products: 'products_table'
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
```
|
|
2075
|
+
|
|
2076
|
+
**Advanced Multi-Table Configuration:**
|
|
2077
|
+
```javascript
|
|
2078
|
+
{
|
|
2079
|
+
driver: 'bigquery',
|
|
2080
|
+
config: {
|
|
2081
|
+
projectId: 'my-analytics-project',
|
|
2082
|
+
datasetId: 'analytics_warehouse',
|
|
2083
|
+
location: 'US',
|
|
2084
|
+
logTable: 'replication_audit_log', // Optional: audit all operations
|
|
2085
|
+
credentials: JSON.parse(Buffer.from(process.env.GOOGLE_CREDENTIALS, 'base64').toString())
|
|
2086
|
+
},
|
|
2087
|
+
resources: {
|
|
2088
|
+
// Simple string mapping
|
|
2089
|
+
sessions: 'user_sessions',
|
|
2090
|
+
clicks: 'click_events',
|
|
2091
|
+
|
|
2092
|
+
// Single table with actions and transforms
|
|
1794
2093
|
users: {
|
|
1795
|
-
table: '
|
|
2094
|
+
table: 'dim_users',
|
|
1796
2095
|
actions: ['insert', 'update'],
|
|
1797
|
-
transform: (data) => {
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2096
|
+
transform: (data) => ({
|
|
2097
|
+
...data,
|
|
2098
|
+
// Data cleaning and enrichment
|
|
2099
|
+
email: data.email?.toLowerCase(),
|
|
2100
|
+
full_name: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
|
|
2101
|
+
registration_date: data.createdAt?.split('T')[0],
|
|
2102
|
+
account_age_days: data.createdAt ?
|
|
2103
|
+
Math.floor((Date.now() - new Date(data.createdAt)) / (1000 * 60 * 60 * 24)) : 0,
|
|
2104
|
+
|
|
2105
|
+
// Computed fields for analytics
|
|
2106
|
+
user_tier: data.totalSpent > 1000 ? 'premium' :
|
|
2107
|
+
data.totalSpent > 100 ? 'standard' : 'basic',
|
|
2108
|
+
is_active: data.lastLoginAt &&
|
|
2109
|
+
(Date.now() - new Date(data.lastLoginAt)) < (30 * 24 * 60 * 60 * 1000),
|
|
2110
|
+
|
|
2111
|
+
// BigQuery-specific fields
|
|
2112
|
+
processed_at: new Date().toISOString(),
|
|
2113
|
+
data_source: 'production_s3db'
|
|
2114
|
+
})
|
|
1804
2115
|
},
|
|
1805
2116
|
|
|
1806
|
-
// Multiple destinations for
|
|
2117
|
+
// Multiple destinations for comprehensive analytics
|
|
1807
2118
|
orders: [
|
|
1808
|
-
|
|
1809
|
-
{
|
|
1810
|
-
|
|
1811
|
-
|
|
2119
|
+
// Fact table for detailed order data
|
|
2120
|
+
{
|
|
2121
|
+
table: 'fact_orders',
|
|
2122
|
+
actions: ['insert', 'update'],
|
|
2123
|
+
transform: (data) => ({
|
|
2124
|
+
order_id: data.id,
|
|
2125
|
+
customer_id: data.userId,
|
|
2126
|
+
order_date: data.createdAt?.split('T')[0],
|
|
2127
|
+
order_timestamp: data.createdAt,
|
|
2128
|
+
amount: parseFloat(data.amount) || 0,
|
|
2129
|
+
currency: data.currency || 'USD',
|
|
2130
|
+
status: data.status,
|
|
2131
|
+
item_count: data.items?.length || 0,
|
|
2132
|
+
|
|
2133
|
+
// Derived analytics fields
|
|
2134
|
+
is_weekend_order: new Date(data.createdAt).getDay() % 6 === 0,
|
|
2135
|
+
order_hour: new Date(data.createdAt).getHours(),
|
|
2136
|
+
is_large_order: data.amount > 500,
|
|
2137
|
+
|
|
2138
|
+
// Metadata
|
|
2139
|
+
ingested_at: new Date().toISOString(),
|
|
2140
|
+
source_system: 'production'
|
|
2141
|
+
})
|
|
2142
|
+
},
|
|
2143
|
+
|
|
2144
|
+
// Daily aggregation table
|
|
2145
|
+
{
|
|
2146
|
+
table: 'daily_revenue_summary',
|
|
2147
|
+
actions: ['insert'], // Only for new orders
|
|
1812
2148
|
transform: (data) => ({
|
|
1813
2149
|
date: data.createdAt?.split('T')[0],
|
|
1814
|
-
revenue: data.amount,
|
|
2150
|
+
revenue: parseFloat(data.amount) || 0,
|
|
2151
|
+
currency: data.currency || 'USD',
|
|
2152
|
+
order_count: 1,
|
|
1815
2153
|
customer_id: data.userId,
|
|
1816
|
-
|
|
2154
|
+
|
|
2155
|
+
// Additional dimensions
|
|
2156
|
+
order_source: data.source || 'web',
|
|
2157
|
+
payment_method: data.paymentMethod,
|
|
2158
|
+
is_first_order: data.isFirstOrder || false,
|
|
2159
|
+
|
|
2160
|
+
created_at: new Date().toISOString()
|
|
2161
|
+
})
|
|
2162
|
+
},
|
|
2163
|
+
|
|
2164
|
+
// Customer analytics (updates only)
|
|
2165
|
+
{
|
|
2166
|
+
table: 'customer_order_analytics',
|
|
2167
|
+
actions: ['insert', 'update'],
|
|
2168
|
+
transform: (data) => ({
|
|
2169
|
+
customer_id: data.userId,
|
|
2170
|
+
latest_order_id: data.id,
|
|
2171
|
+
latest_order_date: data.createdAt?.split('T')[0],
|
|
2172
|
+
latest_order_amount: parseFloat(data.amount) || 0,
|
|
2173
|
+
|
|
2174
|
+
// Will need to be aggregated in BigQuery
|
|
2175
|
+
lifetime_value_increment: parseFloat(data.amount) || 0,
|
|
2176
|
+
|
|
2177
|
+
updated_at: new Date().toISOString()
|
|
1817
2178
|
})
|
|
1818
2179
|
}
|
|
1819
|
-
]
|
|
2180
|
+
],
|
|
2181
|
+
|
|
2182
|
+
// Event tracking with conditional replication
|
|
2183
|
+
events: {
|
|
2184
|
+
table: 'user_events',
|
|
2185
|
+
actions: ['insert'],
|
|
2186
|
+
transform: (data) => {
|
|
2187
|
+
// Only replicate certain event types
|
|
2188
|
+
const allowedEventTypes = ['page_view', 'button_click', 'form_submit', 'purchase'];
|
|
2189
|
+
if (!allowedEventTypes.includes(data.event_type)) {
|
|
2190
|
+
return null; // Skip replication
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
return {
|
|
2194
|
+
event_id: data.id,
|
|
2195
|
+
user_id: data.userId,
|
|
2196
|
+
event_type: data.event_type,
|
|
2197
|
+
event_timestamp: data.timestamp,
|
|
2198
|
+
event_date: data.timestamp?.split('T')[0],
|
|
2199
|
+
|
|
2200
|
+
// Parse and clean event properties
|
|
2201
|
+
properties: JSON.stringify(data.properties || {}),
|
|
2202
|
+
page_url: data.properties?.page_url,
|
|
2203
|
+
referrer: data.properties?.referrer,
|
|
2204
|
+
|
|
2205
|
+
// Session information
|
|
2206
|
+
session_id: data.sessionId,
|
|
2207
|
+
|
|
2208
|
+
// Technical metadata
|
|
2209
|
+
user_agent: data.userAgent,
|
|
2210
|
+
ip_address: data.ipAddress ? 'masked' : null, // Privacy compliance
|
|
2211
|
+
|
|
2212
|
+
// Processing metadata
|
|
2213
|
+
processed_at: new Date().toISOString(),
|
|
2214
|
+
schema_version: '1.0'
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
1820
2218
|
}
|
|
1821
2219
|
}
|
|
1822
2220
|
```
|
|
1823
2221
|
|
|
1824
2222
|
**Transform Function Features:**
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2223
|
+
|
|
2224
|
+
1. **Data Cleaning & Validation:**
|
|
2225
|
+
```javascript
|
|
2226
|
+
transform: (data) => {
|
|
2227
|
+
// Skip invalid records
|
|
2228
|
+
if (!data.email || !data.id) return null;
|
|
2229
|
+
|
|
2230
|
+
return {
|
|
2231
|
+
...data,
|
|
2232
|
+
email: data.email.toLowerCase().trim(),
|
|
2233
|
+
phone: data.phone?.replace(/\D/g, '') || null, // Remove non-digits
|
|
2234
|
+
validated_at: new Date().toISOString()
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
```
|
|
2238
|
+
|
|
2239
|
+
2. **Type Conversion for BigQuery:**
|
|
2240
|
+
```javascript
|
|
2241
|
+
transform: (data) => ({
|
|
2242
|
+
...data,
|
|
2243
|
+
amount: parseFloat(data.amount) || 0,
|
|
2244
|
+
quantity: parseInt(data.quantity) || 0,
|
|
2245
|
+
is_active: Boolean(data.isActive),
|
|
2246
|
+
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
2247
|
+
metadata: JSON.stringify(data.metadata || {})
|
|
2248
|
+
})
|
|
2249
|
+
```
|
|
2250
|
+
|
|
2251
|
+
3. **Computed Analytics Fields:**
|
|
2252
|
+
```javascript
|
|
2253
|
+
transform: (data) => ({
|
|
2254
|
+
...data,
|
|
2255
|
+
// Date dimensions
|
|
2256
|
+
order_date: data.createdAt?.split('T')[0],
|
|
2257
|
+
order_year: new Date(data.createdAt).getFullYear(),
|
|
2258
|
+
order_month: new Date(data.createdAt).getMonth() + 1,
|
|
2259
|
+
order_quarter: Math.ceil((new Date(data.createdAt).getMonth() + 1) / 3),
|
|
2260
|
+
|
|
2261
|
+
// Business logic
|
|
2262
|
+
customer_segment: data.totalSpent > 1000 ? 'VIP' :
|
|
2263
|
+
data.totalSpent > 500 ? 'Premium' : 'Standard',
|
|
2264
|
+
|
|
2265
|
+
// Geospatial (if you have coordinates)
|
|
2266
|
+
location_string: data.lat && data.lng ? `${data.lat},${data.lng}` : null
|
|
2267
|
+
})
|
|
2268
|
+
```
|
|
1830
2269
|
|
|
1831
2270
|
**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
2271
|
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2272
|
+
| Parameter | Type | Required | Description |
|
|
2273
|
+
|-----------|------|----------|-------------|
|
|
2274
|
+
| `projectId` | string | Yes | Google Cloud project ID |
|
|
2275
|
+
| `datasetId` | string | Yes | BigQuery dataset ID |
|
|
2276
|
+
| `credentials` | object | No | Service account credentials (uses ADC if omitted) |
|
|
2277
|
+
| `location` | string | No | Dataset location/region (default: 'US') |
|
|
2278
|
+
| `logTable` | string | No | Table name for operation audit logging |
|
|
2279
|
+
|
|
2280
|
+
**Resource Configuration Formats:**
|
|
2281
|
+
|
|
2282
|
+
1. **String**: Simple table mapping
|
|
2283
|
+
```javascript
|
|
2284
|
+
resources: { users: 'users_table' }
|
|
2285
|
+
```
|
|
2286
|
+
|
|
2287
|
+
2. **Object**: Single table with actions and transform
|
|
2288
|
+
```javascript
|
|
2289
|
+
resources: {
|
|
2290
|
+
users: {
|
|
2291
|
+
table: 'users_table',
|
|
2292
|
+
actions: ['insert', 'update'],
|
|
2293
|
+
transform: (data) => ({ ...data, processed: true })
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
```
|
|
2297
|
+
|
|
2298
|
+
3. **Array**: Multiple destination tables
|
|
2299
|
+
```javascript
|
|
2300
|
+
resources: {
|
|
2301
|
+
orders: [
|
|
2302
|
+
{ table: 'fact_orders', actions: ['insert'] },
|
|
2303
|
+
{ table: 'daily_revenue', actions: ['insert'], transform: aggregationFn }
|
|
2304
|
+
]
|
|
2305
|
+
}
|
|
2306
|
+
```
|
|
1842
2307
|
|
|
1843
2308
|
**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
2309
|
|
|
1849
|
-
|
|
2310
|
+
- **๐ Retry Logic**: Handles BigQuery streaming buffer limitations with 30-second delays
|
|
2311
|
+
- **๐ก๏ธ Error Handling**: Graceful handling of schema mismatches and quota limits
|
|
2312
|
+
- **๐ Operation Logging**: Optional audit trail in specified log table
|
|
2313
|
+
- **๐ง Schema Compatibility**: Automatic handling of missing fields and type coercion
|
|
2314
|
+
- **โก Streaming Inserts**: Uses BigQuery streaming API for real-time data ingestion
|
|
2315
|
+
- **๐ฏ Selective Replication**: Transform functions can return `null` to skip records
|
|
1850
2316
|
|
|
1851
|
-
|
|
2317
|
+
#### ๐ PostgreSQL Replicator
|
|
2318
|
+
|
|
2319
|
+
**Operational database integration** for PostgreSQL with support for complex SQL operations, connection pooling, and custom transformations.
|
|
2320
|
+
|
|
2321
|
+
**โ ๏ธ Required Dependency:**
|
|
2322
|
+
```bash
|
|
2323
|
+
pnpm add pg
|
|
2324
|
+
```
|
|
1852
2325
|
|
|
2326
|
+
**Basic Configuration:**
|
|
1853
2327
|
```javascript
|
|
1854
2328
|
{
|
|
1855
2329
|
driver: 'postgres',
|
|
1856
|
-
|
|
1857
|
-
|
|
2330
|
+
config: {
|
|
2331
|
+
connectionString: 'postgresql://user:pass@localhost:5432/analytics',
|
|
2332
|
+
ssl: { rejectUnauthorized: false },
|
|
2333
|
+
pool: {
|
|
2334
|
+
max: 10,
|
|
2335
|
+
idleTimeoutMillis: 30000,
|
|
2336
|
+
connectionTimeoutMillis: 2000
|
|
2337
|
+
}
|
|
1858
2338
|
},
|
|
2339
|
+
resources: {
|
|
2340
|
+
users: [{
|
|
2341
|
+
table: 'users_table',
|
|
2342
|
+
actions: ['insert', 'update', 'delete']
|
|
2343
|
+
}]
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
```
|
|
2347
|
+
|
|
2348
|
+
**Advanced Configuration with Custom SQL:**
|
|
2349
|
+
```javascript
|
|
2350
|
+
{
|
|
2351
|
+
driver: 'postgres',
|
|
1859
2352
|
config: {
|
|
1860
|
-
connectionString: 'postgresql://
|
|
2353
|
+
connectionString: 'postgresql://analytics:password@localhost:5432/operations',
|
|
2354
|
+
ssl: { rejectUnauthorized: false },
|
|
2355
|
+
logTable: 'replication_audit',
|
|
2356
|
+
pool: {
|
|
2357
|
+
max: 20,
|
|
2358
|
+
min: 5,
|
|
2359
|
+
idleTimeoutMillis: 30000,
|
|
2360
|
+
connectionTimeoutMillis: 2000,
|
|
2361
|
+
acquireTimeoutMillis: 60000
|
|
2362
|
+
}
|
|
2363
|
+
},
|
|
2364
|
+
resources: {
|
|
2365
|
+
// Multi-table replication with different strategies
|
|
2366
|
+
users: [
|
|
2367
|
+
{
|
|
2368
|
+
table: 'operational_users',
|
|
2369
|
+
actions: ['insert', 'update', 'delete'],
|
|
2370
|
+
transform: (data, operation) => {
|
|
2371
|
+
if (operation === 'delete') {
|
|
2372
|
+
return {
|
|
2373
|
+
id: data.id,
|
|
2374
|
+
deleted_at: new Date(),
|
|
2375
|
+
deletion_reason: 'user_requested'
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
return {
|
|
2380
|
+
...data,
|
|
2381
|
+
sync_timestamp: new Date(),
|
|
2382
|
+
source_system: 's3db',
|
|
2383
|
+
data_version: '1.0',
|
|
2384
|
+
|
|
2385
|
+
// Computed fields
|
|
2386
|
+
full_name: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
|
|
2387
|
+
email_domain: data.email?.split('@')[1] || 'unknown',
|
|
2388
|
+
account_age_days: data.createdAt ?
|
|
2389
|
+
Math.floor((Date.now() - new Date(data.createdAt)) / (1000 * 60 * 60 * 24)) : 0
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
},
|
|
2393
|
+
|
|
2394
|
+
// Separate audit trail table
|
|
2395
|
+
{
|
|
2396
|
+
table: 'user_audit_trail',
|
|
2397
|
+
actions: ['insert', 'update', 'delete'],
|
|
2398
|
+
transform: (data, operation) => ({
|
|
2399
|
+
user_id: data.id,
|
|
2400
|
+
operation_type: operation,
|
|
2401
|
+
operation_timestamp: new Date(),
|
|
2402
|
+
data_snapshot: JSON.stringify(data),
|
|
2403
|
+
source_database: 's3db',
|
|
2404
|
+
replication_id: crypto.randomUUID()
|
|
2405
|
+
})
|
|
2406
|
+
}
|
|
2407
|
+
],
|
|
2408
|
+
|
|
2409
|
+
// Orders with complex business logic
|
|
2410
|
+
orders: [
|
|
2411
|
+
{
|
|
2412
|
+
table: 'order_events',
|
|
2413
|
+
actions: ['insert'],
|
|
2414
|
+
transform: (data) => ({
|
|
2415
|
+
event_id: crypto.randomUUID(),
|
|
2416
|
+
order_id: data.id,
|
|
2417
|
+
customer_id: data.userId,
|
|
2418
|
+
event_type: 'order_created',
|
|
2419
|
+
event_timestamp: new Date(),
|
|
2420
|
+
order_amount: parseFloat(data.amount) || 0,
|
|
2421
|
+
order_status: data.status || 'pending',
|
|
2422
|
+
|
|
2423
|
+
// Business metrics
|
|
2424
|
+
is_large_order: data.amount > 500,
|
|
2425
|
+
order_priority: data.amount > 1000 ? 'high' :
|
|
2426
|
+
data.amount > 100 ? 'medium' : 'low',
|
|
2427
|
+
estimated_fulfillment: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days
|
|
2428
|
+
|
|
2429
|
+
// Metadata
|
|
2430
|
+
created_at: new Date(),
|
|
2431
|
+
source_system: 'production_s3db'
|
|
2432
|
+
})
|
|
2433
|
+
},
|
|
2434
|
+
|
|
2435
|
+
{
|
|
2436
|
+
table: 'order_updates',
|
|
2437
|
+
actions: ['update'],
|
|
2438
|
+
transform: (data, operation, beforeData) => ({
|
|
2439
|
+
update_id: crypto.randomUUID(),
|
|
2440
|
+
order_id: data.id,
|
|
2441
|
+
updated_at: new Date(),
|
|
2442
|
+
|
|
2443
|
+
// Track what changed
|
|
2444
|
+
changed_fields: Object.keys(data).filter(key =>
|
|
2445
|
+
beforeData && data[key] !== beforeData[key]
|
|
2446
|
+
),
|
|
2447
|
+
previous_status: beforeData?.status,
|
|
2448
|
+
new_status: data.status,
|
|
2449
|
+
|
|
2450
|
+
// Change metadata
|
|
2451
|
+
status_progression: `${beforeData?.status || 'unknown'} -> ${data.status}`,
|
|
2452
|
+
update_source: 'automated_replication'
|
|
2453
|
+
})
|
|
2454
|
+
}
|
|
2455
|
+
],
|
|
2456
|
+
|
|
2457
|
+
// Conditional replication based on data
|
|
2458
|
+
sensitive_data: {
|
|
2459
|
+
table: 'masked_sensitive_data',
|
|
2460
|
+
actions: ['insert', 'update'],
|
|
2461
|
+
transform: (data) => {
|
|
2462
|
+
// Skip replication for certain sensitive records
|
|
2463
|
+
if (data.privacy_level === 'restricted') {
|
|
2464
|
+
return null;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
return {
|
|
2468
|
+
id: data.id,
|
|
2469
|
+
// Mask sensitive fields
|
|
2470
|
+
email: data.email ? `${data.email.split('@')[0].slice(0, 3)}***@${data.email.split('@')[1]}` : null,
|
|
2471
|
+
phone: data.phone ? `***-***-${data.phone.slice(-4)}` : null,
|
|
2472
|
+
name: data.name ? `${data.name.charAt(0)}${'*'.repeat(data.name.length - 1)}` : null,
|
|
2473
|
+
|
|
2474
|
+
// Keep non-sensitive data
|
|
2475
|
+
created_at: data.createdAt,
|
|
2476
|
+
status: data.status,
|
|
2477
|
+
category: data.category,
|
|
2478
|
+
|
|
2479
|
+
// Add masking metadata
|
|
2480
|
+
masked_at: new Date(),
|
|
2481
|
+
masking_version: '1.0'
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
1861
2485
|
}
|
|
1862
2486
|
}
|
|
1863
2487
|
```
|
|
1864
2488
|
|
|
1865
|
-
|
|
2489
|
+
**Configuration Options:**
|
|
1866
2490
|
|
|
1867
|
-
|
|
2491
|
+
| Parameter | Type | Required | Description |
|
|
2492
|
+
|-----------|------|----------|-------------|
|
|
2493
|
+
| `connectionString` | string | Yes | PostgreSQL connection string |
|
|
2494
|
+
| `ssl` | object | No | SSL configuration options |
|
|
2495
|
+
| `pool` | object | No | Connection pool configuration |
|
|
2496
|
+
| `logTable` | string | No | Table name for operation audit logging |
|
|
1868
2497
|
|
|
1869
|
-
|
|
1870
|
-
// 1. Simple array (replicate to same name)
|
|
1871
|
-
resources: ['users', 'products']
|
|
2498
|
+
**Pool Configuration Options:**
|
|
1872
2499
|
|
|
1873
|
-
|
|
1874
|
-
|
|
2500
|
+
| Parameter | Type | Default | Description |
|
|
2501
|
+
|-----------|------|---------|-------------|
|
|
2502
|
+
| `max` | number | `10` | Maximum number of connections in pool |
|
|
2503
|
+
| `min` | number | `0` | Minimum number of connections in pool |
|
|
2504
|
+
| `idleTimeoutMillis` | number | `10000` | How long a client is allowed to remain idle |
|
|
2505
|
+
| `connectionTimeoutMillis` | number | `0` | Return an error after timeout |
|
|
2506
|
+
| `acquireTimeoutMillis` | number | `0` | Return an error if no connection available |
|
|
1875
2507
|
|
|
1876
|
-
|
|
2508
|
+
**Resource Configuration:**
|
|
2509
|
+
|
|
2510
|
+
Resources must be configured as arrays of table configurations:
|
|
2511
|
+
|
|
2512
|
+
```javascript
|
|
1877
2513
|
resources: {
|
|
1878
|
-
|
|
2514
|
+
resourceName: [
|
|
1879
2515
|
{
|
|
1880
|
-
|
|
1881
|
-
|
|
2516
|
+
table: 'destination_table',
|
|
2517
|
+
actions: ['insert', 'update', 'delete'],
|
|
2518
|
+
transform: (data, operation, beforeData) => ({
|
|
2519
|
+
// Return transformed data or null to skip
|
|
2520
|
+
})
|
|
1882
2521
|
}
|
|
1883
2522
|
]
|
|
1884
2523
|
}
|
|
2524
|
+
```
|
|
1885
2525
|
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
2526
|
+
**Automatic Features:**
|
|
2527
|
+
|
|
2528
|
+
- **๐ Connection Pooling**: Efficient database connection management
|
|
2529
|
+
- **๐ Audit Logging**: Optional audit trail in specified log table
|
|
2530
|
+
- **๐ก๏ธ Error Handling**: Graceful handling of connection failures and SQL errors
|
|
2531
|
+
- **โก Bulk Operations**: Optimized for high-throughput replication
|
|
2532
|
+
- **๐ฏ Flexible Actions**: Support for insert, update, delete operations
|
|
2533
|
+
- **๐ง Custom SQL**: Transform functions receive operation context for complex logic
|
|
2534
|
+
|
|
2535
|
+
### ๐ Monitoring & Health Checks
|
|
2536
|
+
|
|
2537
|
+
Monitor replication health and performance with built-in tools:
|
|
2538
|
+
|
|
2539
|
+
```javascript
|
|
2540
|
+
// Get replication status for all replicators
|
|
2541
|
+
const replicationStatus = await replicatorPlugin.getReplicationStats();
|
|
2542
|
+
|
|
2543
|
+
console.log('Replication Health:', {
|
|
2544
|
+
totalReplicators: replicationStatus.replicators.length,
|
|
2545
|
+
activeReplicators: replicationStatus.replicators.filter(r => r.status.enabled).length,
|
|
2546
|
+
lastSync: replicationStatus.lastSync,
|
|
2547
|
+
errorRate: replicationStatus.stats.errorRate
|
|
2548
|
+
});
|
|
2549
|
+
|
|
2550
|
+
// Check individual replicator health
|
|
2551
|
+
for (const replicator of replicationStatus.replicators) {
|
|
2552
|
+
console.log(`${replicator.driver} replicator:`, {
|
|
2553
|
+
enabled: replicator.status.enabled,
|
|
2554
|
+
connected: replicator.status.connected,
|
|
2555
|
+
lastActivity: replicator.status.lastActivity,
|
|
2556
|
+
totalOperations: replicator.status.totalOperations,
|
|
2557
|
+
errorCount: replicator.status.errorCount
|
|
2558
|
+
});
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// Get detailed replication logs
|
|
2562
|
+
const replicationLogs = await replicatorPlugin.getReplicationLogs({
|
|
2563
|
+
resourceName: 'users', // Filter by resource
|
|
2564
|
+
status: 'failed', // Show only failed replications
|
|
2565
|
+
limit: 50, // Limit results
|
|
2566
|
+
offset: 0 // Pagination
|
|
2567
|
+
});
|
|
2568
|
+
|
|
2569
|
+
console.log('Recent failures:', replicationLogs.map(log => ({
|
|
2570
|
+
timestamp: log.timestamp,
|
|
2571
|
+
resource: log.resourceName,
|
|
2572
|
+
operation: log.operation,
|
|
2573
|
+
error: log.error,
|
|
2574
|
+
replicator: log.replicator
|
|
2575
|
+
})));
|
|
2576
|
+
|
|
2577
|
+
// Retry failed replications
|
|
2578
|
+
const retryResult = await replicatorPlugin.retryFailedReplications();
|
|
2579
|
+
console.log(`Retried ${retryResult.retried} failed replications`);
|
|
2580
|
+
```
|
|
2581
|
+
|
|
2582
|
+
### ๐จ Troubleshooting Guide
|
|
2583
|
+
|
|
2584
|
+
#### Common Issues and Solutions
|
|
2585
|
+
|
|
2586
|
+
**1. Replication Not Happening**
|
|
2587
|
+
```javascript
|
|
2588
|
+
// Check if replicator is enabled
|
|
2589
|
+
const plugin = s3db.plugins.find(p => p.constructor.name === 'ReplicatorPlugin');
|
|
2590
|
+
console.log('Plugin enabled:', plugin.config.enabled);
|
|
2591
|
+
|
|
2592
|
+
// Check resource configuration
|
|
2593
|
+
console.log('Resources configured:', Object.keys(plugin.config.replicators[0].resources));
|
|
2594
|
+
|
|
2595
|
+
// Listen for debug events
|
|
2596
|
+
plugin.on('replicator_error', (error) => {
|
|
2597
|
+
console.error('Replication error:', error);
|
|
2598
|
+
});
|
|
2599
|
+
```
|
|
2600
|
+
|
|
2601
|
+
**2. Transform Function Errors**
|
|
2602
|
+
```javascript
|
|
2603
|
+
// Add error handling in transform functions
|
|
2604
|
+
transform: (data) => {
|
|
2605
|
+
try {
|
|
2606
|
+
return {
|
|
2607
|
+
...data,
|
|
2608
|
+
fullName: `${data.firstName} ${data.lastName}`,
|
|
2609
|
+
processedAt: new Date().toISOString()
|
|
2610
|
+
};
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
console.error('Transform error:', error, 'Data:', data);
|
|
2613
|
+
return data; // Fallback to original data
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
```
|
|
2617
|
+
|
|
2618
|
+
**3. Connection Issues**
|
|
2619
|
+
```javascript
|
|
2620
|
+
// Test replicator connections
|
|
2621
|
+
const testResults = await Promise.allSettled(
|
|
2622
|
+
replicatorPlugin.replicators.map(async (replicator) => {
|
|
2623
|
+
try {
|
|
2624
|
+
const result = await replicator.testConnection();
|
|
2625
|
+
return { replicator: replicator.id, success: result, error: null };
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
return { replicator: replicator.id, success: false, error: error.message };
|
|
2628
|
+
}
|
|
2629
|
+
})
|
|
2630
|
+
);
|
|
2631
|
+
|
|
2632
|
+
testResults.forEach(result => {
|
|
2633
|
+
if (result.status === 'fulfilled') {
|
|
2634
|
+
console.log(`${result.value.replicator}: ${result.value.success ? 'โ
' : 'โ'} ${result.value.error || ''}`);
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
```
|
|
2638
|
+
|
|
2639
|
+
**4. Performance Issues**
|
|
2640
|
+
```javascript
|
|
2641
|
+
// Monitor replication performance
|
|
2642
|
+
const performanceMonitor = setInterval(async () => {
|
|
2643
|
+
const stats = await replicatorPlugin.getReplicationStats();
|
|
2644
|
+
|
|
2645
|
+
console.log('Performance Metrics:', {
|
|
2646
|
+
operationsPerSecond: stats.operationsPerSecond,
|
|
2647
|
+
averageLatency: stats.averageLatency,
|
|
2648
|
+
queueSize: stats.queueSize,
|
|
2649
|
+
memoryUsage: process.memoryUsage()
|
|
2650
|
+
});
|
|
2651
|
+
|
|
2652
|
+
// Alert on performance degradation
|
|
2653
|
+
if (stats.averageLatency > 5000) { // 5 seconds
|
|
2654
|
+
console.warn('โ ๏ธ High replication latency detected:', stats.averageLatency + 'ms');
|
|
2655
|
+
}
|
|
2656
|
+
}, 30000); // Check every 30 seconds
|
|
2657
|
+
```
|
|
2658
|
+
|
|
2659
|
+
### ๐ฏ Advanced Use Cases
|
|
2660
|
+
|
|
2661
|
+
#### Multi-Environment Replication Pipeline
|
|
2662
|
+
|
|
2663
|
+
```javascript
|
|
2664
|
+
const replicatorPlugin = new ReplicatorPlugin({
|
|
2665
|
+
verbose: true,
|
|
2666
|
+
persistReplicatorLog: true,
|
|
2667
|
+
replicators: [
|
|
2668
|
+
// Production backup
|
|
2669
|
+
{
|
|
2670
|
+
driver: 's3db',
|
|
2671
|
+
config: { connectionString: process.env.BACKUP_DB_URL },
|
|
2672
|
+
resources: ['users', 'orders', 'products']
|
|
2673
|
+
},
|
|
2674
|
+
|
|
2675
|
+
// Staging environment sync
|
|
2676
|
+
{
|
|
2677
|
+
driver: 's3db',
|
|
2678
|
+
config: { connectionString: process.env.STAGING_DB_URL },
|
|
2679
|
+
resources: {
|
|
2680
|
+
users: {
|
|
2681
|
+
resource: 'users',
|
|
2682
|
+
transform: (data) => ({
|
|
2683
|
+
...data,
|
|
2684
|
+
// Remove PII for staging
|
|
2685
|
+
email: data.email?.replace(/(.{2})(.*)(@.*)/, '$1***$3'),
|
|
2686
|
+
phone: '***-***-' + (data.phone?.slice(-4) || '0000')
|
|
2687
|
+
})
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
},
|
|
2691
|
+
|
|
2692
|
+
// Analytics warehouse
|
|
2693
|
+
{
|
|
2694
|
+
driver: 'bigquery',
|
|
2695
|
+
config: {
|
|
2696
|
+
projectId: 'analytics-project',
|
|
2697
|
+
datasetId: 'production_data'
|
|
2698
|
+
},
|
|
2699
|
+
resources: {
|
|
2700
|
+
orders: [
|
|
2701
|
+
{ table: 'fact_orders', actions: ['insert'] },
|
|
2702
|
+
{ table: 'daily_revenue', actions: ['insert'], transform: aggregateDaily }
|
|
2703
|
+
]
|
|
2704
|
+
}
|
|
2705
|
+
},
|
|
2706
|
+
|
|
2707
|
+
// Real-time events
|
|
2708
|
+
{
|
|
2709
|
+
driver: 'sqs',
|
|
2710
|
+
config: {
|
|
2711
|
+
region: 'us-east-1',
|
|
2712
|
+
queues: {
|
|
2713
|
+
users: process.env.USER_EVENTS_QUEUE,
|
|
2714
|
+
orders: process.env.ORDER_EVENTS_QUEUE
|
|
2715
|
+
}
|
|
2716
|
+
},
|
|
2717
|
+
resources: ['users', 'orders']
|
|
2718
|
+
}
|
|
2719
|
+
]
|
|
2720
|
+
});
|
|
2721
|
+
```
|
|
2722
|
+
|
|
2723
|
+
#### Conditional Replication Based on Business Rules
|
|
2724
|
+
|
|
2725
|
+
```javascript
|
|
2726
|
+
const businessRulesReplicator = new ReplicatorPlugin({
|
|
2727
|
+
replicators: [
|
|
2728
|
+
{
|
|
2729
|
+
driver: 's3db',
|
|
2730
|
+
config: { connectionString: process.env.COMPLIANCE_DB_URL },
|
|
2731
|
+
resources: {
|
|
2732
|
+
users: {
|
|
2733
|
+
resource: 'compliant_users',
|
|
2734
|
+
transform: (data) => {
|
|
2735
|
+
// Only replicate users from certain regions
|
|
2736
|
+
const allowedRegions = ['US', 'EU', 'CA'];
|
|
2737
|
+
if (!allowedRegions.includes(data.region)) {
|
|
2738
|
+
return null; // Skip replication
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
// Apply data retention rules
|
|
2742
|
+
const accountAge = Date.now() - new Date(data.createdAt);
|
|
2743
|
+
const maxAge = 7 * 365 * 24 * 60 * 60 * 1000; // 7 years
|
|
2744
|
+
|
|
2745
|
+
if (accountAge > maxAge && data.status === 'inactive') {
|
|
2746
|
+
return null; // Skip old inactive accounts
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
return {
|
|
2750
|
+
...data,
|
|
2751
|
+
complianceVersion: '2.0',
|
|
2752
|
+
lastAudit: new Date().toISOString(),
|
|
2753
|
+
retentionCategory: accountAge > maxAge * 0.8 ? 'pending_review' : 'active'
|
|
2754
|
+
};
|
|
2755
|
+
}
|
|
2756
|
+
},
|
|
2757
|
+
|
|
2758
|
+
orders: {
|
|
2759
|
+
resource: 'audit_orders',
|
|
2760
|
+
transform: (data) => {
|
|
2761
|
+
// Only replicate high-value orders for compliance
|
|
2762
|
+
if (data.amount < 10000) return null;
|
|
2763
|
+
|
|
2764
|
+
return {
|
|
2765
|
+
order_id: data.id,
|
|
2766
|
+
customer_id: data.userId,
|
|
2767
|
+
amount: data.amount,
|
|
2768
|
+
currency: data.currency,
|
|
2769
|
+
order_date: data.createdAt,
|
|
2770
|
+
compliance_flag: 'high_value',
|
|
2771
|
+
audit_required: true,
|
|
2772
|
+
retention_years: 10
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
1891
2778
|
]
|
|
2779
|
+
});
|
|
2780
|
+
```
|
|
2781
|
+
|
|
2782
|
+
#### Event-Driven Architecture Integration
|
|
2783
|
+
|
|
2784
|
+
```javascript
|
|
2785
|
+
// Custom event handlers for complex workflows
|
|
2786
|
+
replicatorPlugin.on('replicated', async (event) => {
|
|
2787
|
+
const { resourceName, operation, recordId, replicator } = event;
|
|
2788
|
+
|
|
2789
|
+
// Trigger downstream processes
|
|
2790
|
+
if (resourceName === 'orders' && operation === 'insert') {
|
|
2791
|
+
// Trigger inventory update
|
|
2792
|
+
await inventoryService.updateStock(event.data);
|
|
2793
|
+
|
|
2794
|
+
// Send confirmation email
|
|
2795
|
+
await emailService.sendOrderConfirmation(event.data);
|
|
2796
|
+
|
|
2797
|
+
// Update analytics dashboard
|
|
2798
|
+
await analyticsService.recordSale(event.data);
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
if (resourceName === 'users' && operation === 'update') {
|
|
2802
|
+
// Check for important profile changes
|
|
2803
|
+
const criticalFields = ['email', 'phone', 'address'];
|
|
2804
|
+
const changedFields = Object.keys(event.data);
|
|
2805
|
+
|
|
2806
|
+
if (criticalFields.some(field => changedFields.includes(field))) {
|
|
2807
|
+
await auditService.logCriticalChange({
|
|
2808
|
+
userId: recordId,
|
|
2809
|
+
changedFields: changedFields.filter(f => criticalFields.includes(f)),
|
|
2810
|
+
timestamp: new Date()
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
});
|
|
2815
|
+
|
|
2816
|
+
// Handle replication failures with custom logic
|
|
2817
|
+
replicatorPlugin.on('replicator_error', async (error) => {
|
|
2818
|
+
const { resourceName, operation, recordId, replicator, error: errorMessage } = error;
|
|
2819
|
+
|
|
2820
|
+
// Log to external monitoring system
|
|
2821
|
+
await monitoringService.logError({
|
|
2822
|
+
service: 'replication',
|
|
2823
|
+
error: errorMessage,
|
|
2824
|
+
context: { resourceName, operation, recordId, replicator }
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
// Send alerts for critical resources
|
|
2828
|
+
const criticalResources = ['users', 'orders', 'payments'];
|
|
2829
|
+
if (criticalResources.includes(resourceName)) {
|
|
2830
|
+
await alertingService.sendAlert({
|
|
2831
|
+
severity: 'high',
|
|
2832
|
+
message: `Critical replication failure: ${resourceName}`,
|
|
2833
|
+
details: error
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
// Implement circuit breaker pattern
|
|
2838
|
+
const errorCount = await redis.incr(`replication_errors:${replicator}`);
|
|
2839
|
+
await redis.expire(`replication_errors:${replicator}`, 3600); // 1 hour window
|
|
2840
|
+
|
|
2841
|
+
if (errorCount > 10) {
|
|
2842
|
+
console.warn(`โ ๏ธ Circuit breaker: Disabling ${replicator} due to high error rate`);
|
|
2843
|
+
// Temporarily disable problematic replicator
|
|
2844
|
+
const problematicReplicator = replicatorPlugin.replicators.find(r => r.id === replicator);
|
|
2845
|
+
if (problematicReplicator) {
|
|
2846
|
+
problematicReplicator.enabled = false;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
});
|
|
2850
|
+
```
|
|
2851
|
+
|
|
2852
|
+
### ๐๏ธ Performance Tuning
|
|
2853
|
+
|
|
2854
|
+
#### Optimize Replication Performance
|
|
2855
|
+
|
|
2856
|
+
```javascript
|
|
2857
|
+
// Configure for high-throughput scenarios
|
|
2858
|
+
const highPerformanceReplicator = new ReplicatorPlugin({
|
|
2859
|
+
batchSize: 500, // Larger batches for better throughput
|
|
2860
|
+
maxRetries: 5, // More retries for reliability
|
|
2861
|
+
timeout: 60000, // Longer timeout for large operations
|
|
2862
|
+
|
|
2863
|
+
replicators: [
|
|
2864
|
+
{
|
|
2865
|
+
driver: 'bigquery',
|
|
2866
|
+
config: {
|
|
2867
|
+
projectId: 'analytics',
|
|
2868
|
+
datasetId: 'high_volume_data',
|
|
2869
|
+
// Use streaming inserts for real-time data
|
|
2870
|
+
streamingInserts: true,
|
|
2871
|
+
insertAllTimeout: 30000
|
|
2872
|
+
},
|
|
2873
|
+
resources: {
|
|
2874
|
+
events: {
|
|
2875
|
+
table: 'raw_events',
|
|
2876
|
+
actions: ['insert'],
|
|
2877
|
+
// Optimize transform for performance
|
|
2878
|
+
transform: (data) => {
|
|
2879
|
+
// Pre-compute expensive operations
|
|
2880
|
+
const eventDate = data.timestamp?.split('T')[0];
|
|
2881
|
+
|
|
2882
|
+
return {
|
|
2883
|
+
event_id: data.id,
|
|
2884
|
+
user_id: data.userId,
|
|
2885
|
+
event_type: data.type,
|
|
2886
|
+
event_date: eventDate,
|
|
2887
|
+
properties: JSON.stringify(data.properties || {}),
|
|
2888
|
+
|
|
2889
|
+
// Batch-friendly fields
|
|
2890
|
+
partition_date: eventDate,
|
|
2891
|
+
ingestion_timestamp: Math.floor(Date.now() / 1000) // Unix timestamp
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
},
|
|
2897
|
+
|
|
2898
|
+
{
|
|
2899
|
+
driver: 'postgres',
|
|
2900
|
+
config: {
|
|
2901
|
+
connectionString: process.env.POSTGRES_URL,
|
|
2902
|
+
pool: {
|
|
2903
|
+
max: 50, // Larger connection pool
|
|
2904
|
+
min: 10,
|
|
2905
|
+
idleTimeoutMillis: 60000,
|
|
2906
|
+
acquireTimeoutMillis: 10000
|
|
2907
|
+
}
|
|
2908
|
+
},
|
|
2909
|
+
resources: {
|
|
2910
|
+
users: [{
|
|
2911
|
+
table: 'users_cache',
|
|
2912
|
+
actions: ['insert', 'update'],
|
|
2913
|
+
// Use upsert pattern for better performance
|
|
2914
|
+
upsertMode: true,
|
|
2915
|
+
transform: (data) => ({
|
|
2916
|
+
id: data.id,
|
|
2917
|
+
email: data.email,
|
|
2918
|
+
name: data.name,
|
|
2919
|
+
updated_at: new Date(),
|
|
2920
|
+
|
|
2921
|
+
// Optimized for query performance
|
|
2922
|
+
search_vector: `${data.name} ${data.email}`.toLowerCase()
|
|
2923
|
+
})
|
|
2924
|
+
}]
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
]
|
|
2928
|
+
});
|
|
2929
|
+
```
|
|
2930
|
+
|
|
2931
|
+
### ๐ Security Best Practices
|
|
2932
|
+
|
|
2933
|
+
#### Secure Replication Configuration
|
|
2934
|
+
|
|
2935
|
+
```javascript
|
|
2936
|
+
const secureReplicator = new ReplicatorPlugin({
|
|
2937
|
+
replicators: [
|
|
2938
|
+
{
|
|
2939
|
+
driver: 'bigquery',
|
|
2940
|
+
config: {
|
|
2941
|
+
projectId: 'secure-analytics',
|
|
2942
|
+
datasetId: 'encrypted_data',
|
|
2943
|
+
// Use service account with minimal permissions
|
|
2944
|
+
credentials: {
|
|
2945
|
+
type: 'service_account',
|
|
2946
|
+
project_id: 'secure-analytics',
|
|
2947
|
+
private_key: process.env.BIGQUERY_PRIVATE_KEY,
|
|
2948
|
+
client_email: 'replication-service@secure-analytics.iam.gserviceaccount.com'
|
|
2949
|
+
}
|
|
2950
|
+
},
|
|
2951
|
+
resources: {
|
|
2952
|
+
users: {
|
|
2953
|
+
table: 'encrypted_users',
|
|
2954
|
+
actions: ['insert', 'update'],
|
|
2955
|
+
transform: (data) => ({
|
|
2956
|
+
// Hash sensitive identifiers
|
|
2957
|
+
user_hash: crypto.createHash('sha256').update(data.id + process.env.SALT).digest('hex'),
|
|
2958
|
+
|
|
2959
|
+
// Encrypt PII fields
|
|
2960
|
+
encrypted_email: encrypt(data.email),
|
|
2961
|
+
encrypted_phone: data.phone ? encrypt(data.phone) : null,
|
|
2962
|
+
|
|
2963
|
+
// Keep non-sensitive analytics data
|
|
2964
|
+
registration_date: data.createdAt?.split('T')[0],
|
|
2965
|
+
account_type: data.accountType,
|
|
2966
|
+
region: data.region,
|
|
2967
|
+
|
|
2968
|
+
// Add encryption metadata
|
|
2969
|
+
encryption_version: '1.0',
|
|
2970
|
+
processed_at: new Date().toISOString()
|
|
2971
|
+
})
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
]
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
// Encryption utility functions
|
|
2979
|
+
function encrypt(text) {
|
|
2980
|
+
if (!text) return null;
|
|
2981
|
+
const cipher = crypto.createCipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
|
|
2982
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
2983
|
+
encrypted += cipher.final('hex');
|
|
2984
|
+
return encrypted;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
function decrypt(encryptedText) {
|
|
2988
|
+
if (!encryptedText) return null;
|
|
2989
|
+
const decipher = crypto.createDecipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
|
|
2990
|
+
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
|
|
2991
|
+
decrypted += decipher.final('utf8');
|
|
2992
|
+
return decrypted;
|
|
1892
2993
|
}
|
|
1893
2994
|
```
|
|
1894
2995
|
|
|
2996
|
+
### โ
Best Practices Summary
|
|
2997
|
+
|
|
2998
|
+
1. **Configuration Management**
|
|
2999
|
+
- Use environment variables for connection strings and credentials
|
|
3000
|
+
- Implement proper error handling in transform functions
|
|
3001
|
+
- Test replicator connections during application startup
|
|
3002
|
+
|
|
3003
|
+
2. **Performance Optimization**
|
|
3004
|
+
- Use appropriate batch sizes for your data volume
|
|
3005
|
+
- Configure connection pools for database replicators
|
|
3006
|
+
- Monitor replication lag and throughput
|
|
3007
|
+
|
|
3008
|
+
3. **Security & Compliance**
|
|
3009
|
+
- Encrypt sensitive data before replication
|
|
3010
|
+
- Implement data masking for non-production environments
|
|
3011
|
+
- Use minimal privilege service accounts
|
|
3012
|
+
|
|
3013
|
+
4. **Monitoring & Alerting**
|
|
3014
|
+
- Set up alerts for replication failures
|
|
3015
|
+
- Monitor replication lag and error rates
|
|
3016
|
+
- Implement health checks for all replicators
|
|
3017
|
+
|
|
3018
|
+
5. **Error Handling**
|
|
3019
|
+
- Implement circuit breakers for unreliable targets
|
|
3020
|
+
- Use dead letter queues for failed messages
|
|
3021
|
+
- Log detailed error information for debugging
|
|
3022
|
+
|
|
1895
3023
|
### ๐ง Easy Example
|
|
1896
3024
|
|
|
1897
3025
|
```javascript
|