s3db.js 9.2.2 → 10.0.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 +71 -13
- package/dist/s3db.cjs.js +466 -8
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.es.js +466 -9
- package/dist/s3db.es.js.map +1 -1
- package/mcp/server.js +12 -8
- package/package.json +4 -4
- package/src/client.class.js +2 -2
- package/src/concerns/high-performance-inserter.js +285 -0
- package/src/concerns/partition-queue.js +171 -0
- package/src/errors.js +10 -2
- package/src/partition-drivers/base-partition-driver.js +96 -0
- package/src/partition-drivers/index.js +60 -0
- package/src/partition-drivers/memory-partition-driver.js +274 -0
- package/src/partition-drivers/sqs-partition-driver.js +332 -0
- package/src/partition-drivers/sync-partition-driver.js +38 -0
- package/src/plugins/backup.plugin.js +1 -1
- package/src/plugins/backup.plugin.js.backup +1 -1
- package/src/plugins/eventual-consistency.plugin.js +609 -0
- package/src/plugins/index.js +1 -0
- package/PLUGINS.md +0 -5036
package/PLUGINS.md
DELETED
|
@@ -1,5036 +0,0 @@
|
|
|
1
|
-
# 🔌 s3db.js Plugins Documentation
|
|
2
|
-
|
|
3
|
-
<p align="center">
|
|
4
|
-
<strong>Comprehensive guide to all s3db.js plugins</strong><br>
|
|
5
|
-
<em>Extend your database with powerful features</em>
|
|
6
|
-
</p>
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## 📋 Table of Contents
|
|
11
|
-
|
|
12
|
-
- [🚀 Getting Started](#-getting-started-with-plugins)
|
|
13
|
-
- [🧩 Available Plugins](#-available-plugins)
|
|
14
|
-
- [💾 Cache Plugin](#-cache-plugin)
|
|
15
|
-
- [💰 Costs Plugin](#-costs-plugin)
|
|
16
|
-
- [📝 Audit Plugin](#-audit-plugin)
|
|
17
|
-
- [🔍 FullText Plugin](#-fulltext-plugin)
|
|
18
|
-
- [📊 Metrics Plugin](#-metrics-plugin)
|
|
19
|
-
- [🔄 Replicator Plugin](#-replicator-plugin)
|
|
20
|
-
- [📬 Queue Consumer Plugin](#-queue-consumer-plugin)
|
|
21
|
-
- [🤖 State Machine Plugin](#-state-machine-plugin)
|
|
22
|
-
- [💾 Backup Plugin](#-backup-plugin)
|
|
23
|
-
- [⏰ Scheduler Plugin](#-scheduler-plugin)
|
|
24
|
-
- [🔧 Plugin Development](#-plugin-development)
|
|
25
|
-
- [💡 Plugin Combinations](#-plugin-combinations)
|
|
26
|
-
- [🎯 Best Practices](#-best-practices)
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## 🚀 Getting Started with Plugins
|
|
31
|
-
|
|
32
|
-
Plugins extend s3db.js with additional functionality using a **driver-based architecture**. They can be used individually or combined for powerful workflows.
|
|
33
|
-
|
|
34
|
-
### Plugin Architecture
|
|
35
|
-
|
|
36
|
-
Most s3db.js plugins follow a **driver pattern** where you specify:
|
|
37
|
-
- **`driver`**: The storage/connection type (`filesystem`, `s3`, `multi`, etc.)
|
|
38
|
-
- **`config`**: Driver-specific configuration options
|
|
39
|
-
- **Plugin options**: Global settings that apply across drivers
|
|
40
|
-
|
|
41
|
-
### Basic Plugin Usage
|
|
42
|
-
|
|
43
|
-
```javascript
|
|
44
|
-
import { S3db, CachePlugin, BackupPlugin, CostsPlugin } from 's3db.js';
|
|
45
|
-
|
|
46
|
-
const s3db = new S3db({
|
|
47
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
await s3db.connect();
|
|
51
|
-
|
|
52
|
-
// Driver-based plugins (most common)
|
|
53
|
-
await s3db.usePlugin(new CachePlugin({
|
|
54
|
-
driver: 'memory',
|
|
55
|
-
config: { maxSize: 1000 }
|
|
56
|
-
}));
|
|
57
|
-
|
|
58
|
-
await s3db.usePlugin(new BackupPlugin({
|
|
59
|
-
driver: 'filesystem',
|
|
60
|
-
config: { path: './backups/{date}/' }
|
|
61
|
-
}));
|
|
62
|
-
|
|
63
|
-
// Static utility plugins
|
|
64
|
-
await s3db.usePlugin(CostsPlugin);
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Driver-Based Configuration Pattern
|
|
68
|
-
|
|
69
|
-
```javascript
|
|
70
|
-
// Single driver example
|
|
71
|
-
new SomePlugin({
|
|
72
|
-
driver: 'driverType',
|
|
73
|
-
config: {
|
|
74
|
-
// Driver-specific options
|
|
75
|
-
option1: 'value1',
|
|
76
|
-
option2: 'value2'
|
|
77
|
-
},
|
|
78
|
-
// Global plugin options
|
|
79
|
-
verbose: true,
|
|
80
|
-
timeout: 30000
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Multi-driver example
|
|
84
|
-
new SomePlugin({
|
|
85
|
-
driver: 'multi',
|
|
86
|
-
config: {
|
|
87
|
-
strategy: 'all',
|
|
88
|
-
destinations: [
|
|
89
|
-
{ driver: 'driver1', config: {...} },
|
|
90
|
-
{ driver: 'driver2', config: {...} }
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
### Plugin Types
|
|
97
|
-
|
|
98
|
-
- **Instance Plugins**: Require `new` - `new CachePlugin(config)`
|
|
99
|
-
- **Static Plugins**: Used directly - `CostsPlugin`
|
|
100
|
-
- **Configurable**: Accept options for customization
|
|
101
|
-
- **Event-Driven**: Emit events for monitoring and integration
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## 🧩 Available Plugins
|
|
106
|
-
|
|
107
|
-
## 💾 Cache Plugin
|
|
108
|
-
|
|
109
|
-
**Driver-Based Caching System** - Intelligent caching that reduces S3 API calls and improves performance using configurable storage drivers.
|
|
110
|
-
|
|
111
|
-
> 🏎️ **Performance**: Dramatically reduces S3 costs and latency by caching frequently accessed data.
|
|
112
|
-
|
|
113
|
-
### 🚀 Quick Start
|
|
114
|
-
|
|
115
|
-
#### Memory Driver (Fast & Temporary)
|
|
116
|
-
```javascript
|
|
117
|
-
import { S3db, CachePlugin } from 's3db.js';
|
|
118
|
-
|
|
119
|
-
const s3db = new S3db({
|
|
120
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
121
|
-
plugins: [
|
|
122
|
-
new CachePlugin({
|
|
123
|
-
driver: 'memory',
|
|
124
|
-
ttl: 300000, // 5 minutes
|
|
125
|
-
maxSize: 1000, // Max 1000 items
|
|
126
|
-
config: {
|
|
127
|
-
evictionPolicy: 'lru',
|
|
128
|
-
enableStats: true
|
|
129
|
-
}
|
|
130
|
-
})
|
|
131
|
-
]
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
await s3db.connect();
|
|
135
|
-
|
|
136
|
-
// Cache automatically intercepts read operations
|
|
137
|
-
const users = s3db.resource('users');
|
|
138
|
-
await users.count(); // ⚡ Cached for 5 minutes
|
|
139
|
-
await users.list(); // ⚡ Cached result
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
#### S3 Driver (Persistent & Shared)
|
|
143
|
-
```javascript
|
|
144
|
-
const s3db = new S3db({
|
|
145
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
146
|
-
plugins: [
|
|
147
|
-
new CachePlugin({
|
|
148
|
-
driver: 's3',
|
|
149
|
-
ttl: 1800000, // 30 minutes
|
|
150
|
-
config: {
|
|
151
|
-
bucket: 'my-cache-bucket', // Optional: separate bucket
|
|
152
|
-
keyPrefix: 'cache/', // Cache key prefix
|
|
153
|
-
storageClass: 'STANDARD' // S3 storage class
|
|
154
|
-
}
|
|
155
|
-
})
|
|
156
|
-
]
|
|
157
|
-
});
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
#### Filesystem Driver (Local & Fast)
|
|
161
|
-
```javascript
|
|
162
|
-
const s3db = new S3db({
|
|
163
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
164
|
-
plugins: [
|
|
165
|
-
new CachePlugin({
|
|
166
|
-
driver: 'filesystem',
|
|
167
|
-
config: {
|
|
168
|
-
path: './cache',
|
|
169
|
-
partitionAware: true,
|
|
170
|
-
partitionStrategy: 'hierarchical'
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
]
|
|
174
|
-
});
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### ⚙️ Configuration Parameters
|
|
178
|
-
|
|
179
|
-
| Parameter | Type | Default | Description |
|
|
180
|
-
|-----------|------|---------|-------------|
|
|
181
|
-
| `driver` | string | `'s3'` | Cache driver: `'memory'`, `'s3'`, or `'filesystem'` |
|
|
182
|
-
| `ttl` | number | `300000` | Time-to-live in milliseconds (5 minutes) - global setting |
|
|
183
|
-
| `maxSize` | number | `1000` | Maximum number of items in cache - global setting |
|
|
184
|
-
| `config` | object | `{}` | Driver-specific configuration options (can override global settings) |
|
|
185
|
-
| `includePartitions` | boolean | `true` | Include partition values in cache keys |
|
|
186
|
-
| `partitionAware` | boolean | `false` | Use partition-aware filesystem cache |
|
|
187
|
-
| `partitionStrategy` | string | `'hierarchical'` | Partition strategy: `'hierarchical'`, `'flat'`, `'temporal'` |
|
|
188
|
-
| `trackUsage` | boolean | `true` | Track partition usage statistics |
|
|
189
|
-
| `preloadRelated` | boolean | `false` | Preload related partition data |
|
|
190
|
-
|
|
191
|
-
**Configuration Priority:** Driver-specific `config` options override global plugin settings. For example, if you set `ttl: 600000` at the plugin level and `ttl: 1800000` in the driver config, the driver will use 1800000 (30 minutes) while the global setting serves as the default for any drivers that don't specify their own TTL.
|
|
192
|
-
|
|
193
|
-
### Driver Configuration Options
|
|
194
|
-
|
|
195
|
-
The `config` object contains driver-specific options. Note that `ttl` and `maxSize` can be configured at the plugin level (applies to all operations) or in the driver config (driver-specific override).
|
|
196
|
-
|
|
197
|
-
#### Memory Driver (`driver: 'memory'`)
|
|
198
|
-
|
|
199
|
-
| Parameter | Type | Default | Description |
|
|
200
|
-
|-----------|------|---------|-------------|
|
|
201
|
-
| `ttl` | number | inherited | TTL override for memory cache (inherits from plugin level) |
|
|
202
|
-
| `maxSize` | number | inherited | Max items override for memory cache (inherits from plugin level) |
|
|
203
|
-
| `enableStats` | boolean | `false` | Whether to track cache statistics (hits, misses, etc.) |
|
|
204
|
-
| `evictionPolicy` | string | `'lru'` | Cache eviction policy: `'lru'` (Least Recently Used) or `'fifo'` (First In First Out) |
|
|
205
|
-
| `logEvictions` | boolean | `false` | Whether to log when items are evicted from cache |
|
|
206
|
-
| `cleanupInterval` | number | `60000` | Interval in milliseconds to run cleanup of expired items (1 minute default) |
|
|
207
|
-
| `caseSensitive` | boolean | `true` | Whether cache keys are case sensitive |
|
|
208
|
-
| `enableCompression` | boolean | `false` | Whether to compress values using gzip (requires zlib) |
|
|
209
|
-
| `compressionThreshold` | number | `1024` | Minimum size in bytes to trigger compression |
|
|
210
|
-
| `tags` | object | `{}` | Default tags to apply to all cached items |
|
|
211
|
-
| `persistent` | boolean | `false` | Whether to persist cache to disk (experimental) |
|
|
212
|
-
| `persistencePath` | string | `'./cache'` | Directory path for persistent cache storage |
|
|
213
|
-
| `persistenceInterval` | number | `300000` | Interval in milliseconds to save cache to disk (5 minutes default) |
|
|
214
|
-
|
|
215
|
-
#### S3 Driver (`driver: 's3'`)
|
|
216
|
-
|
|
217
|
-
| Parameter | Type | Default | Description |
|
|
218
|
-
|-----------|------|---------|-------------|
|
|
219
|
-
| `ttl` | number | inherited | TTL override for S3 cache (inherits from plugin level) |
|
|
220
|
-
| `keyPrefix` | string | `'cache'` | S3 key prefix for cache objects |
|
|
221
|
-
| `client` | object | Database client | Custom S3 client instance |
|
|
222
|
-
|
|
223
|
-
**Note:** S3 cache automatically uses gzip compression for all cached values.
|
|
224
|
-
|
|
225
|
-
#### Filesystem Driver (`driver: 'filesystem'`)
|
|
226
|
-
|
|
227
|
-
| Parameter | Type | Default | Description |
|
|
228
|
-
|-----------|------|---------|-------------|
|
|
229
|
-
| `directory` | string | required | Directory path to store cache files |
|
|
230
|
-
| `ttl` | number | inherited | TTL override for filesystem cache (inherits from plugin level) |
|
|
231
|
-
| `prefix` | string | `'cache'` | Prefix for cache filenames |
|
|
232
|
-
| `enableCompression` | boolean | `true` | Whether to compress cache values using gzip |
|
|
233
|
-
| `compressionThreshold` | number | `1024` | Minimum size in bytes to trigger compression |
|
|
234
|
-
| `createDirectory` | boolean | `true` | Whether to create the directory if it doesn't exist |
|
|
235
|
-
| `fileExtension` | string | `'.cache'` | File extension for cache files |
|
|
236
|
-
| `enableMetadata` | boolean | `true` | Whether to store metadata alongside cache data |
|
|
237
|
-
| `maxFileSize` | number | `10485760` | Maximum file size in bytes (10MB) |
|
|
238
|
-
| `enableStats` | boolean | `false` | Whether to track cache statistics |
|
|
239
|
-
| `enableCleanup` | boolean | `true` | Whether to automatically clean up expired files |
|
|
240
|
-
| `cleanupInterval` | number | `300000` | Interval in milliseconds to run cleanup (5 minutes) |
|
|
241
|
-
| `encoding` | string | `'utf8'` | File encoding to use |
|
|
242
|
-
| `fileMode` | number | `0o644` | File permissions in octal notation |
|
|
243
|
-
| `enableBackup` | boolean | `false` | Whether to create backup files before overwriting |
|
|
244
|
-
| `backupSuffix` | string | `'.bak'` | Suffix for backup files |
|
|
245
|
-
| `enableLocking` | boolean | `false` | Whether to use file locking to prevent concurrent access |
|
|
246
|
-
| `lockTimeout` | number | `5000` | Lock timeout in milliseconds |
|
|
247
|
-
| `enableJournal` | boolean | `false` | Whether to maintain a journal of operations |
|
|
248
|
-
| `journalFile` | string | `'cache.journal'` | Journal filename |
|
|
249
|
-
|
|
250
|
-
#### Partition-Aware Filesystem Driver (when `partitionAware: true`)
|
|
251
|
-
|
|
252
|
-
| Parameter | Type | Default | Description |
|
|
253
|
-
|-----------|------|---------|-------------|
|
|
254
|
-
| `partitionStrategy` | string | `'hierarchical'` | Partition strategy: `'hierarchical'`, `'flat'`, `'temporal'` |
|
|
255
|
-
| `trackUsage` | boolean | `true` | Track partition usage statistics |
|
|
256
|
-
| `preloadRelated` | boolean | `false` | Preload related partition data |
|
|
257
|
-
| `preloadThreshold` | number | `10` | Minimum usage count to trigger preloading |
|
|
258
|
-
| `maxCacheSize` | string/null | `null` | Maximum cache size (e.g., `'1GB'`, `'500MB'`) |
|
|
259
|
-
| `usageStatsFile` | string | `'partition-usage.json'` | File to store usage statistics |
|
|
260
|
-
|
|
261
|
-
**Note:** All Filesystem Driver parameters also apply to Partition-Aware Filesystem Driver.
|
|
262
|
-
|
|
263
|
-
### 🔧 Easy Example
|
|
264
|
-
|
|
265
|
-
```javascript
|
|
266
|
-
import { S3db, CachePlugin } from 's3db.js';
|
|
267
|
-
|
|
268
|
-
// Memory cache example with global settings
|
|
269
|
-
const s3db = new S3db({
|
|
270
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
271
|
-
plugins: [new CachePlugin({
|
|
272
|
-
driver: 'memory',
|
|
273
|
-
ttl: 600000, // 10 minutes - applies to all cache operations
|
|
274
|
-
maxSize: 500, // 500 items max - applies to all cache operations
|
|
275
|
-
config: {
|
|
276
|
-
enableStats: true,
|
|
277
|
-
evictionPolicy: 'lru'
|
|
278
|
-
}
|
|
279
|
-
})]
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
// Filesystem cache example with driver-specific override
|
|
283
|
-
const s3dbWithFileCache = new S3db({
|
|
284
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
285
|
-
plugins: [new CachePlugin({
|
|
286
|
-
driver: 'filesystem',
|
|
287
|
-
ttl: 900000, // 15 minutes - global default
|
|
288
|
-
maxSize: 2000, // 2000 items max - global default
|
|
289
|
-
config: {
|
|
290
|
-
directory: './cache',
|
|
291
|
-
ttl: 1800000, // 30 minutes - overrides global for filesystem only
|
|
292
|
-
enableCompression: true,
|
|
293
|
-
enableCleanup: true,
|
|
294
|
-
enableMetadata: true
|
|
295
|
-
}
|
|
296
|
-
})]
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
// S3 cache example
|
|
300
|
-
const s3dbWithS3Cache = new S3db({
|
|
301
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
302
|
-
plugins: [new CachePlugin({
|
|
303
|
-
driver: 's3',
|
|
304
|
-
ttl: 3600000, // 1 hour
|
|
305
|
-
config: {
|
|
306
|
-
keyPrefix: 'app-cache'
|
|
307
|
-
}
|
|
308
|
-
})]
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
await s3db.connect();
|
|
312
|
-
|
|
313
|
-
const products = s3db.resource('products');
|
|
314
|
-
|
|
315
|
-
// First call hits the database
|
|
316
|
-
console.time('First call');
|
|
317
|
-
const result1 = await products.count();
|
|
318
|
-
console.timeEnd('First call'); // ~200ms
|
|
319
|
-
|
|
320
|
-
// Second call uses cache
|
|
321
|
-
console.time('Cached call');
|
|
322
|
-
const result2 = await products.count();
|
|
323
|
-
console.timeEnd('Cached call'); // ~2ms
|
|
324
|
-
|
|
325
|
-
// Cache is automatically cleared on write operations
|
|
326
|
-
await products.insert({ name: 'New Product', price: 29.99 });
|
|
327
|
-
|
|
328
|
-
// Next call will hit database again (cache cleared)
|
|
329
|
-
const result3 = await products.count(); // Fresh data
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
### 🚀 Advanced Configuration Example
|
|
333
|
-
|
|
334
|
-
```javascript
|
|
335
|
-
import { S3db, CachePlugin } from 's3db.js';
|
|
336
|
-
|
|
337
|
-
// Advanced cache configuration with partition-aware filesystem cache
|
|
338
|
-
const s3dbWithAdvancedCache = new S3db({
|
|
339
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
340
|
-
plugins: [new CachePlugin({
|
|
341
|
-
driver: 'filesystem',
|
|
342
|
-
|
|
343
|
-
// Global cache settings (apply to all operations)
|
|
344
|
-
ttl: 3600000, // 1 hour default
|
|
345
|
-
maxSize: 5000, // 5000 items max
|
|
346
|
-
includePartitions: true,
|
|
347
|
-
partitionAware: true, // Enable partition-aware caching
|
|
348
|
-
partitionStrategy: 'hierarchical',
|
|
349
|
-
trackUsage: true,
|
|
350
|
-
preloadRelated: true,
|
|
351
|
-
|
|
352
|
-
// Driver-specific configuration
|
|
353
|
-
config: {
|
|
354
|
-
directory: './data/cache',
|
|
355
|
-
prefix: 'app-cache',
|
|
356
|
-
ttl: 7200000, // 2 hours - overrides global TTL for filesystem
|
|
357
|
-
enableCompression: true,
|
|
358
|
-
compressionThreshold: 512, // Compress files > 512 bytes
|
|
359
|
-
enableCleanup: true,
|
|
360
|
-
cleanupInterval: 600000, // 10 minutes
|
|
361
|
-
enableMetadata: true,
|
|
362
|
-
maxFileSize: 5242880, // 5MB per file
|
|
363
|
-
enableStats: true,
|
|
364
|
-
fileMode: 0o644,
|
|
365
|
-
encoding: 'utf8',
|
|
366
|
-
enableBackup: true,
|
|
367
|
-
enableLocking: true,
|
|
368
|
-
lockTimeout: 3000
|
|
369
|
-
}
|
|
370
|
-
})]
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
// Memory cache with advanced features
|
|
374
|
-
const s3dbWithMemoryCache = new S3db({
|
|
375
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
376
|
-
plugins: [new CachePlugin({
|
|
377
|
-
driver: 'memory',
|
|
378
|
-
ttl: 600000, // 10 minutes - global TTL
|
|
379
|
-
maxSize: 5000, // 5000 items max - global limit
|
|
380
|
-
includePartitions: true,
|
|
381
|
-
config: {
|
|
382
|
-
enableStats: true,
|
|
383
|
-
evictionPolicy: 'lru',
|
|
384
|
-
logEvictions: true,
|
|
385
|
-
enableCompression: true,
|
|
386
|
-
compressionThreshold: 1024,
|
|
387
|
-
tags: { environment: 'production' }
|
|
388
|
-
}
|
|
389
|
-
})]
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// S3 cache with custom prefix
|
|
393
|
-
const s3dbWithS3Cache = new S3db({
|
|
394
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
395
|
-
plugins: [new CachePlugin({
|
|
396
|
-
driver: 's3',
|
|
397
|
-
ttl: 3600000, // 1 hour - global TTL
|
|
398
|
-
includePartitions: true,
|
|
399
|
-
config: {
|
|
400
|
-
keyPrefix: 'app-cache'
|
|
401
|
-
}
|
|
402
|
-
})]
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
await s3dbWithAdvancedCache.connect();
|
|
406
|
-
|
|
407
|
-
// Access cache methods on resources
|
|
408
|
-
const users = s3dbWithAdvancedCache.resource('users');
|
|
409
|
-
|
|
410
|
-
// Generate custom cache keys
|
|
411
|
-
const cacheKey = await users.cacheKeyFor({
|
|
412
|
-
action: 'list',
|
|
413
|
-
params: { limit: 10 },
|
|
414
|
-
partition: 'byStatus',
|
|
415
|
-
partitionValues: { status: 'active' }
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// Manual cache operations
|
|
419
|
-
await users.cache.set(cacheKey, data);
|
|
420
|
-
const cached = await users.cache.get(cacheKey);
|
|
421
|
-
await users.cache.delete(cacheKey);
|
|
422
|
-
await users.cache.clear(); // Clear all cache
|
|
423
|
-
|
|
424
|
-
// Partition-aware cache operations (if using partition-aware cache)
|
|
425
|
-
if (users.cache.clearPartition) {
|
|
426
|
-
await users.cache.clearPartition('byStatus', { status: 'active' });
|
|
427
|
-
const stats = await users.cache.getPartitionStats('byStatus');
|
|
428
|
-
console.log('Partition stats:', stats);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Cache statistics (if enabled)
|
|
432
|
-
if (users.cache.stats) {
|
|
433
|
-
const stats = users.cache.stats();
|
|
434
|
-
console.log('Cache hit rate:', stats.hitRate);
|
|
435
|
-
console.log('Total hits:', stats.hits);
|
|
436
|
-
console.log('Total misses:', stats.misses);
|
|
437
|
-
}
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
---
|
|
441
|
-
|
|
442
|
-
## 💰 Costs Plugin
|
|
443
|
-
|
|
444
|
-
Track and monitor AWS S3 costs in real-time by calculating expenses for each API operation. Essential for cost optimization and budget management.
|
|
445
|
-
|
|
446
|
-
### ⚡ Quick Start
|
|
447
|
-
|
|
448
|
-
```javascript
|
|
449
|
-
import { S3db, CostsPlugin } from 's3db.js';
|
|
450
|
-
|
|
451
|
-
const s3db = new S3db({
|
|
452
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
453
|
-
plugins: [CostsPlugin] // Static plugin - no 'new' required
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
await s3db.connect();
|
|
457
|
-
|
|
458
|
-
// Use your database normally
|
|
459
|
-
const users = s3db.resource('users');
|
|
460
|
-
await users.insert({ name: 'John', email: 'john@example.com' });
|
|
461
|
-
await users.list();
|
|
462
|
-
|
|
463
|
-
// Check costs
|
|
464
|
-
console.log('Total cost:', s3db.client.costs.total);
|
|
465
|
-
console.log('Request breakdown:', s3db.client.costs.requests);
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
### ⚙️ Configuration Parameters
|
|
469
|
-
|
|
470
|
-
**Note**: CostsPlugin is a static plugin with no configuration options. It automatically tracks all S3 operations.
|
|
471
|
-
|
|
472
|
-
### Cost Tracking Details
|
|
473
|
-
|
|
474
|
-
| Operation | Cost per 1000 requests | Tracked Commands |
|
|
475
|
-
|-----------|------------------------|------------------|
|
|
476
|
-
| PUT operations | $0.005 | PutObjectCommand |
|
|
477
|
-
| GET operations | $0.0004 | GetObjectCommand |
|
|
478
|
-
| HEAD operations | $0.0004 | HeadObjectCommand |
|
|
479
|
-
| DELETE operations | $0.0004 | DeleteObjectCommand, DeleteObjectsCommand |
|
|
480
|
-
| LIST operations | $0.005 | ListObjectsV2Command |
|
|
481
|
-
|
|
482
|
-
### Cost Data Structure
|
|
483
|
-
|
|
484
|
-
```javascript
|
|
485
|
-
{
|
|
486
|
-
total: 0.000123, // Total cost in USD
|
|
487
|
-
prices: { // Cost per 1000 requests
|
|
488
|
-
put: 0.000005,
|
|
489
|
-
get: 0.0000004,
|
|
490
|
-
head: 0.0000004,
|
|
491
|
-
delete: 0.0000004,
|
|
492
|
-
list: 0.000005
|
|
493
|
-
},
|
|
494
|
-
requests: { // Request counters
|
|
495
|
-
total: 15,
|
|
496
|
-
put: 3,
|
|
497
|
-
get: 8,
|
|
498
|
-
head: 2,
|
|
499
|
-
delete: 1,
|
|
500
|
-
list: 1
|
|
501
|
-
},
|
|
502
|
-
events: { // Command-specific counters
|
|
503
|
-
total: 15,
|
|
504
|
-
PutObjectCommand: 3,
|
|
505
|
-
GetObjectCommand: 8,
|
|
506
|
-
HeadObjectCommand: 2,
|
|
507
|
-
DeleteObjectCommand: 1,
|
|
508
|
-
ListObjectsV2Command: 1
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
### 🔧 Easy Example
|
|
514
|
-
|
|
515
|
-
```javascript
|
|
516
|
-
import { S3db, CostsPlugin } from 's3db.js';
|
|
517
|
-
|
|
518
|
-
const s3db = new S3db({
|
|
519
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
520
|
-
plugins: [CostsPlugin]
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
await s3db.connect();
|
|
524
|
-
|
|
525
|
-
const products = s3db.resource('products');
|
|
526
|
-
|
|
527
|
-
// Perform operations and track costs
|
|
528
|
-
await products.insert({ name: 'Widget A', price: 19.99 });
|
|
529
|
-
await products.insert({ name: 'Widget B', price: 29.99 });
|
|
530
|
-
await products.list();
|
|
531
|
-
await products.count();
|
|
532
|
-
|
|
533
|
-
// Analyze costs
|
|
534
|
-
const costs = s3db.client.costs;
|
|
535
|
-
console.log(`Operations performed: ${costs.requests.total}`);
|
|
536
|
-
console.log(`Total cost: $${costs.total.toFixed(6)}`);
|
|
537
|
-
console.log(`Most expensive operation: PUT (${costs.requests.put} requests)`);
|
|
538
|
-
|
|
539
|
-
// Cost breakdown
|
|
540
|
-
console.log('\nCost breakdown:');
|
|
541
|
-
Object.entries(costs.requests).forEach(([operation, count]) => {
|
|
542
|
-
if (operation !== 'total' && count > 0) {
|
|
543
|
-
const operationCost = count * costs.prices[operation];
|
|
544
|
-
console.log(` ${operation.toUpperCase()}: ${count} requests = $${operationCost.toFixed(6)}`);
|
|
545
|
-
}
|
|
546
|
-
});
|
|
547
|
-
```
|
|
548
|
-
|
|
549
|
-
### 🚀 Advanced Monitoring Example
|
|
550
|
-
|
|
551
|
-
```javascript
|
|
552
|
-
import { S3db, CostsPlugin } from 's3db.js';
|
|
553
|
-
|
|
554
|
-
class CostMonitor {
|
|
555
|
-
constructor(s3db) {
|
|
556
|
-
this.s3db = s3db;
|
|
557
|
-
this.startTime = Date.now();
|
|
558
|
-
this.checkpoints = [];
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
checkpoint(label) {
|
|
562
|
-
const costs = { ...this.s3db.client.costs };
|
|
563
|
-
const timestamp = Date.now();
|
|
564
|
-
|
|
565
|
-
this.checkpoints.push({
|
|
566
|
-
label,
|
|
567
|
-
timestamp,
|
|
568
|
-
costs,
|
|
569
|
-
duration: timestamp - this.startTime
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
return costs;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
report() {
|
|
576
|
-
console.log('\n=== Cost Analysis Report ===');
|
|
577
|
-
|
|
578
|
-
for (let i = 0; i < this.checkpoints.length; i++) {
|
|
579
|
-
const checkpoint = this.checkpoints[i];
|
|
580
|
-
const prevCheckpoint = i > 0 ? this.checkpoints[i - 1] : null;
|
|
581
|
-
|
|
582
|
-
console.log(`\n${checkpoint.label}:`);
|
|
583
|
-
console.log(` Time: ${checkpoint.duration}ms`);
|
|
584
|
-
console.log(` Total cost: $${checkpoint.costs.total.toFixed(6)}`);
|
|
585
|
-
|
|
586
|
-
if (prevCheckpoint) {
|
|
587
|
-
const costDiff = checkpoint.costs.total - prevCheckpoint.costs.total;
|
|
588
|
-
const requestDiff = checkpoint.costs.requests.total - prevCheckpoint.costs.requests.total;
|
|
589
|
-
console.log(` Cost increase: $${costDiff.toFixed(6)}`);
|
|
590
|
-
console.log(` New requests: ${requestDiff}`);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// Efficiency metrics
|
|
595
|
-
const finalCosts = this.checkpoints[this.checkpoints.length - 1].costs;
|
|
596
|
-
const totalTime = this.checkpoints[this.checkpoints.length - 1].duration;
|
|
597
|
-
|
|
598
|
-
console.log('\n=== Efficiency Metrics ===');
|
|
599
|
-
console.log(`Total execution time: ${totalTime}ms`);
|
|
600
|
-
console.log(`Total requests: ${finalCosts.requests.total}`);
|
|
601
|
-
console.log(`Requests per second: ${(finalCosts.requests.total / (totalTime / 1000)).toFixed(2)}`);
|
|
602
|
-
console.log(`Cost per request: $${(finalCosts.total / finalCosts.requests.total).toFixed(8)}`);
|
|
603
|
-
console.log(`Monthly projection (1M ops): $${(finalCosts.total * 1000000).toFixed(2)}`);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Usage
|
|
608
|
-
const s3db = new S3db({
|
|
609
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
610
|
-
plugins: [CostsPlugin]
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
await s3db.connect();
|
|
614
|
-
|
|
615
|
-
const monitor = new CostMonitor(s3db);
|
|
616
|
-
const users = s3db.resource('users');
|
|
617
|
-
|
|
618
|
-
// Bulk operations with cost tracking
|
|
619
|
-
monitor.checkpoint('Initial state');
|
|
620
|
-
|
|
621
|
-
// Bulk insert
|
|
622
|
-
const userData = Array.from({ length: 100 }, (_, i) => ({
|
|
623
|
-
name: `User ${i}`,
|
|
624
|
-
email: `user${i}@example.com`,
|
|
625
|
-
role: i % 3 === 0 ? 'admin' : 'user'
|
|
626
|
-
}));
|
|
627
|
-
|
|
628
|
-
await users.insertMany(userData);
|
|
629
|
-
monitor.checkpoint('After bulk insert');
|
|
630
|
-
|
|
631
|
-
// Query operations
|
|
632
|
-
await users.count();
|
|
633
|
-
await users.list({ limit: 50 });
|
|
634
|
-
await users.list({ limit: 25, offset: 25 });
|
|
635
|
-
monitor.checkpoint('After queries');
|
|
636
|
-
|
|
637
|
-
// Update operations
|
|
638
|
-
const userList = await users.list({ limit: 10 });
|
|
639
|
-
for (const user of userList) {
|
|
640
|
-
await users.update(user.id, { lastLogin: new Date().toISOString() });
|
|
641
|
-
}
|
|
642
|
-
monitor.checkpoint('After updates');
|
|
643
|
-
|
|
644
|
-
// Generate detailed report
|
|
645
|
-
monitor.report();
|
|
646
|
-
|
|
647
|
-
// Set cost alerts
|
|
648
|
-
const currentCost = s3db.client.costs.total;
|
|
649
|
-
if (currentCost > 0.01) { // $0.01 threshold
|
|
650
|
-
console.warn(`⚠️ Cost threshold exceeded: $${currentCost.toFixed(6)}`);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Export cost data for external analysis
|
|
654
|
-
const costData = {
|
|
655
|
-
timestamp: new Date().toISOString(),
|
|
656
|
-
sessionCosts: s3db.client.costs,
|
|
657
|
-
checkpoints: monitor.checkpoints,
|
|
658
|
-
summary: {
|
|
659
|
-
totalCost: s3db.client.costs.total,
|
|
660
|
-
totalRequests: s3db.client.costs.requests.total,
|
|
661
|
-
avgCostPerRequest: s3db.client.costs.total / s3db.client.costs.requests.total,
|
|
662
|
-
mostExpensiveOperation: Object.entries(s3db.client.costs.requests)
|
|
663
|
-
.filter(([key]) => key !== 'total')
|
|
664
|
-
.sort(([,a], [,b]) => b - a)[0]
|
|
665
|
-
}
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
console.log('\nExportable cost data:', JSON.stringify(costData, null, 2));
|
|
669
|
-
```
|
|
670
|
-
|
|
671
|
-
---
|
|
672
|
-
|
|
673
|
-
## 📝 Audit Plugin
|
|
674
|
-
|
|
675
|
-
Comprehensive audit logging system that tracks all database operations for compliance, security monitoring, and debugging purposes.
|
|
676
|
-
|
|
677
|
-
### ⚡ Quick Start
|
|
678
|
-
|
|
679
|
-
```javascript
|
|
680
|
-
import { S3db, AuditPlugin } from 's3db.js';
|
|
681
|
-
|
|
682
|
-
const s3db = new S3db({
|
|
683
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
684
|
-
plugins: [new AuditPlugin({ enabled: true })]
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
await s3db.connect();
|
|
688
|
-
|
|
689
|
-
// All operations are automatically logged
|
|
690
|
-
const users = s3db.resource('users');
|
|
691
|
-
await users.insert({ name: 'John', email: 'john@example.com' });
|
|
692
|
-
await users.update(userId, { name: 'John Doe' });
|
|
693
|
-
|
|
694
|
-
// Access audit logs
|
|
695
|
-
const auditResource = s3db.resource('audits');
|
|
696
|
-
const logs = await auditResource.list();
|
|
697
|
-
console.log('Audit trail:', logs);
|
|
698
|
-
```
|
|
699
|
-
|
|
700
|
-
### ⚙️ Configuration Parameters
|
|
701
|
-
|
|
702
|
-
| Parameter | Type | Default | Description |
|
|
703
|
-
|-----------|------|---------|-------------|
|
|
704
|
-
| `enabled` | boolean | `true` | Enable/disable audit logging |
|
|
705
|
-
| `includeData` | boolean | `true` | Include data payloads in audit logs |
|
|
706
|
-
| `includePartitions` | boolean | `true` | Include partition information in logs |
|
|
707
|
-
| `maxDataSize` | number | `10000` | Maximum data size to log (bytes) |
|
|
708
|
-
| `trackOperations` | array | `['insert', 'update', 'delete']` | Operations to audit |
|
|
709
|
-
| `excludeResources` | array | `[]` | Resources to exclude from auditing |
|
|
710
|
-
| `userId` | function | `null` | Function to extract user ID from context |
|
|
711
|
-
| `metadata` | function | `null` | Function to add custom metadata |
|
|
712
|
-
|
|
713
|
-
### Audit Log Structure
|
|
714
|
-
|
|
715
|
-
```javascript
|
|
716
|
-
{
|
|
717
|
-
id: 'audit-abc123',
|
|
718
|
-
resourceName: 'users',
|
|
719
|
-
operation: 'insert',
|
|
720
|
-
recordId: 'user-123',
|
|
721
|
-
userId: 'admin-456',
|
|
722
|
-
timestamp: '2024-01-15T10:30:00.000Z',
|
|
723
|
-
oldData: '{"name":"John"}', // For updates
|
|
724
|
-
newData: '{"name":"John Doe"}', // JSON string of data
|
|
725
|
-
partition: 'byStatus', // If using partitions
|
|
726
|
-
partitionValues: '{"status":"active"}',
|
|
727
|
-
metadata: '{"ip":"192.168.1.1"}', // Custom metadata
|
|
728
|
-
_v: 0 // Audit record version
|
|
729
|
-
}
|
|
730
|
-
```
|
|
731
|
-
|
|
732
|
-
### 🔧 Easy Example
|
|
733
|
-
|
|
734
|
-
```javascript
|
|
735
|
-
import { S3db, AuditPlugin } from 's3db.js';
|
|
736
|
-
|
|
737
|
-
const s3db = new S3db({
|
|
738
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
739
|
-
plugins: [new AuditPlugin({
|
|
740
|
-
enabled: true,
|
|
741
|
-
includeData: true,
|
|
742
|
-
trackOperations: ['insert', 'update', 'delete', 'get'],
|
|
743
|
-
maxDataSize: 5000
|
|
744
|
-
})]
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
await s3db.connect();
|
|
748
|
-
|
|
749
|
-
const products = s3db.resource('products');
|
|
750
|
-
const audits = s3db.resource('audits');
|
|
751
|
-
|
|
752
|
-
// Perform operations (automatically audited)
|
|
753
|
-
const product = await products.insert({
|
|
754
|
-
name: 'Gaming Laptop',
|
|
755
|
-
price: 1299.99,
|
|
756
|
-
category: 'electronics'
|
|
757
|
-
});
|
|
758
|
-
|
|
759
|
-
await products.update(product.id, { price: 1199.99 });
|
|
760
|
-
await products.get(product.id);
|
|
761
|
-
await products.delete(product.id);
|
|
762
|
-
|
|
763
|
-
// Review audit trail
|
|
764
|
-
const auditLogs = await audits.list();
|
|
765
|
-
|
|
766
|
-
console.log('\n=== Audit Trail ===');
|
|
767
|
-
auditLogs.forEach(log => {
|
|
768
|
-
console.log(`${log.timestamp} | ${log.operation.toUpperCase()} | ${log.resourceName} | ${log.recordId}`);
|
|
769
|
-
|
|
770
|
-
if (log.operation === 'update') {
|
|
771
|
-
const oldData = JSON.parse(log.oldData);
|
|
772
|
-
const newData = JSON.parse(log.newData);
|
|
773
|
-
console.log(` Price changed: $${oldData.price} → $${newData.price}`);
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
// Query specific audit logs
|
|
778
|
-
const updateLogs = await audits.list({
|
|
779
|
-
filter: log => log.operation === 'update'
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
console.log(`\nFound ${updateLogs.length} update operations`);
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
### 🚀 Advanced Configuration Example
|
|
786
|
-
|
|
787
|
-
```javascript
|
|
788
|
-
import { S3db, AuditPlugin } from 's3db.js';
|
|
789
|
-
|
|
790
|
-
const s3db = new S3db({
|
|
791
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
792
|
-
plugins: [new AuditPlugin({
|
|
793
|
-
enabled: true,
|
|
794
|
-
includeData: true,
|
|
795
|
-
includePartitions: true,
|
|
796
|
-
maxDataSize: 20000, // 20KB limit
|
|
797
|
-
|
|
798
|
-
// Track all operations including reads
|
|
799
|
-
trackOperations: ['insert', 'update', 'delete', 'get', 'list'],
|
|
800
|
-
|
|
801
|
-
// Exclude sensitive resources from auditing
|
|
802
|
-
excludeResources: ['sessions', 'temp_data'],
|
|
803
|
-
|
|
804
|
-
// Extract user ID from request context
|
|
805
|
-
userId: (context) => {
|
|
806
|
-
return context?.user?.id ||
|
|
807
|
-
context?.headers?.['x-user-id'] ||
|
|
808
|
-
'anonymous';
|
|
809
|
-
},
|
|
810
|
-
|
|
811
|
-
// Add custom metadata to audit logs
|
|
812
|
-
metadata: (operation, resourceName, data, context) => {
|
|
813
|
-
return {
|
|
814
|
-
ip: context?.ip,
|
|
815
|
-
userAgent: context?.userAgent,
|
|
816
|
-
sessionId: context?.sessionId,
|
|
817
|
-
apiVersion: '1.0',
|
|
818
|
-
environment: process.env.NODE_ENV,
|
|
819
|
-
requestId: context?.requestId,
|
|
820
|
-
|
|
821
|
-
// Operation-specific metadata
|
|
822
|
-
...(operation === 'insert' && {
|
|
823
|
-
createdVia: 'api',
|
|
824
|
-
validationPassed: true
|
|
825
|
-
}),
|
|
826
|
-
|
|
827
|
-
...(operation === 'update' && {
|
|
828
|
-
fieldsChanged: Object.keys(data || {}),
|
|
829
|
-
automaticUpdate: false
|
|
830
|
-
}),
|
|
831
|
-
|
|
832
|
-
...(operation === 'delete' && {
|
|
833
|
-
softDelete: false,
|
|
834
|
-
cascadeDelete: false
|
|
835
|
-
})
|
|
836
|
-
};
|
|
837
|
-
}
|
|
838
|
-
})]
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
await s3db.connect();
|
|
842
|
-
|
|
843
|
-
// Custom audit query functions
|
|
844
|
-
class AuditAnalyzer {
|
|
845
|
-
constructor(auditResource) {
|
|
846
|
-
this.audits = auditResource;
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
async getUserActivity(userId, timeRange = 24) {
|
|
850
|
-
const since = new Date(Date.now() - timeRange * 60 * 60 * 1000);
|
|
851
|
-
const logs = await this.audits.list();
|
|
852
|
-
|
|
853
|
-
return logs.filter(log =>
|
|
854
|
-
log.userId === userId &&
|
|
855
|
-
new Date(log.timestamp) > since
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
async getResourceActivity(resourceName, operation = null) {
|
|
860
|
-
const logs = await this.audits.list();
|
|
861
|
-
|
|
862
|
-
return logs.filter(log =>
|
|
863
|
-
log.resourceName === resourceName &&
|
|
864
|
-
(!operation || log.operation === operation)
|
|
865
|
-
);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
async getDataChanges(resourceName, recordId) {
|
|
869
|
-
const logs = await this.audits.list();
|
|
870
|
-
|
|
871
|
-
return logs
|
|
872
|
-
.filter(log =>
|
|
873
|
-
log.resourceName === resourceName &&
|
|
874
|
-
log.recordId === recordId &&
|
|
875
|
-
log.operation === 'update'
|
|
876
|
-
)
|
|
877
|
-
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp))
|
|
878
|
-
.map(log => ({
|
|
879
|
-
timestamp: log.timestamp,
|
|
880
|
-
oldData: JSON.parse(log.oldData || '{}'),
|
|
881
|
-
newData: JSON.parse(log.newData || '{}'),
|
|
882
|
-
userId: log.userId,
|
|
883
|
-
metadata: JSON.parse(log.metadata || '{}')
|
|
884
|
-
}));
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
async generateComplianceReport(startDate, endDate) {
|
|
888
|
-
const logs = await this.audits.list();
|
|
889
|
-
|
|
890
|
-
const filteredLogs = logs.filter(log => {
|
|
891
|
-
const logDate = new Date(log.timestamp);
|
|
892
|
-
return logDate >= startDate && logDate <= endDate;
|
|
893
|
-
});
|
|
894
|
-
|
|
895
|
-
const summary = {
|
|
896
|
-
totalOperations: filteredLogs.length,
|
|
897
|
-
operationBreakdown: {},
|
|
898
|
-
resourceActivity: {},
|
|
899
|
-
userActivity: {},
|
|
900
|
-
timeRange: { startDate, endDate }
|
|
901
|
-
};
|
|
902
|
-
|
|
903
|
-
filteredLogs.forEach(log => {
|
|
904
|
-
// Operation breakdown
|
|
905
|
-
summary.operationBreakdown[log.operation] =
|
|
906
|
-
(summary.operationBreakdown[log.operation] || 0) + 1;
|
|
907
|
-
|
|
908
|
-
// Resource activity
|
|
909
|
-
summary.resourceActivity[log.resourceName] =
|
|
910
|
-
(summary.resourceActivity[log.resourceName] || 0) + 1;
|
|
911
|
-
|
|
912
|
-
// User activity
|
|
913
|
-
summary.userActivity[log.userId] =
|
|
914
|
-
(summary.userActivity[log.userId] || 0) + 1;
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
return summary;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
// Usage with context
|
|
922
|
-
const users = s3db.resource('users');
|
|
923
|
-
const audits = s3db.resource('audits');
|
|
924
|
-
const analyzer = new AuditAnalyzer(audits);
|
|
925
|
-
|
|
926
|
-
// Simulate operations with user context
|
|
927
|
-
const userContext = {
|
|
928
|
-
user: { id: 'admin-123', role: 'admin' },
|
|
929
|
-
ip: '192.168.1.100',
|
|
930
|
-
userAgent: 'Mozilla/5.0...',
|
|
931
|
-
sessionId: 'sess-789',
|
|
932
|
-
requestId: 'req-456'
|
|
933
|
-
};
|
|
934
|
-
|
|
935
|
-
// Operations with context (would be passed through middleware in real app)
|
|
936
|
-
await users.insert({
|
|
937
|
-
name: 'Alice Johnson',
|
|
938
|
-
email: 'alice@example.com'
|
|
939
|
-
}, userContext);
|
|
940
|
-
|
|
941
|
-
// Analyze audit data
|
|
942
|
-
const userActivity = await analyzer.getUserActivity('admin-123');
|
|
943
|
-
console.log('Recent user activity:', userActivity);
|
|
944
|
-
|
|
945
|
-
const complianceReport = await analyzer.generateComplianceReport(
|
|
946
|
-
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // Last 7 days
|
|
947
|
-
new Date()
|
|
948
|
-
);
|
|
949
|
-
|
|
950
|
-
console.log('\n=== Compliance Report ===');
|
|
951
|
-
console.log(`Total operations: ${complianceReport.totalOperations}`);
|
|
952
|
-
console.log('Operation breakdown:', complianceReport.operationBreakdown);
|
|
953
|
-
console.log('Most active resource:',
|
|
954
|
-
Object.entries(complianceReport.resourceActivity)
|
|
955
|
-
.sort(([,a], [,b]) => b - a)[0]
|
|
956
|
-
);
|
|
957
|
-
|
|
958
|
-
// Real-time audit monitoring
|
|
959
|
-
audits.on('insert', (auditLog) => {
|
|
960
|
-
console.log(`🔍 New audit log: ${auditLog.operation} on ${auditLog.resourceName}`);
|
|
961
|
-
|
|
962
|
-
// Security alerts
|
|
963
|
-
if (auditLog.operation === 'delete' && auditLog.userId === 'anonymous') {
|
|
964
|
-
console.warn('🚨 SECURITY ALERT: Anonymous user performed delete operation');
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
if (auditLog.operation === 'get' && auditLog.resourceName === 'sensitive_data') {
|
|
968
|
-
console.warn('🔒 PRIVACY ALERT: Sensitive data accessed');
|
|
969
|
-
}
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
// Audit log retention and cleanup
|
|
973
|
-
setInterval(async () => {
|
|
974
|
-
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
975
|
-
const oldLogs = await audits.list({
|
|
976
|
-
filter: log => new Date(log.timestamp) < thirtyDaysAgo
|
|
977
|
-
});
|
|
978
|
-
|
|
979
|
-
console.log(`Cleaning up ${oldLogs.length} old audit logs`);
|
|
980
|
-
|
|
981
|
-
for (const log of oldLogs) {
|
|
982
|
-
await audits.delete(log.id);
|
|
983
|
-
}
|
|
984
|
-
}, 24 * 60 * 60 * 1000); // Daily cleanup
|
|
985
|
-
```
|
|
986
|
-
|
|
987
|
-
---
|
|
988
|
-
|
|
989
|
-
## 🔍 FullText Plugin
|
|
990
|
-
|
|
991
|
-
Powerful full-text search engine with automatic indexing, scoring, and advanced search capabilities for your s3db resources.
|
|
992
|
-
|
|
993
|
-
### ⚡ Quick Start
|
|
994
|
-
|
|
995
|
-
```javascript
|
|
996
|
-
import { S3db, FullTextPlugin } from 's3db.js';
|
|
997
|
-
|
|
998
|
-
const s3db = new S3db({
|
|
999
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1000
|
-
plugins: [new FullTextPlugin({
|
|
1001
|
-
enabled: true,
|
|
1002
|
-
fields: ['title', 'description', 'content']
|
|
1003
|
-
})]
|
|
1004
|
-
});
|
|
1005
|
-
|
|
1006
|
-
await s3db.connect();
|
|
1007
|
-
|
|
1008
|
-
const articles = s3db.resource('articles');
|
|
1009
|
-
|
|
1010
|
-
// Insert data (automatically indexed)
|
|
1011
|
-
await articles.insert({
|
|
1012
|
-
title: 'Introduction to Machine Learning',
|
|
1013
|
-
description: 'A comprehensive guide to ML basics',
|
|
1014
|
-
content: 'Machine learning is a subset of artificial intelligence...'
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
// Search across indexed fields
|
|
1018
|
-
const results = await s3db.plugins.fulltext.searchRecords('articles', 'machine learning');
|
|
1019
|
-
console.log('Search results:', results);
|
|
1020
|
-
```
|
|
1021
|
-
|
|
1022
|
-
### ⚙️ Configuration Parameters
|
|
1023
|
-
|
|
1024
|
-
| Parameter | Type | Default | Description |
|
|
1025
|
-
|-----------|------|---------|-------------|
|
|
1026
|
-
| `enabled` | boolean | `true` | Enable/disable full-text search |
|
|
1027
|
-
| `fields` | array | `[]` | Fields to index for search |
|
|
1028
|
-
| `minWordLength` | number | `3` | Minimum word length for indexing |
|
|
1029
|
-
| `maxResults` | number | `100` | Maximum search results to return |
|
|
1030
|
-
| `language` | string | `'en-US'` | Language for text processing |
|
|
1031
|
-
| `stopWords` | array | `['the', 'a', 'an', ...]` | Words to exclude from indexing |
|
|
1032
|
-
| `stemming` | boolean | `false` | Enable word stemming |
|
|
1033
|
-
| `caseSensitive` | boolean | `false` | Case-sensitive search |
|
|
1034
|
-
| `fuzzySearch` | boolean | `false` | Enable fuzzy matching |
|
|
1035
|
-
| `indexName` | string | `'fulltext_indexes'` | Name of index resource |
|
|
1036
|
-
|
|
1037
|
-
### Search Result Structure
|
|
1038
|
-
|
|
1039
|
-
```javascript
|
|
1040
|
-
{
|
|
1041
|
-
id: 'article-123',
|
|
1042
|
-
title: 'Introduction to Machine Learning',
|
|
1043
|
-
description: 'A comprehensive guide to ML basics',
|
|
1044
|
-
content: 'Machine learning is a subset...',
|
|
1045
|
-
_searchScore: 0.85, // Relevance score (0-1)
|
|
1046
|
-
_matchedFields: ['title', 'content'], // Fields with matches
|
|
1047
|
-
_matchedWords: ['machine', 'learning'], // Matched search terms
|
|
1048
|
-
_highlights: { // Highlighted snippets
|
|
1049
|
-
title: 'Introduction to <mark>Machine Learning</mark>',
|
|
1050
|
-
content: '<mark>Machine learning</mark> is a subset...'
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
```
|
|
1054
|
-
|
|
1055
|
-
### 🔧 Easy Example
|
|
1056
|
-
|
|
1057
|
-
```javascript
|
|
1058
|
-
import { S3db, FullTextPlugin } from 's3db.js';
|
|
1059
|
-
|
|
1060
|
-
const s3db = new S3db({
|
|
1061
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1062
|
-
plugins: [new FullTextPlugin({
|
|
1063
|
-
enabled: true,
|
|
1064
|
-
fields: ['name', 'description', 'tags'],
|
|
1065
|
-
minWordLength: 2,
|
|
1066
|
-
maxResults: 50
|
|
1067
|
-
})]
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
await s3db.connect();
|
|
1071
|
-
|
|
1072
|
-
const products = s3db.resource('products');
|
|
1073
|
-
|
|
1074
|
-
// Add products with searchable content
|
|
1075
|
-
await products.insertMany([
|
|
1076
|
-
{
|
|
1077
|
-
name: 'Gaming Laptop Pro',
|
|
1078
|
-
description: 'High-performance laptop for gaming and productivity',
|
|
1079
|
-
tags: ['gaming', 'laptop', 'computer', 'electronics']
|
|
1080
|
-
},
|
|
1081
|
-
{
|
|
1082
|
-
name: 'Wireless Gaming Mouse',
|
|
1083
|
-
description: 'Precision wireless mouse designed for gamers',
|
|
1084
|
-
tags: ['gaming', 'mouse', 'wireless', 'electronics']
|
|
1085
|
-
},
|
|
1086
|
-
{
|
|
1087
|
-
name: 'Mechanical Keyboard',
|
|
1088
|
-
description: 'Professional mechanical keyboard with RGB lighting',
|
|
1089
|
-
tags: ['keyboard', 'mechanical', 'typing', 'electronics']
|
|
1090
|
-
}
|
|
1091
|
-
]);
|
|
1092
|
-
|
|
1093
|
-
// Search for gaming products
|
|
1094
|
-
const gamingProducts = await s3db.plugins.fulltext.searchRecords('products', 'gaming');
|
|
1095
|
-
|
|
1096
|
-
console.log('\n=== Gaming Products ===');
|
|
1097
|
-
gamingProducts.forEach(product => {
|
|
1098
|
-
console.log(`${product.name} (Score: ${product._searchScore.toFixed(2)})`);
|
|
1099
|
-
console.log(` Matched fields: ${product._matchedFields.join(', ')}`);
|
|
1100
|
-
console.log(` Description: ${product.description}`);
|
|
1101
|
-
});
|
|
1102
|
-
|
|
1103
|
-
// Search for wireless devices
|
|
1104
|
-
const wirelessProducts = await s3db.plugins.fulltext.searchRecords('products', 'wireless');
|
|
1105
|
-
|
|
1106
|
-
console.log('\n=== Wireless Products ===');
|
|
1107
|
-
wirelessProducts.forEach(product => {
|
|
1108
|
-
console.log(`${product.name} - ${product.description}`);
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
// Multi-word search
|
|
1112
|
-
const laptopGaming = await s3db.plugins.fulltext.searchRecords('products', 'laptop gaming');
|
|
1113
|
-
|
|
1114
|
-
console.log('\n=== Laptop Gaming Search ===');
|
|
1115
|
-
console.log(`Found ${laptopGaming.length} products matching "laptop gaming"`);
|
|
1116
|
-
```
|
|
1117
|
-
|
|
1118
|
-
### 🚀 Advanced Configuration Example
|
|
1119
|
-
|
|
1120
|
-
```javascript
|
|
1121
|
-
import { S3db, FullTextPlugin } from 's3db.js';
|
|
1122
|
-
|
|
1123
|
-
const s3db = new S3db({
|
|
1124
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1125
|
-
plugins: [new FullTextPlugin({
|
|
1126
|
-
enabled: true,
|
|
1127
|
-
|
|
1128
|
-
// Comprehensive field indexing
|
|
1129
|
-
fields: ['title', 'description', 'content', 'tags', 'category', 'author'],
|
|
1130
|
-
|
|
1131
|
-
// Advanced text processing
|
|
1132
|
-
minWordLength: 2,
|
|
1133
|
-
maxResults: 200,
|
|
1134
|
-
language: 'en-US',
|
|
1135
|
-
stemming: true, // Enable word stemming (run/running/ran)
|
|
1136
|
-
caseSensitive: false,
|
|
1137
|
-
fuzzySearch: true, // Enable typo tolerance
|
|
1138
|
-
|
|
1139
|
-
// Custom stop words (words to ignore)
|
|
1140
|
-
stopWords: [
|
|
1141
|
-
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
|
1142
|
-
'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
|
|
1143
|
-
'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
|
|
1144
|
-
'should', 'may', 'might', 'must', 'can', 'this', 'that', 'these', 'those'
|
|
1145
|
-
],
|
|
1146
|
-
|
|
1147
|
-
// Advanced search options
|
|
1148
|
-
highlightTags: {
|
|
1149
|
-
start: '<mark class="highlight">',
|
|
1150
|
-
end: '</mark>'
|
|
1151
|
-
},
|
|
1152
|
-
|
|
1153
|
-
// Custom scoring weights per field
|
|
1154
|
-
fieldWeights: {
|
|
1155
|
-
title: 3.0, // Title matches score higher
|
|
1156
|
-
description: 2.0, // Description is important
|
|
1157
|
-
content: 1.0, // Content has normal weight
|
|
1158
|
-
tags: 2.5, // Tags are highly relevant
|
|
1159
|
-
category: 1.5, // Category is moderately important
|
|
1160
|
-
author: 1.0 // Author has normal weight
|
|
1161
|
-
},
|
|
1162
|
-
|
|
1163
|
-
// Indexing behavior
|
|
1164
|
-
indexName: 'search_indexes',
|
|
1165
|
-
autoReindex: true, // Automatically reindex on data changes
|
|
1166
|
-
batchSize: 100, // Index batch size
|
|
1167
|
-
maxIndexSize: 10000 // Maximum index entries
|
|
1168
|
-
})]
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
await s3db.connect();
|
|
1172
|
-
|
|
1173
|
-
// Advanced search class with custom methods
|
|
1174
|
-
class AdvancedSearch {
|
|
1175
|
-
constructor(fulltextPlugin) {
|
|
1176
|
-
this.plugin = fulltextPlugin;
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
async searchWithFilters(resourceName, query, filters = {}) {
|
|
1180
|
-
let results = await this.plugin.searchRecords(resourceName, query);
|
|
1181
|
-
|
|
1182
|
-
// Apply additional filters
|
|
1183
|
-
if (filters.category) {
|
|
1184
|
-
results = results.filter(item => item.category === filters.category);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
if (filters.minScore) {
|
|
1188
|
-
results = results.filter(item => item._searchScore >= filters.minScore);
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
if (filters.dateRange) {
|
|
1192
|
-
const { start, end } = filters.dateRange;
|
|
1193
|
-
results = results.filter(item => {
|
|
1194
|
-
const itemDate = new Date(item.createdAt);
|
|
1195
|
-
return itemDate >= start && itemDate <= end;
|
|
1196
|
-
});
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
return results;
|
|
1200
|
-
}
|
|
1201
|
-
|
|
1202
|
-
async searchMultipleResources(resourceNames, query) {
|
|
1203
|
-
const allResults = [];
|
|
1204
|
-
|
|
1205
|
-
for (const resourceName of resourceNames) {
|
|
1206
|
-
const results = await this.plugin.searchRecords(resourceName, query);
|
|
1207
|
-
allResults.push(...results.map(item => ({
|
|
1208
|
-
...item,
|
|
1209
|
-
_resourceType: resourceName
|
|
1210
|
-
})));
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// Sort by relevance across all resources
|
|
1214
|
-
return allResults.sort((a, b) => b._searchScore - a._searchScore);
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
async suggestWords(resourceName, partial) {
|
|
1218
|
-
// Get all indexed words that start with partial
|
|
1219
|
-
const allIndexes = await this.plugin.indexResource.list();
|
|
1220
|
-
|
|
1221
|
-
const suggestions = allIndexes
|
|
1222
|
-
.filter(index =>
|
|
1223
|
-
index.resourceName === resourceName &&
|
|
1224
|
-
index.word.toLowerCase().startsWith(partial.toLowerCase())
|
|
1225
|
-
)
|
|
1226
|
-
.sort((a, b) => b.count - a.count) // Sort by frequency
|
|
1227
|
-
.slice(0, 10)
|
|
1228
|
-
.map(index => index.word);
|
|
1229
|
-
|
|
1230
|
-
return [...new Set(suggestions)]; // Remove duplicates
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
async getSearchAnalytics(resourceName) {
|
|
1234
|
-
const indexes = await this.plugin.indexResource.list();
|
|
1235
|
-
const resourceIndexes = indexes.filter(i => i.resourceName === resourceName);
|
|
1236
|
-
|
|
1237
|
-
const analytics = {
|
|
1238
|
-
totalWords: resourceIndexes.length,
|
|
1239
|
-
totalOccurrences: resourceIndexes.reduce((sum, i) => sum + i.count, 0),
|
|
1240
|
-
avgWordsPerDocument: 0,
|
|
1241
|
-
topWords: resourceIndexes
|
|
1242
|
-
.sort((a, b) => b.count - a.count)
|
|
1243
|
-
.slice(0, 20)
|
|
1244
|
-
.map(i => ({ word: i.word, count: i.count })),
|
|
1245
|
-
wordDistribution: {},
|
|
1246
|
-
lastIndexed: Math.max(...resourceIndexes.map(i => new Date(i.lastUpdated)))
|
|
1247
|
-
};
|
|
1248
|
-
|
|
1249
|
-
// Calculate word distribution by frequency ranges
|
|
1250
|
-
resourceIndexes.forEach(index => {
|
|
1251
|
-
const range = index.count < 5 ? 'rare' :
|
|
1252
|
-
index.count < 20 ? 'common' : 'frequent';
|
|
1253
|
-
analytics.wordDistribution[range] = (analytics.wordDistribution[range] || 0) + 1;
|
|
1254
|
-
});
|
|
1255
|
-
|
|
1256
|
-
return analytics;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// Setup sample data
|
|
1261
|
-
const articles = s3db.resource('articles');
|
|
1262
|
-
const products = s3db.resource('products');
|
|
1263
|
-
|
|
1264
|
-
await articles.insertMany([
|
|
1265
|
-
{
|
|
1266
|
-
title: 'Advanced JavaScript Techniques',
|
|
1267
|
-
description: 'Deep dive into modern JavaScript features',
|
|
1268
|
-
content: 'JavaScript has evolved significantly with ES6+ features...',
|
|
1269
|
-
tags: ['javascript', 'programming', 'web-development'],
|
|
1270
|
-
category: 'technology',
|
|
1271
|
-
author: 'John Smith'
|
|
1272
|
-
},
|
|
1273
|
-
{
|
|
1274
|
-
title: 'Machine Learning Fundamentals',
|
|
1275
|
-
description: 'Introduction to ML concepts and algorithms',
|
|
1276
|
-
content: 'Machine learning is revolutionizing how we process data...',
|
|
1277
|
-
tags: ['machine-learning', 'ai', 'data-science'],
|
|
1278
|
-
category: 'technology',
|
|
1279
|
-
author: 'Jane Doe'
|
|
1280
|
-
},
|
|
1281
|
-
{
|
|
1282
|
-
title: 'Sustainable Cooking Tips',
|
|
1283
|
-
description: 'Eco-friendly approaches to home cooking',
|
|
1284
|
-
content: 'Sustainable cooking practices can reduce your environmental impact...',
|
|
1285
|
-
tags: ['cooking', 'sustainability', 'environment'],
|
|
1286
|
-
category: 'lifestyle',
|
|
1287
|
-
author: 'Chef Maria'
|
|
1288
|
-
}
|
|
1289
|
-
]);
|
|
1290
|
-
|
|
1291
|
-
// Initialize advanced search
|
|
1292
|
-
const search = new AdvancedSearch(s3db.plugins.fulltext);
|
|
1293
|
-
|
|
1294
|
-
// Complex search with filters
|
|
1295
|
-
const techArticles = await search.searchWithFilters('articles', 'javascript programming', {
|
|
1296
|
-
category: 'technology',
|
|
1297
|
-
minScore: 0.5
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
console.log('\n=== Technology Articles ===');
|
|
1301
|
-
techArticles.forEach(article => {
|
|
1302
|
-
console.log(`${article.title} by ${article.author}`);
|
|
1303
|
-
console.log(` Score: ${article._searchScore.toFixed(3)}`);
|
|
1304
|
-
console.log(` Matches: ${article._matchedWords.join(', ')}`);
|
|
1305
|
-
console.log(` Highlighted: ${article._highlights?.title || article.title}`);
|
|
1306
|
-
});
|
|
1307
|
-
|
|
1308
|
-
// Multi-resource search
|
|
1309
|
-
const allContent = await search.searchMultipleResources(['articles', 'products'], 'technology');
|
|
1310
|
-
|
|
1311
|
-
console.log('\n=== Cross-Resource Search ===');
|
|
1312
|
-
allContent.forEach(item => {
|
|
1313
|
-
console.log(`[${item._resourceType.toUpperCase()}] ${item.title || item.name}`);
|
|
1314
|
-
console.log(` Score: ${item._searchScore.toFixed(3)}`);
|
|
1315
|
-
});
|
|
1316
|
-
|
|
1317
|
-
// Auto-complete suggestions
|
|
1318
|
-
const suggestions = await search.suggestWords('articles', 'java');
|
|
1319
|
-
console.log('\nSuggestions for "java":', suggestions);
|
|
1320
|
-
|
|
1321
|
-
// Search analytics
|
|
1322
|
-
const analytics = await search.getSearchAnalytics('articles');
|
|
1323
|
-
console.log('\n=== Search Analytics ===');
|
|
1324
|
-
console.log(`Total indexed words: ${analytics.totalWords}`);
|
|
1325
|
-
console.log(`Total word occurrences: ${analytics.totalOccurrences}`);
|
|
1326
|
-
console.log('Top words:', analytics.topWords.slice(0, 5));
|
|
1327
|
-
console.log('Word distribution:', analytics.wordDistribution);
|
|
1328
|
-
|
|
1329
|
-
// Real-time search monitoring
|
|
1330
|
-
s3db.plugins.fulltext.on('indexed', (data) => {
|
|
1331
|
-
console.log(`🔍 Indexed: ${data.resourceName} - ${data.recordId}`);
|
|
1332
|
-
});
|
|
1333
|
-
|
|
1334
|
-
s3db.plugins.fulltext.on('searched', (data) => {
|
|
1335
|
-
console.log(`🔎 Search: "${data.query}" in ${data.resourceName} (${data.results} results)`);
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
// Performance monitoring
|
|
1339
|
-
console.time('Search Performance');
|
|
1340
|
-
const perfResults = await s3db.plugins.fulltext.searchRecords('articles', 'machine learning javascript');
|
|
1341
|
-
console.timeEnd('Search Performance');
|
|
1342
|
-
console.log(`Search returned ${perfResults.length} results`);
|
|
1343
|
-
```
|
|
1344
|
-
|
|
1345
|
-
---
|
|
1346
|
-
|
|
1347
|
-
## 📊 Metrics Plugin
|
|
1348
|
-
|
|
1349
|
-
Comprehensive performance monitoring and usage analytics system that tracks operation timing, resource usage, errors, and provides detailed insights.
|
|
1350
|
-
|
|
1351
|
-
### ⚡ Quick Start
|
|
1352
|
-
|
|
1353
|
-
```javascript
|
|
1354
|
-
import { S3db, MetricsPlugin } from 's3db.js';
|
|
1355
|
-
|
|
1356
|
-
const s3db = new S3db({
|
|
1357
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1358
|
-
plugins: [new MetricsPlugin({ enabled: true })]
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
await s3db.connect();
|
|
1362
|
-
|
|
1363
|
-
// Use your database normally - metrics are collected automatically
|
|
1364
|
-
const users = s3db.resource('users');
|
|
1365
|
-
await users.insert({ name: 'John', email: 'john@example.com' });
|
|
1366
|
-
await users.list();
|
|
1367
|
-
await users.count();
|
|
1368
|
-
|
|
1369
|
-
// Get comprehensive metrics
|
|
1370
|
-
const metrics = await s3db.plugins.metrics.getMetrics();
|
|
1371
|
-
console.log('Performance metrics:', metrics);
|
|
1372
|
-
```
|
|
1373
|
-
|
|
1374
|
-
### ⚙️ Configuration Parameters
|
|
1375
|
-
|
|
1376
|
-
| Parameter | Type | Default | Description |
|
|
1377
|
-
|-----------|------|---------|-------------|
|
|
1378
|
-
| `enabled` | boolean | `true` | Enable/disable metrics collection |
|
|
1379
|
-
| `collectPerformance` | boolean | `true` | Track operation timing and performance |
|
|
1380
|
-
| `collectErrors` | boolean | `true` | Track errors and failures |
|
|
1381
|
-
| `collectUsage` | boolean | `true` | Track resource usage patterns |
|
|
1382
|
-
| `retentionDays` | number | `30` | Days to retain metric data |
|
|
1383
|
-
| `flushInterval` | number | `60000` | Interval to flush metrics (ms) |
|
|
1384
|
-
| `sampleRate` | number | `1.0` | Sampling rate for metrics (0.0-1.0) |
|
|
1385
|
-
| `trackSlowQueries` | boolean | `true` | Track slow operations |
|
|
1386
|
-
| `slowQueryThreshold` | number | `1000` | Threshold for slow queries (ms) |
|
|
1387
|
-
| `batchSize` | number | `100` | Batch size for metric storage |
|
|
1388
|
-
|
|
1389
|
-
### Metrics Data Structure
|
|
1390
|
-
|
|
1391
|
-
```javascript
|
|
1392
|
-
{
|
|
1393
|
-
performance: {
|
|
1394
|
-
averageResponseTime: 245, // milliseconds
|
|
1395
|
-
totalRequests: 1250,
|
|
1396
|
-
requestsPerSecond: 12.5,
|
|
1397
|
-
slowestOperations: [
|
|
1398
|
-
{ operation: "list", resource: "users", avgTime: 450, count: 50 },
|
|
1399
|
-
{ operation: "get", resource: "products", avgTime: 320, count: 200 }
|
|
1400
|
-
],
|
|
1401
|
-
operationTiming: {
|
|
1402
|
-
insert: { avg: 180, min: 120, max: 350, total: 50 },
|
|
1403
|
-
update: { avg: 160, min: 90, max: 280, total: 30 },
|
|
1404
|
-
get: { avg: 95, min: 45, max: 180, total: 200 }
|
|
1405
|
-
}
|
|
1406
|
-
},
|
|
1407
|
-
usage: {
|
|
1408
|
-
resources: {
|
|
1409
|
-
users: { inserts: 150, updates: 75, deletes: 10, reads: 800 },
|
|
1410
|
-
products: { inserts: 300, updates: 120, deletes: 25, reads: 1200 }
|
|
1411
|
-
},
|
|
1412
|
-
totalOperations: 2680,
|
|
1413
|
-
mostActiveResource: "products",
|
|
1414
|
-
peakUsageHour: "14:00",
|
|
1415
|
-
dailyPatterns: { /* hourly usage data */ }
|
|
1416
|
-
},
|
|
1417
|
-
errors: {
|
|
1418
|
-
total: 15,
|
|
1419
|
-
byType: {
|
|
1420
|
-
"ValidationError": 8,
|
|
1421
|
-
"NotFoundError": 5,
|
|
1422
|
-
"PermissionError": 2
|
|
1423
|
-
},
|
|
1424
|
-
byResource: { users: 10, products: 5 },
|
|
1425
|
-
errorRate: 0.0056 // 0.56%
|
|
1426
|
-
},
|
|
1427
|
-
cache: {
|
|
1428
|
-
hitRate: 0.78,
|
|
1429
|
-
totalHits: 980,
|
|
1430
|
-
totalMisses: 270
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
```
|
|
1434
|
-
|
|
1435
|
-
### 🔧 Easy Example
|
|
1436
|
-
|
|
1437
|
-
```javascript
|
|
1438
|
-
import { S3db, MetricsPlugin } from 's3db.js';
|
|
1439
|
-
|
|
1440
|
-
const s3db = new S3db({
|
|
1441
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1442
|
-
plugins: [new MetricsPlugin({
|
|
1443
|
-
enabled: true,
|
|
1444
|
-
collectPerformance: true,
|
|
1445
|
-
collectErrors: true,
|
|
1446
|
-
flushInterval: 30000 // 30 seconds
|
|
1447
|
-
})]
|
|
1448
|
-
});
|
|
1449
|
-
|
|
1450
|
-
await s3db.connect();
|
|
1451
|
-
|
|
1452
|
-
const orders = s3db.resource('orders');
|
|
1453
|
-
|
|
1454
|
-
// Simulate various operations
|
|
1455
|
-
console.log('Performing operations...');
|
|
1456
|
-
|
|
1457
|
-
// Fast operations
|
|
1458
|
-
for (let i = 0; i < 10; i++) {
|
|
1459
|
-
await orders.insert({
|
|
1460
|
-
customerId: `customer-${i}`,
|
|
1461
|
-
amount: Math.random() * 1000,
|
|
1462
|
-
status: 'pending'
|
|
1463
|
-
});
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// Query operations
|
|
1467
|
-
await orders.count();
|
|
1468
|
-
await orders.list({ limit: 5 });
|
|
1469
|
-
|
|
1470
|
-
// Some updates
|
|
1471
|
-
const orderList = await orders.list({ limit: 3 });
|
|
1472
|
-
for (const order of orderList) {
|
|
1473
|
-
await orders.update(order.id, { status: 'processing' });
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
// Get performance metrics
|
|
1477
|
-
const metrics = await s3db.plugins.metrics.getMetrics();
|
|
1478
|
-
|
|
1479
|
-
console.log('\n=== Performance Report ===');
|
|
1480
|
-
console.log(`Average response time: ${metrics.performance.averageResponseTime}ms`);
|
|
1481
|
-
console.log(`Total operations: ${metrics.usage.totalOperations}`);
|
|
1482
|
-
console.log(`Error rate: ${(metrics.errors.errorRate * 100).toFixed(2)}%`);
|
|
1483
|
-
|
|
1484
|
-
console.log('\n=== Operation Breakdown ===');
|
|
1485
|
-
Object.entries(metrics.performance.operationTiming).forEach(([op, timing]) => {
|
|
1486
|
-
console.log(`${op.toUpperCase()}: avg ${timing.avg}ms (${timing.total} operations)`);
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
console.log('\n=== Resource Usage ===');
|
|
1490
|
-
Object.entries(metrics.usage.resources).forEach(([resource, usage]) => {
|
|
1491
|
-
const total = Object.values(usage).reduce((sum, count) => sum + count, 0);
|
|
1492
|
-
console.log(`${resource}: ${total} total operations`);
|
|
1493
|
-
});
|
|
1494
|
-
```
|
|
1495
|
-
|
|
1496
|
-
### 🚀 Advanced Configuration Example
|
|
1497
|
-
|
|
1498
|
-
```javascript
|
|
1499
|
-
import { S3db, MetricsPlugin } from 's3db.js';
|
|
1500
|
-
|
|
1501
|
-
const s3db = new S3db({
|
|
1502
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1503
|
-
plugins: [new MetricsPlugin({
|
|
1504
|
-
enabled: true,
|
|
1505
|
-
|
|
1506
|
-
// Comprehensive monitoring
|
|
1507
|
-
collectPerformance: true,
|
|
1508
|
-
collectErrors: true,
|
|
1509
|
-
collectUsage: true,
|
|
1510
|
-
|
|
1511
|
-
// Advanced settings
|
|
1512
|
-
retentionDays: 90, // 3 months of data
|
|
1513
|
-
flushInterval: 10000, // 10 seconds
|
|
1514
|
-
sampleRate: 1.0, // 100% sampling
|
|
1515
|
-
|
|
1516
|
-
// Performance thresholds
|
|
1517
|
-
trackSlowQueries: true,
|
|
1518
|
-
slowQueryThreshold: 500, // 500ms threshold
|
|
1519
|
-
|
|
1520
|
-
// Storage optimization
|
|
1521
|
-
batchSize: 50,
|
|
1522
|
-
compressionEnabled: true,
|
|
1523
|
-
|
|
1524
|
-
// Custom alerting thresholds
|
|
1525
|
-
alertThresholds: {
|
|
1526
|
-
errorRate: 0.05, // 5% error rate
|
|
1527
|
-
avgResponseTime: 1000, // 1 second average
|
|
1528
|
-
memoryUsage: 0.9 // 90% memory usage
|
|
1529
|
-
},
|
|
1530
|
-
|
|
1531
|
-
// Event hooks
|
|
1532
|
-
onSlowQuery: (operation, resource, duration) => {
|
|
1533
|
-
console.warn(`🐌 Slow query: ${operation} on ${resource} took ${duration}ms`);
|
|
1534
|
-
},
|
|
1535
|
-
|
|
1536
|
-
onHighErrorRate: (resource, errorRate) => {
|
|
1537
|
-
console.error(`🚨 High error rate: ${resource} has ${(errorRate * 100).toFixed(1)}% errors`);
|
|
1538
|
-
},
|
|
1539
|
-
|
|
1540
|
-
onThresholdExceeded: (metric, value, threshold) => {
|
|
1541
|
-
console.warn(`⚠️ Threshold exceeded: ${metric} = ${value} (threshold: ${threshold})`);
|
|
1542
|
-
}
|
|
1543
|
-
})]
|
|
1544
|
-
});
|
|
1545
|
-
|
|
1546
|
-
await s3db.connect();
|
|
1547
|
-
|
|
1548
|
-
// Advanced metrics analysis class
|
|
1549
|
-
class MetricsAnalyzer {
|
|
1550
|
-
constructor(metricsPlugin) {
|
|
1551
|
-
this.plugin = metricsPlugin;
|
|
1552
|
-
this.alertHandlers = new Map();
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
addAlertHandler(condition, handler) {
|
|
1556
|
-
this.alertHandlers.set(condition, handler);
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
async analyzePerformance(timeRange = 3600000) { // 1 hour
|
|
1560
|
-
const metrics = await this.plugin.getMetrics();
|
|
1561
|
-
const analysis = {
|
|
1562
|
-
summary: {
|
|
1563
|
-
totalOperations: metrics.usage.totalOperations,
|
|
1564
|
-
avgResponseTime: metrics.performance.averageResponseTime,
|
|
1565
|
-
errorRate: metrics.errors.errorRate,
|
|
1566
|
-
slowQueries: metrics.performance.slowestOperations.length
|
|
1567
|
-
},
|
|
1568
|
-
recommendations: [],
|
|
1569
|
-
alerts: []
|
|
1570
|
-
};
|
|
1571
|
-
|
|
1572
|
-
// Performance analysis
|
|
1573
|
-
if (metrics.performance.averageResponseTime > 500) {
|
|
1574
|
-
analysis.recommendations.push({
|
|
1575
|
-
type: 'performance',
|
|
1576
|
-
message: 'Average response time is high. Consider adding caching or optimizing queries.',
|
|
1577
|
-
priority: 'high'
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
// Error rate analysis
|
|
1582
|
-
if (metrics.errors.errorRate > 0.02) { // 2%
|
|
1583
|
-
analysis.alerts.push({
|
|
1584
|
-
type: 'error_rate',
|
|
1585
|
-
message: `Error rate (${(metrics.errors.errorRate * 100).toFixed(2)}%) exceeds threshold`,
|
|
1586
|
-
severity: 'warning'
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// Resource usage patterns
|
|
1591
|
-
const resourceUsage = Object.entries(metrics.usage.resources);
|
|
1592
|
-
const imbalancedResources = resourceUsage.filter(([name, usage]) => {
|
|
1593
|
-
const writes = usage.inserts + usage.updates + usage.deletes;
|
|
1594
|
-
const reads = usage.reads;
|
|
1595
|
-
return writes > 0 && (reads / writes) < 0.1; // Very low read/write ratio
|
|
1596
|
-
});
|
|
1597
|
-
|
|
1598
|
-
if (imbalancedResources.length > 0) {
|
|
1599
|
-
analysis.recommendations.push({
|
|
1600
|
-
type: 'usage_pattern',
|
|
1601
|
-
message: `Resources with low read/write ratio: ${imbalancedResources.map(([name]) => name).join(', ')}`,
|
|
1602
|
-
priority: 'medium'
|
|
1603
|
-
});
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
return analysis;
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
async generateReport(format = 'console') {
|
|
1610
|
-
const metrics = await this.plugin.getMetrics();
|
|
1611
|
-
const analysis = await this.analyzePerformance();
|
|
1612
|
-
|
|
1613
|
-
if (format === 'console') {
|
|
1614
|
-
console.log('\n=== 📊 COMPREHENSIVE METRICS REPORT ===');
|
|
1615
|
-
|
|
1616
|
-
// Performance Summary
|
|
1617
|
-
console.log('\n🚀 Performance Summary:');
|
|
1618
|
-
console.log(` Total Operations: ${analysis.summary.totalOperations.toLocaleString()}`);
|
|
1619
|
-
console.log(` Average Response Time: ${analysis.summary.avgResponseTime}ms`);
|
|
1620
|
-
console.log(` Error Rate: ${(analysis.summary.errorRate * 100).toFixed(2)}%`);
|
|
1621
|
-
console.log(` Slow Queries: ${analysis.summary.slowQueries}`);
|
|
1622
|
-
|
|
1623
|
-
// Operation Breakdown
|
|
1624
|
-
console.log('\n⏱️ Operation Timing:');
|
|
1625
|
-
Object.entries(metrics.performance.operationTiming).forEach(([op, timing]) => {
|
|
1626
|
-
console.log(` ${op.toUpperCase()}:`);
|
|
1627
|
-
console.log(` Average: ${timing.avg}ms`);
|
|
1628
|
-
console.log(` Range: ${timing.min}ms - ${timing.max}ms`);
|
|
1629
|
-
console.log(` Count: ${timing.total}`);
|
|
1630
|
-
});
|
|
1631
|
-
|
|
1632
|
-
// Resource Activity
|
|
1633
|
-
console.log('\n📈 Resource Activity:');
|
|
1634
|
-
Object.entries(metrics.usage.resources)
|
|
1635
|
-
.sort(([,a], [,b]) => {
|
|
1636
|
-
const totalA = Object.values(a).reduce((sum, val) => sum + val, 0);
|
|
1637
|
-
const totalB = Object.values(b).reduce((sum, val) => sum + val, 0);
|
|
1638
|
-
return totalB - totalA;
|
|
1639
|
-
})
|
|
1640
|
-
.forEach(([resource, usage]) => {
|
|
1641
|
-
const total = Object.values(usage).reduce((sum, val) => sum + val, 0);
|
|
1642
|
-
console.log(` ${resource}: ${total} operations`);
|
|
1643
|
-
console.log(` Reads: ${usage.reads}, Writes: ${usage.inserts + usage.updates + usage.deletes}`);
|
|
1644
|
-
});
|
|
1645
|
-
|
|
1646
|
-
// Error Analysis
|
|
1647
|
-
if (metrics.errors.total > 0) {
|
|
1648
|
-
console.log('\n🚨 Error Analysis:');
|
|
1649
|
-
console.log(` Total Errors: ${metrics.errors.total}`);
|
|
1650
|
-
console.log(' By Type:');
|
|
1651
|
-
Object.entries(metrics.errors.byType).forEach(([type, count]) => {
|
|
1652
|
-
console.log(` ${type}: ${count}`);
|
|
1653
|
-
});
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
// Recommendations
|
|
1657
|
-
if (analysis.recommendations.length > 0) {
|
|
1658
|
-
console.log('\n💡 Recommendations:');
|
|
1659
|
-
analysis.recommendations.forEach(rec => {
|
|
1660
|
-
const emoji = rec.priority === 'high' ? '🔴' : rec.priority === 'medium' ? '🟡' : '🟢';
|
|
1661
|
-
console.log(` ${emoji} [${rec.priority.toUpperCase()}] ${rec.message}`);
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
// Alerts
|
|
1666
|
-
if (analysis.alerts.length > 0) {
|
|
1667
|
-
console.log('\n⚠️ Active Alerts:');
|
|
1668
|
-
analysis.alerts.forEach(alert => {
|
|
1669
|
-
console.log(` 🚨 ${alert.message}`);
|
|
1670
|
-
});
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
return { metrics, analysis };
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
async exportMetrics(filename) {
|
|
1678
|
-
const metrics = await this.plugin.getMetrics();
|
|
1679
|
-
const data = {
|
|
1680
|
-
timestamp: new Date().toISOString(),
|
|
1681
|
-
metrics,
|
|
1682
|
-
analysis: await this.analyzePerformance()
|
|
1683
|
-
};
|
|
1684
|
-
|
|
1685
|
-
// In real implementation, save to file
|
|
1686
|
-
console.log(`📁 Metrics exported to ${filename}`);
|
|
1687
|
-
return data;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
startRealTimeMonitoring(interval = 5000) {
|
|
1691
|
-
const monitor = setInterval(async () => {
|
|
1692
|
-
const metrics = await this.plugin.getMetrics();
|
|
1693
|
-
|
|
1694
|
-
// Check alert conditions
|
|
1695
|
-
this.alertHandlers.forEach((handler, condition) => {
|
|
1696
|
-
if (condition(metrics)) {
|
|
1697
|
-
handler(metrics);
|
|
1698
|
-
}
|
|
1699
|
-
});
|
|
1700
|
-
|
|
1701
|
-
// Auto-optimization suggestions
|
|
1702
|
-
if (metrics.performance.averageResponseTime > 1000) {
|
|
1703
|
-
console.log('💡 Suggestion: Consider implementing caching for frequently accessed data');
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
if (metrics.errors.errorRate > 0.05) {
|
|
1707
|
-
console.log('🚨 Alert: Error rate is above 5% - investigate immediately');
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
}, interval);
|
|
1711
|
-
|
|
1712
|
-
return monitor;
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
|
|
1716
|
-
// Simulate complex workload
|
|
1717
|
-
const users = s3db.resource('users');
|
|
1718
|
-
const products = s3db.resource('products');
|
|
1719
|
-
const orders = s3db.resource('orders');
|
|
1720
|
-
|
|
1721
|
-
// Setup metrics analyzer
|
|
1722
|
-
const analyzer = new MetricsAnalyzer(s3db.plugins.metrics);
|
|
1723
|
-
|
|
1724
|
-
// Add custom alert handlers
|
|
1725
|
-
analyzer.addAlertHandler(
|
|
1726
|
-
(metrics) => metrics.errors.errorRate > 0.03,
|
|
1727
|
-
(metrics) => console.log('🚨 Error rate alert triggered!')
|
|
1728
|
-
);
|
|
1729
|
-
|
|
1730
|
-
analyzer.addAlertHandler(
|
|
1731
|
-
(metrics) => metrics.performance.averageResponseTime > 800,
|
|
1732
|
-
(metrics) => console.log('⏰ Performance degradation detected!')
|
|
1733
|
-
);
|
|
1734
|
-
|
|
1735
|
-
// Simulate workload
|
|
1736
|
-
console.log('🔄 Simulating complex workload...');
|
|
1737
|
-
|
|
1738
|
-
// Bulk operations
|
|
1739
|
-
const userData = Array.from({ length: 50 }, (_, i) => ({
|
|
1740
|
-
name: `User ${i}`,
|
|
1741
|
-
email: `user${i}@example.com`,
|
|
1742
|
-
role: i % 3 === 0 ? 'admin' : 'user'
|
|
1743
|
-
}));
|
|
1744
|
-
|
|
1745
|
-
await users.insertMany(userData);
|
|
1746
|
-
|
|
1747
|
-
// Mixed operations with some errors
|
|
1748
|
-
for (let i = 0; i < 20; i++) {
|
|
1749
|
-
try {
|
|
1750
|
-
await products.insert({
|
|
1751
|
-
name: `Product ${i}`,
|
|
1752
|
-
price: Math.random() * 100,
|
|
1753
|
-
category: ['electronics', 'books', 'clothing'][i % 3]
|
|
1754
|
-
});
|
|
1755
|
-
|
|
1756
|
-
if (i % 5 === 0) {
|
|
1757
|
-
// Simulate some slow operations
|
|
1758
|
-
await new Promise(resolve => setTimeout(resolve, 600));
|
|
1759
|
-
await products.list({ limit: 20 });
|
|
1760
|
-
}
|
|
1761
|
-
|
|
1762
|
-
if (i % 10 === 0) {
|
|
1763
|
-
// Simulate some errors
|
|
1764
|
-
try {
|
|
1765
|
-
await products.get('non-existent-id');
|
|
1766
|
-
} catch (error) {
|
|
1767
|
-
// Expected error for testing
|
|
1768
|
-
}
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
} catch (error) {
|
|
1772
|
-
// Handle errors
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
// Generate comprehensive report
|
|
1777
|
-
await analyzer.generateReport();
|
|
1778
|
-
|
|
1779
|
-
// Start real-time monitoring
|
|
1780
|
-
const monitor = analyzer.startRealTimeMonitoring(3000);
|
|
1781
|
-
|
|
1782
|
-
// Export metrics for external analysis
|
|
1783
|
-
await analyzer.exportMetrics('metrics-export.json');
|
|
1784
|
-
|
|
1785
|
-
// Stop monitoring after demo
|
|
1786
|
-
setTimeout(() => {
|
|
1787
|
-
clearInterval(monitor);
|
|
1788
|
-
console.log('\n✅ Metrics demonstration completed');
|
|
1789
|
-
}, 15000);
|
|
1790
|
-
```
|
|
1791
|
-
|
|
1792
|
-
---
|
|
1793
|
-
|
|
1794
|
-
## 🔄 Replicator Plugin
|
|
1795
|
-
|
|
1796
|
-
**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.
|
|
1797
|
-
|
|
1798
|
-
### 🎯 Key Features
|
|
1799
|
-
|
|
1800
|
-
- **Real-time Replication**: Automatic data synchronization on insert, update, and delete operations
|
|
1801
|
-
- **Multi-Target Support**: Replicate to S3DB, BigQuery, PostgreSQL, SQS, and custom targets
|
|
1802
|
-
- **Advanced Transformations**: Transform data with custom functions before replication
|
|
1803
|
-
- **Error Resilience**: Automatic retries, detailed error reporting, and dead letter queue support
|
|
1804
|
-
- **Performance Monitoring**: Built-in metrics, performance tracking, and health monitoring
|
|
1805
|
-
- **Flexible Configuration**: Support for multiple resource mapping syntaxes and selective replication
|
|
1806
|
-
|
|
1807
|
-
### ⚡ Quick Start
|
|
1808
|
-
|
|
1809
|
-
```javascript
|
|
1810
|
-
import { S3db, ReplicatorPlugin } from 's3db.js';
|
|
1811
|
-
|
|
1812
|
-
const s3db = new S3db({
|
|
1813
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
1814
|
-
plugins: [new ReplicatorPlugin({
|
|
1815
|
-
verbose: true, // Enable detailed logging for debugging
|
|
1816
|
-
replicators: [
|
|
1817
|
-
{
|
|
1818
|
-
driver: 's3db',
|
|
1819
|
-
resources: ['users'],
|
|
1820
|
-
config: {
|
|
1821
|
-
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
]
|
|
1825
|
-
})]
|
|
1826
|
-
});
|
|
1827
|
-
|
|
1828
|
-
await s3db.connect();
|
|
1829
|
-
|
|
1830
|
-
// Data is automatically replicated with detailed error reporting
|
|
1831
|
-
const users = s3db.resource('users');
|
|
1832
|
-
await users.insert({ name: 'John', email: 'john@example.com' });
|
|
1833
|
-
// This insert is automatically replicated to the backup database
|
|
1834
|
-
```
|
|
1835
|
-
|
|
1836
|
-
### ⚙️ Configuration Parameters
|
|
1837
|
-
|
|
1838
|
-
| Parameter | Type | Default | Description |
|
|
1839
|
-
|-----------|------|---------|-------------|
|
|
1840
|
-
| `enabled` | boolean | `true` | Enable/disable replication globally |
|
|
1841
|
-
| `replicators` | array | `[]` | Array of replicator configurations (required) |
|
|
1842
|
-
| `verbose` | boolean | `false` | Enable detailed console logging for debugging |
|
|
1843
|
-
| `persistReplicatorLog` | boolean | `false` | Store replication logs in database resource |
|
|
1844
|
-
| `replicatorLogResource` | string | `'replicator_log'` | Name of log resource for persistence |
|
|
1845
|
-
| `logErrors` | boolean | `true` | Log errors to replication log resource |
|
|
1846
|
-
| `batchSize` | number | `100` | Batch size for bulk replication operations |
|
|
1847
|
-
| `maxRetries` | number | `3` | Maximum retry attempts for failed replications |
|
|
1848
|
-
| `timeout` | number | `30000` | Timeout for replication operations (ms) |
|
|
1849
|
-
|
|
1850
|
-
### 🗂️ Event System & Debugging
|
|
1851
|
-
|
|
1852
|
-
The Replicator Plugin emits comprehensive events for monitoring and debugging:
|
|
1853
|
-
|
|
1854
|
-
```javascript
|
|
1855
|
-
const replicatorPlugin = s3db.plugins.find(p => p.constructor.name === 'ReplicatorPlugin');
|
|
1856
|
-
|
|
1857
|
-
// Success events
|
|
1858
|
-
replicatorPlugin.on('replicated', (data) => {
|
|
1859
|
-
console.log(`✅ Replicated: ${data.operation} on ${data.resourceName} to ${data.replicator}`);
|
|
1860
|
-
});
|
|
1861
|
-
|
|
1862
|
-
// Error events
|
|
1863
|
-
replicatorPlugin.on('replicator_error', (data) => {
|
|
1864
|
-
console.error(`❌ Replication failed: ${data.error} (${data.resourceName})`);
|
|
1865
|
-
});
|
|
1866
|
-
|
|
1867
|
-
// Log resource errors
|
|
1868
|
-
replicatorPlugin.on('replicator_log_error', (data) => {
|
|
1869
|
-
console.warn(`⚠️ Failed to log replication: ${data.logError}`);
|
|
1870
|
-
});
|
|
1871
|
-
|
|
1872
|
-
// Setup errors
|
|
1873
|
-
replicatorPlugin.on('replicator_log_resource_creation_error', (data) => {
|
|
1874
|
-
console.error(`🚨 Log resource creation failed: ${data.error}`);
|
|
1875
|
-
});
|
|
1876
|
-
|
|
1877
|
-
// Cleanup errors
|
|
1878
|
-
replicatorPlugin.on('replicator_cleanup_error', (data) => {
|
|
1879
|
-
console.warn(`🧹 Cleanup failed for ${data.replicator}: ${data.error}`);
|
|
1880
|
-
});
|
|
1881
|
-
```
|
|
1882
|
-
|
|
1883
|
-
### 🔧 Replicator Drivers
|
|
1884
|
-
|
|
1885
|
-
#### 🗃️ S3DB Replicator
|
|
1886
|
-
|
|
1887
|
-
Replicate to another S3DB instance with **advanced resource mapping and transformation capabilities**. Supports multiple configuration syntaxes for maximum flexibility.
|
|
1888
|
-
|
|
1889
|
-
**Basic Configuration:**
|
|
1890
|
-
```javascript
|
|
1891
|
-
{
|
|
1892
|
-
driver: 's3db',
|
|
1893
|
-
config: {
|
|
1894
|
-
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
1895
|
-
},
|
|
1896
|
-
resources: {
|
|
1897
|
-
// Simple resource mapping (replicate to same name)
|
|
1898
|
-
users: 'users',
|
|
1899
|
-
|
|
1900
|
-
// Map source → destination resource name
|
|
1901
|
-
products: 'backup_products',
|
|
1902
|
-
|
|
1903
|
-
// Advanced mapping with transform function
|
|
1904
|
-
orders: {
|
|
1905
|
-
resource: 'order_backup',
|
|
1906
|
-
transform: (data) => ({
|
|
1907
|
-
...data,
|
|
1908
|
-
backup_timestamp: new Date().toISOString(),
|
|
1909
|
-
original_source: 'production',
|
|
1910
|
-
migrated_at: new Date().toISOString()
|
|
1911
|
-
}),
|
|
1912
|
-
actions: ['insert', 'update', 'delete']
|
|
1913
|
-
}
|
|
1914
|
-
}
|
|
1915
|
-
}
|
|
1916
|
-
```
|
|
1917
|
-
|
|
1918
|
-
### 📋 Resource Configuration Syntaxes
|
|
1919
|
-
|
|
1920
|
-
The S3DB replicator supports **multiple configuration syntaxes** for maximum flexibility. You can mix and match these formats as needed:
|
|
1921
|
-
|
|
1922
|
-
#### 1. Array of Resource Names
|
|
1923
|
-
**Use case**: Simple backup/clone scenarios
|
|
1924
|
-
```javascript
|
|
1925
|
-
resources: ['users', 'products', 'orders']
|
|
1926
|
-
// Replicates each resource to itself in the destination database
|
|
1927
|
-
```
|
|
1928
|
-
|
|
1929
|
-
#### 2. Simple Object Mapping
|
|
1930
|
-
**Use case**: Rename resources during replication
|
|
1931
|
-
```javascript
|
|
1932
|
-
resources: {
|
|
1933
|
-
users: 'people', // users → people
|
|
1934
|
-
products: 'items', // products → items
|
|
1935
|
-
orders: 'order_history' // orders → order_history
|
|
1936
|
-
}
|
|
1937
|
-
```
|
|
1938
|
-
|
|
1939
|
-
#### 3. Object with Transform Function
|
|
1940
|
-
**Use case**: Data transformation during replication ⭐ **RECOMMENDED**
|
|
1941
|
-
```javascript
|
|
1942
|
-
resources: {
|
|
1943
|
-
users: {
|
|
1944
|
-
resource: 'people', // Destination resource name
|
|
1945
|
-
transform: (data) => ({ // Data transformation function
|
|
1946
|
-
...data,
|
|
1947
|
-
fullName: `${data.firstName} ${data.lastName}`,
|
|
1948
|
-
migrated_at: new Date().toISOString(),
|
|
1949
|
-
source_system: 'production'
|
|
1950
|
-
}),
|
|
1951
|
-
actions: ['insert', 'update', 'delete'] // Optional: which operations to replicate
|
|
1952
|
-
}
|
|
1953
|
-
}
|
|
1954
|
-
```
|
|
1955
|
-
|
|
1956
|
-
#### 4. Function-Only Transformation
|
|
1957
|
-
**Use case**: Transform data without changing resource name
|
|
1958
|
-
```javascript
|
|
1959
|
-
resources: {
|
|
1960
|
-
users: (data) => ({
|
|
1961
|
-
...data,
|
|
1962
|
-
processed: true,
|
|
1963
|
-
backup_date: new Date().toISOString(),
|
|
1964
|
-
hash: crypto.createHash('md5').update(JSON.stringify(data)).digest('hex')
|
|
1965
|
-
})
|
|
1966
|
-
}
|
|
1967
|
-
```
|
|
1968
|
-
|
|
1969
|
-
#### 5. Multi-Destination Replication
|
|
1970
|
-
**Use case**: Send data to multiple targets with different transformations
|
|
1971
|
-
```javascript
|
|
1972
|
-
resources: {
|
|
1973
|
-
users: [
|
|
1974
|
-
'people', // Simple copy to 'people'
|
|
1975
|
-
{
|
|
1976
|
-
resource: 'user_analytics',
|
|
1977
|
-
transform: (data) => ({ // Transformed copy to 'user_analytics'
|
|
1978
|
-
id: data.id,
|
|
1979
|
-
signup_date: data.createdAt,
|
|
1980
|
-
user_type: data.role || 'standard',
|
|
1981
|
-
last_activity: new Date().toISOString()
|
|
1982
|
-
})
|
|
1983
|
-
},
|
|
1984
|
-
{
|
|
1985
|
-
resource: 'audit_trail',
|
|
1986
|
-
transform: (data) => ({ // Audit copy to 'audit_trail'
|
|
1987
|
-
user_id: data.id,
|
|
1988
|
-
action: 'user_replicated',
|
|
1989
|
-
timestamp: new Date().toISOString(),
|
|
1990
|
-
data_hash: crypto.createHash('sha256').update(JSON.stringify(data)).digest('hex')
|
|
1991
|
-
})
|
|
1992
|
-
}
|
|
1993
|
-
]
|
|
1994
|
-
}
|
|
1995
|
-
```
|
|
1996
|
-
|
|
1997
|
-
#### 6. Advanced Mixed Configuration
|
|
1998
|
-
**Use case**: Complex enterprise scenarios
|
|
1999
|
-
```javascript
|
|
2000
|
-
resources: {
|
|
2001
|
-
// Simple mappings
|
|
2002
|
-
sessions: 'user_sessions',
|
|
2003
|
-
|
|
2004
|
-
// Transform without renaming
|
|
2005
|
-
products: (data) => ({
|
|
2006
|
-
...data,
|
|
2007
|
-
search_keywords: data.name?.toLowerCase().split(' ') || [],
|
|
2008
|
-
price_category: data.price > 100 ? 'premium' : 'standard'
|
|
2009
|
-
}),
|
|
2010
|
-
|
|
2011
|
-
// Complex multi-destination with conditions
|
|
2012
|
-
orders: [
|
|
2013
|
-
'order_backup', // Simple backup
|
|
2014
|
-
{
|
|
2015
|
-
resource: 'order_analytics',
|
|
2016
|
-
transform: (data) => ({
|
|
2017
|
-
order_id: data.id,
|
|
2018
|
-
customer_id: data.userId,
|
|
2019
|
-
amount: data.amount,
|
|
2020
|
-
order_date: data.createdAt?.split('T')[0],
|
|
2021
|
-
status: data.status || 'pending',
|
|
2022
|
-
item_count: data.items?.length || 0,
|
|
2023
|
-
is_large_order: data.amount > 1000
|
|
2024
|
-
}),
|
|
2025
|
-
actions: ['insert', 'update'] // Only replicate inserts and updates, not deletes
|
|
2026
|
-
},
|
|
2027
|
-
{
|
|
2028
|
-
resource: 'financial_records',
|
|
2029
|
-
transform: (data) => ({
|
|
2030
|
-
transaction_id: data.id,
|
|
2031
|
-
amount: data.amount,
|
|
2032
|
-
currency: data.currency || 'USD',
|
|
2033
|
-
type: 'order_payment',
|
|
2034
|
-
timestamp: new Date().toISOString(),
|
|
2035
|
-
metadata: {
|
|
2036
|
-
customer_id: data.userId,
|
|
2037
|
-
order_items: data.items?.length || 0
|
|
2038
|
-
}
|
|
2039
|
-
}),
|
|
2040
|
-
actions: ['insert'] // Only replicate new orders for financial records
|
|
2041
|
-
}
|
|
2042
|
-
],
|
|
2043
|
-
|
|
2044
|
-
// Conditional replication
|
|
2045
|
-
users: {
|
|
2046
|
-
resource: 'customer_profiles',
|
|
2047
|
-
transform: (data) => {
|
|
2048
|
-
// Only replicate active users
|
|
2049
|
-
if (data.status !== 'active') return null;
|
|
2050
|
-
|
|
2051
|
-
return {
|
|
2052
|
-
...data,
|
|
2053
|
-
fullName: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
|
|
2054
|
-
account_age_days: data.createdAt ?
|
|
2055
|
-
Math.floor((Date.now() - new Date(data.createdAt)) / (1000 * 60 * 60 * 24)) : 0,
|
|
2056
|
-
preferences: data.preferences || {},
|
|
2057
|
-
last_updated: new Date().toISOString()
|
|
2058
|
-
};
|
|
2059
|
-
},
|
|
2060
|
-
actions: ['insert', 'update']
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
```
|
|
2064
|
-
|
|
2065
|
-
### 🔧 S3DB Replicator Configuration Options
|
|
2066
|
-
|
|
2067
|
-
| Parameter | Type | Required | Description |
|
|
2068
|
-
|-----------|------|----------|-------------|
|
|
2069
|
-
| `connectionString` | string | Yes* | S3DB connection string for destination database |
|
|
2070
|
-
| `client` | S3db | Yes* | Pre-configured S3DB client instance |
|
|
2071
|
-
| `resources` | object/array | Yes | Resource mapping configuration (see syntaxes above) |
|
|
2072
|
-
| `timeout` | number | No | Operation timeout in milliseconds (default: 30000) |
|
|
2073
|
-
| `retryAttempts` | number | No | Number of retry attempts for failed operations (default: 3) |
|
|
2074
|
-
|
|
2075
|
-
*Either `connectionString` or `client` must be provided.
|
|
2076
|
-
|
|
2077
|
-
### 🎯 Transform Function Features
|
|
2078
|
-
|
|
2079
|
-
Transform functions provide powerful data manipulation capabilities:
|
|
2080
|
-
|
|
2081
|
-
#### Data Transformation Examples:
|
|
2082
|
-
```javascript
|
|
2083
|
-
// 1. Field mapping and enrichment
|
|
2084
|
-
transform: (data) => ({
|
|
2085
|
-
id: data.id,
|
|
2086
|
-
customer_name: `${data.firstName} ${data.lastName}`,
|
|
2087
|
-
email_domain: data.email?.split('@')[1] || 'unknown',
|
|
2088
|
-
created_timestamp: Date.now(),
|
|
2089
|
-
source: 'production-db'
|
|
2090
|
-
})
|
|
2091
|
-
|
|
2092
|
-
// 2. Conditional logic
|
|
2093
|
-
transform: (data) => {
|
|
2094
|
-
if (data.type === 'premium') {
|
|
2095
|
-
return { ...data, priority: 'high', sla: '4hours' };
|
|
2096
|
-
}
|
|
2097
|
-
return { ...data, priority: 'normal', sla: '24hours' };
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
// 3. Data validation and filtering
|
|
2101
|
-
transform: (data) => {
|
|
2102
|
-
// Skip replication for invalid data
|
|
2103
|
-
if (!data.email || !data.name) return null;
|
|
2104
|
-
|
|
2105
|
-
return {
|
|
2106
|
-
...data,
|
|
2107
|
-
email: data.email.toLowerCase(),
|
|
2108
|
-
name: data.name.trim(),
|
|
2109
|
-
validated: true
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
// 4. Computed fields
|
|
2114
|
-
transform: (data) => ({
|
|
2115
|
-
...data,
|
|
2116
|
-
age: data.birthDate ?
|
|
2117
|
-
Math.floor((Date.now() - new Date(data.birthDate)) / (1000 * 60 * 60 * 24 * 365)) : null,
|
|
2118
|
-
account_value: (data.orders || []).reduce((sum, order) => sum + order.amount, 0),
|
|
2119
|
-
last_activity: new Date().toISOString()
|
|
2120
|
-
})
|
|
2121
|
-
|
|
2122
|
-
// 5. Data aggregation
|
|
2123
|
-
transform: (data) => ({
|
|
2124
|
-
user_id: data.id,
|
|
2125
|
-
total_orders: data.orderHistory?.length || 0,
|
|
2126
|
-
total_spent: data.orderHistory?.reduce((sum, order) => sum + order.amount, 0) || 0,
|
|
2127
|
-
favorite_category: data.orderHistory?.map(o => o.category)
|
|
2128
|
-
.sort((a,b) => a.localeCompare(b))
|
|
2129
|
-
.reduce((prev, curr, i, arr) =>
|
|
2130
|
-
arr.filter(v => v === prev).length >= arr.filter(v => v === curr).length ? prev : curr
|
|
2131
|
-
) || null,
|
|
2132
|
-
analysis_date: new Date().toISOString()
|
|
2133
|
-
})
|
|
2134
|
-
```
|
|
2135
|
-
|
|
2136
|
-
**Transform Function Best Practices:**
|
|
2137
|
-
- **Return `null`** to skip replication for specific records
|
|
2138
|
-
- **Preserve the `id` field** unless specifically mapping to a different field
|
|
2139
|
-
- **Handle edge cases** like missing fields or null values
|
|
2140
|
-
- **Use immutable operations** to avoid modifying the original data
|
|
2141
|
-
- **Keep transforms lightweight** to maintain replication performance
|
|
2142
|
-
- **Add metadata fields** like timestamps for tracking purposes
|
|
2143
|
-
|
|
2144
|
-
#### 📬 SQS Replicator
|
|
2145
|
-
|
|
2146
|
-
**Real-time event streaming** to AWS SQS queues for microservices integration and event-driven architectures.
|
|
2147
|
-
|
|
2148
|
-
**⚠️ Required Dependency:**
|
|
2149
|
-
```bash
|
|
2150
|
-
pnpm add @aws-sdk/client-sqs
|
|
2151
|
-
```
|
|
2152
|
-
|
|
2153
|
-
**Basic Configuration:**
|
|
2154
|
-
```javascript
|
|
2155
|
-
{
|
|
2156
|
-
driver: 'sqs',
|
|
2157
|
-
resources: ['orders', 'users'],
|
|
2158
|
-
config: {
|
|
2159
|
-
region: 'us-east-1',
|
|
2160
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/events.fifo',
|
|
2161
|
-
messageGroupId: 's3db-events',
|
|
2162
|
-
deduplicationId: true
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
```
|
|
2166
|
-
|
|
2167
|
-
**Advanced Configuration with Resource-Specific Queues:**
|
|
2168
|
-
```javascript
|
|
2169
|
-
{
|
|
2170
|
-
driver: 'sqs',
|
|
2171
|
-
config: {
|
|
2172
|
-
region: 'us-east-1',
|
|
2173
|
-
credentials: {
|
|
2174
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
2175
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
2176
|
-
},
|
|
2177
|
-
|
|
2178
|
-
// Resource-specific queue URLs
|
|
2179
|
-
queues: {
|
|
2180
|
-
orders: 'https://sqs.us-east-1.amazonaws.com/123456789012/order-events.fifo',
|
|
2181
|
-
users: 'https://sqs.us-east-1.amazonaws.com/123456789012/user-events.fifo',
|
|
2182
|
-
payments: 'https://sqs.us-east-1.amazonaws.com/123456789012/payment-events.fifo'
|
|
2183
|
-
},
|
|
2184
|
-
|
|
2185
|
-
// Default queue for resources not specifically mapped
|
|
2186
|
-
defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/general-events.fifo',
|
|
2187
|
-
|
|
2188
|
-
// FIFO queue settings
|
|
2189
|
-
messageGroupId: 's3db-replicator',
|
|
2190
|
-
deduplicationId: true,
|
|
2191
|
-
|
|
2192
|
-
// Message attributes (applied to all messages)
|
|
2193
|
-
messageAttributes: {
|
|
2194
|
-
source: { StringValue: 'production-db', DataType: 'String' },
|
|
2195
|
-
version: { StringValue: '1.0', DataType: 'String' },
|
|
2196
|
-
environment: { StringValue: process.env.NODE_ENV || 'development', DataType: 'String' }
|
|
2197
|
-
}
|
|
2198
|
-
},
|
|
2199
|
-
resources: {
|
|
2200
|
-
// Simple resource mapping
|
|
2201
|
-
orders: true,
|
|
2202
|
-
users: true,
|
|
2203
|
-
|
|
2204
|
-
// Resource with transformation
|
|
2205
|
-
payments: {
|
|
2206
|
-
transform: (data) => ({
|
|
2207
|
-
payment_id: data.id,
|
|
2208
|
-
amount: data.amount,
|
|
2209
|
-
currency: data.currency || 'USD',
|
|
2210
|
-
customer_id: data.userId,
|
|
2211
|
-
payment_method: data.method,
|
|
2212
|
-
status: data.status,
|
|
2213
|
-
timestamp: new Date().toISOString(),
|
|
2214
|
-
|
|
2215
|
-
// Add computed fields
|
|
2216
|
-
amount_usd: data.currency === 'USD' ? data.amount : data.amount * (data.exchange_rate || 1),
|
|
2217
|
-
is_large_payment: data.amount > 1000,
|
|
2218
|
-
risk_score: data.amount > 5000 ? 'high' : data.amount > 1000 ? 'medium' : 'low'
|
|
2219
|
-
})
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
```
|
|
2224
|
-
|
|
2225
|
-
**SQS Message Format:**
|
|
2226
|
-
```javascript
|
|
2227
|
-
{
|
|
2228
|
-
// Message Body (JSON string)
|
|
2229
|
-
MessageBody: JSON.stringify({
|
|
2230
|
-
resource: 'orders', // Source resource name
|
|
2231
|
-
operation: 'insert', // Operation: insert, update, delete
|
|
2232
|
-
id: 'order-123', // Record ID
|
|
2233
|
-
data: { // Transformed data payload
|
|
2234
|
-
id: 'order-123',
|
|
2235
|
-
userId: 'user-456',
|
|
2236
|
-
amount: 99.99,
|
|
2237
|
-
status: 'pending',
|
|
2238
|
-
timestamp: '2024-01-15T10:30:00.000Z'
|
|
2239
|
-
},
|
|
2240
|
-
beforeData: null, // Previous data for updates
|
|
2241
|
-
metadata: {
|
|
2242
|
-
source: 'production-db',
|
|
2243
|
-
replicator: 'sqs-replicator',
|
|
2244
|
-
timestamp: '2024-01-15T10:30:00.000Z'
|
|
2245
|
-
}
|
|
2246
|
-
}),
|
|
2247
|
-
|
|
2248
|
-
// Message Attributes
|
|
2249
|
-
MessageAttributes: {
|
|
2250
|
-
source: { StringValue: 'production-db', DataType: 'String' },
|
|
2251
|
-
resource: { StringValue: 'orders', DataType: 'String' },
|
|
2252
|
-
operation: { StringValue: 'insert', DataType: 'String' }
|
|
2253
|
-
},
|
|
2254
|
-
|
|
2255
|
-
// FIFO Queue Settings
|
|
2256
|
-
MessageGroupId: 's3db-events',
|
|
2257
|
-
MessageDeduplicationId: 'orders:insert:order-123'
|
|
2258
|
-
}
|
|
2259
|
-
```
|
|
2260
|
-
|
|
2261
|
-
**Configuration Options:**
|
|
2262
|
-
|
|
2263
|
-
| Parameter | Type | Required | Description |
|
|
2264
|
-
|-----------|------|----------|-------------|
|
|
2265
|
-
| `region` | string | Yes | AWS region for SQS service |
|
|
2266
|
-
| `queueUrl` | string | Yes* | Single queue URL for all resources |
|
|
2267
|
-
| `queues` | object | Yes* | Resource-specific queue mapping |
|
|
2268
|
-
| `defaultQueueUrl` | string | No | Fallback queue for unmapped resources |
|
|
2269
|
-
| `credentials` | object | No | AWS credentials (uses default chain if omitted) |
|
|
2270
|
-
| `messageGroupId` | string | No | Message group ID for FIFO queues |
|
|
2271
|
-
| `deduplicationId` | boolean | No | Enable message deduplication for FIFO queues |
|
|
2272
|
-
| `messageAttributes` | object | No | Custom message attributes applied to all messages |
|
|
2273
|
-
|
|
2274
|
-
*Either `queueUrl` or `queues` must be provided.
|
|
2275
|
-
|
|
2276
|
-
#### 📊 BigQuery Replicator
|
|
2277
|
-
|
|
2278
|
-
**Data warehouse integration** for Google BigQuery with advanced transformation capabilities, multi-table support, and automatic retry logic for streaming buffer limitations.
|
|
2279
|
-
|
|
2280
|
-
**⚠️ Required Dependency:**
|
|
2281
|
-
```bash
|
|
2282
|
-
pnpm add @google-cloud/bigquery
|
|
2283
|
-
```
|
|
2284
|
-
|
|
2285
|
-
**Basic Configuration:**
|
|
2286
|
-
```javascript
|
|
2287
|
-
{
|
|
2288
|
-
driver: 'bigquery',
|
|
2289
|
-
config: {
|
|
2290
|
-
projectId: 'my-analytics-project',
|
|
2291
|
-
datasetId: 'production_data',
|
|
2292
|
-
location: 'US',
|
|
2293
|
-
credentials: {
|
|
2294
|
-
client_email: 'service-account@project.iam.gserviceaccount.com',
|
|
2295
|
-
private_key: process.env.BIGQUERY_PRIVATE_KEY,
|
|
2296
|
-
project_id: 'my-analytics-project'
|
|
2297
|
-
}
|
|
2298
|
-
},
|
|
2299
|
-
resources: {
|
|
2300
|
-
// Simple table mapping
|
|
2301
|
-
users: 'users_table',
|
|
2302
|
-
orders: 'orders_table',
|
|
2303
|
-
products: 'products_table'
|
|
2304
|
-
}
|
|
2305
|
-
}
|
|
2306
|
-
```
|
|
2307
|
-
|
|
2308
|
-
**Advanced Multi-Table Configuration:**
|
|
2309
|
-
```javascript
|
|
2310
|
-
{
|
|
2311
|
-
driver: 'bigquery',
|
|
2312
|
-
config: {
|
|
2313
|
-
projectId: 'my-analytics-project',
|
|
2314
|
-
datasetId: 'analytics_warehouse',
|
|
2315
|
-
location: 'US',
|
|
2316
|
-
logTable: 'replication_audit_log', // Optional: audit all operations
|
|
2317
|
-
credentials: JSON.parse(Buffer.from(process.env.GOOGLE_CREDENTIALS, 'base64').toString())
|
|
2318
|
-
},
|
|
2319
|
-
resources: {
|
|
2320
|
-
// Simple string mapping
|
|
2321
|
-
sessions: 'user_sessions',
|
|
2322
|
-
clicks: 'click_events',
|
|
2323
|
-
|
|
2324
|
-
// Single table with actions and transforms
|
|
2325
|
-
users: {
|
|
2326
|
-
table: 'dim_users',
|
|
2327
|
-
actions: ['insert', 'update'],
|
|
2328
|
-
transform: (data) => ({
|
|
2329
|
-
...data,
|
|
2330
|
-
// Data cleaning and enrichment
|
|
2331
|
-
email: data.email?.toLowerCase(),
|
|
2332
|
-
full_name: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
|
|
2333
|
-
registration_date: data.createdAt?.split('T')[0],
|
|
2334
|
-
account_age_days: data.createdAt ?
|
|
2335
|
-
Math.floor((Date.now() - new Date(data.createdAt)) / (1000 * 60 * 60 * 24)) : 0,
|
|
2336
|
-
|
|
2337
|
-
// Computed fields for analytics
|
|
2338
|
-
user_tier: data.totalSpent > 1000 ? 'premium' :
|
|
2339
|
-
data.totalSpent > 100 ? 'standard' : 'basic',
|
|
2340
|
-
is_active: data.lastLoginAt &&
|
|
2341
|
-
(Date.now() - new Date(data.lastLoginAt)) < (30 * 24 * 60 * 60 * 1000),
|
|
2342
|
-
|
|
2343
|
-
// BigQuery-specific fields
|
|
2344
|
-
processed_at: new Date().toISOString(),
|
|
2345
|
-
data_source: 'production_s3db'
|
|
2346
|
-
})
|
|
2347
|
-
},
|
|
2348
|
-
|
|
2349
|
-
// Multiple destinations for comprehensive analytics
|
|
2350
|
-
orders: [
|
|
2351
|
-
// Fact table for detailed order data
|
|
2352
|
-
{
|
|
2353
|
-
table: 'fact_orders',
|
|
2354
|
-
actions: ['insert', 'update'],
|
|
2355
|
-
transform: (data) => ({
|
|
2356
|
-
order_id: data.id,
|
|
2357
|
-
customer_id: data.userId,
|
|
2358
|
-
order_date: data.createdAt?.split('T')[0],
|
|
2359
|
-
order_timestamp: data.createdAt,
|
|
2360
|
-
amount: parseFloat(data.amount) || 0,
|
|
2361
|
-
currency: data.currency || 'USD',
|
|
2362
|
-
status: data.status,
|
|
2363
|
-
item_count: data.items?.length || 0,
|
|
2364
|
-
|
|
2365
|
-
// Derived analytics fields
|
|
2366
|
-
is_weekend_order: new Date(data.createdAt).getDay() % 6 === 0,
|
|
2367
|
-
order_hour: new Date(data.createdAt).getHours(),
|
|
2368
|
-
is_large_order: data.amount > 500,
|
|
2369
|
-
|
|
2370
|
-
// Metadata
|
|
2371
|
-
ingested_at: new Date().toISOString(),
|
|
2372
|
-
source_system: 'production'
|
|
2373
|
-
})
|
|
2374
|
-
},
|
|
2375
|
-
|
|
2376
|
-
// Daily aggregation table
|
|
2377
|
-
{
|
|
2378
|
-
table: 'daily_revenue_summary',
|
|
2379
|
-
actions: ['insert'], // Only for new orders
|
|
2380
|
-
transform: (data) => ({
|
|
2381
|
-
date: data.createdAt?.split('T')[0],
|
|
2382
|
-
revenue: parseFloat(data.amount) || 0,
|
|
2383
|
-
currency: data.currency || 'USD',
|
|
2384
|
-
order_count: 1,
|
|
2385
|
-
customer_id: data.userId,
|
|
2386
|
-
|
|
2387
|
-
// Additional dimensions
|
|
2388
|
-
order_source: data.source || 'web',
|
|
2389
|
-
payment_method: data.paymentMethod,
|
|
2390
|
-
is_first_order: data.isFirstOrder || false,
|
|
2391
|
-
|
|
2392
|
-
created_at: new Date().toISOString()
|
|
2393
|
-
})
|
|
2394
|
-
},
|
|
2395
|
-
|
|
2396
|
-
// Customer analytics (updates only)
|
|
2397
|
-
{
|
|
2398
|
-
table: 'customer_order_analytics',
|
|
2399
|
-
actions: ['insert', 'update'],
|
|
2400
|
-
transform: (data) => ({
|
|
2401
|
-
customer_id: data.userId,
|
|
2402
|
-
latest_order_id: data.id,
|
|
2403
|
-
latest_order_date: data.createdAt?.split('T')[0],
|
|
2404
|
-
latest_order_amount: parseFloat(data.amount) || 0,
|
|
2405
|
-
|
|
2406
|
-
// Will need to be aggregated in BigQuery
|
|
2407
|
-
lifetime_value_increment: parseFloat(data.amount) || 0,
|
|
2408
|
-
|
|
2409
|
-
updated_at: new Date().toISOString()
|
|
2410
|
-
})
|
|
2411
|
-
}
|
|
2412
|
-
],
|
|
2413
|
-
|
|
2414
|
-
// Event tracking with conditional replication
|
|
2415
|
-
events: {
|
|
2416
|
-
table: 'user_events',
|
|
2417
|
-
actions: ['insert'],
|
|
2418
|
-
transform: (data) => {
|
|
2419
|
-
// Only replicate certain event types
|
|
2420
|
-
const allowedEventTypes = ['page_view', 'button_click', 'form_submit', 'purchase'];
|
|
2421
|
-
if (!allowedEventTypes.includes(data.event_type)) {
|
|
2422
|
-
return null; // Skip replication
|
|
2423
|
-
}
|
|
2424
|
-
|
|
2425
|
-
return {
|
|
2426
|
-
event_id: data.id,
|
|
2427
|
-
user_id: data.userId,
|
|
2428
|
-
event_type: data.event_type,
|
|
2429
|
-
event_timestamp: data.timestamp,
|
|
2430
|
-
event_date: data.timestamp?.split('T')[0],
|
|
2431
|
-
|
|
2432
|
-
// Parse and clean event properties
|
|
2433
|
-
properties: JSON.stringify(data.properties || {}),
|
|
2434
|
-
page_url: data.properties?.page_url,
|
|
2435
|
-
referrer: data.properties?.referrer,
|
|
2436
|
-
|
|
2437
|
-
// Session information
|
|
2438
|
-
session_id: data.sessionId,
|
|
2439
|
-
|
|
2440
|
-
// Technical metadata
|
|
2441
|
-
user_agent: data.userAgent,
|
|
2442
|
-
ip_address: data.ipAddress ? 'masked' : null, // Privacy compliance
|
|
2443
|
-
|
|
2444
|
-
// Processing metadata
|
|
2445
|
-
processed_at: new Date().toISOString(),
|
|
2446
|
-
schema_version: '1.0'
|
|
2447
|
-
};
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
```
|
|
2453
|
-
|
|
2454
|
-
**Transform Function Features:**
|
|
2455
|
-
|
|
2456
|
-
1. **Data Cleaning & Validation:**
|
|
2457
|
-
```javascript
|
|
2458
|
-
transform: (data) => {
|
|
2459
|
-
// Skip invalid records
|
|
2460
|
-
if (!data.email || !data.id) return null;
|
|
2461
|
-
|
|
2462
|
-
return {
|
|
2463
|
-
...data,
|
|
2464
|
-
email: data.email.toLowerCase().trim(),
|
|
2465
|
-
phone: data.phone?.replace(/\D/g, '') || null, // Remove non-digits
|
|
2466
|
-
validated_at: new Date().toISOString()
|
|
2467
|
-
};
|
|
2468
|
-
}
|
|
2469
|
-
```
|
|
2470
|
-
|
|
2471
|
-
2. **Type Conversion for BigQuery:**
|
|
2472
|
-
```javascript
|
|
2473
|
-
transform: (data) => ({
|
|
2474
|
-
...data,
|
|
2475
|
-
amount: parseFloat(data.amount) || 0,
|
|
2476
|
-
quantity: parseInt(data.quantity) || 0,
|
|
2477
|
-
is_active: Boolean(data.isActive),
|
|
2478
|
-
tags: Array.isArray(data.tags) ? data.tags : [],
|
|
2479
|
-
metadata: JSON.stringify(data.metadata || {})
|
|
2480
|
-
})
|
|
2481
|
-
```
|
|
2482
|
-
|
|
2483
|
-
3. **Computed Analytics Fields:**
|
|
2484
|
-
```javascript
|
|
2485
|
-
transform: (data) => ({
|
|
2486
|
-
...data,
|
|
2487
|
-
// Date dimensions
|
|
2488
|
-
order_date: data.createdAt?.split('T')[0],
|
|
2489
|
-
order_year: new Date(data.createdAt).getFullYear(),
|
|
2490
|
-
order_month: new Date(data.createdAt).getMonth() + 1,
|
|
2491
|
-
order_quarter: Math.ceil((new Date(data.createdAt).getMonth() + 1) / 3),
|
|
2492
|
-
|
|
2493
|
-
// Business logic
|
|
2494
|
-
customer_segment: data.totalSpent > 1000 ? 'VIP' :
|
|
2495
|
-
data.totalSpent > 500 ? 'Premium' : 'Standard',
|
|
2496
|
-
|
|
2497
|
-
// Geospatial (if you have coordinates)
|
|
2498
|
-
location_string: data.lat && data.lng ? `${data.lat},${data.lng}` : null
|
|
2499
|
-
})
|
|
2500
|
-
```
|
|
2501
|
-
|
|
2502
|
-
**Configuration Options:**
|
|
2503
|
-
|
|
2504
|
-
| Parameter | Type | Required | Description |
|
|
2505
|
-
|-----------|------|----------|-------------|
|
|
2506
|
-
| `projectId` | string | Yes | Google Cloud project ID |
|
|
2507
|
-
| `datasetId` | string | Yes | BigQuery dataset ID |
|
|
2508
|
-
| `credentials` | object | No | Service account credentials (uses ADC if omitted) |
|
|
2509
|
-
| `location` | string | No | Dataset location/region (default: 'US') |
|
|
2510
|
-
| `logTable` | string | No | Table name for operation audit logging |
|
|
2511
|
-
|
|
2512
|
-
**Resource Configuration Formats:**
|
|
2513
|
-
|
|
2514
|
-
1. **String**: Simple table mapping
|
|
2515
|
-
```javascript
|
|
2516
|
-
resources: { users: 'users_table' }
|
|
2517
|
-
```
|
|
2518
|
-
|
|
2519
|
-
2. **Object**: Single table with actions and transform
|
|
2520
|
-
```javascript
|
|
2521
|
-
resources: {
|
|
2522
|
-
users: {
|
|
2523
|
-
table: 'users_table',
|
|
2524
|
-
actions: ['insert', 'update'],
|
|
2525
|
-
transform: (data) => ({ ...data, processed: true })
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
```
|
|
2529
|
-
|
|
2530
|
-
3. **Array**: Multiple destination tables
|
|
2531
|
-
```javascript
|
|
2532
|
-
resources: {
|
|
2533
|
-
orders: [
|
|
2534
|
-
{ table: 'fact_orders', actions: ['insert'] },
|
|
2535
|
-
{ table: 'daily_revenue', actions: ['insert'], transform: aggregationFn }
|
|
2536
|
-
]
|
|
2537
|
-
}
|
|
2538
|
-
```
|
|
2539
|
-
|
|
2540
|
-
**Automatic Features:**
|
|
2541
|
-
|
|
2542
|
-
- **🔄 Retry Logic**: Handles BigQuery streaming buffer limitations with 30-second delays
|
|
2543
|
-
- **🛡️ Error Handling**: Graceful handling of schema mismatches and quota limits
|
|
2544
|
-
- **📋 Operation Logging**: Optional audit trail in specified log table
|
|
2545
|
-
- **🔧 Schema Compatibility**: Automatic handling of missing fields and type coercion
|
|
2546
|
-
- **⚡ Streaming Inserts**: Uses BigQuery streaming API for real-time data ingestion
|
|
2547
|
-
- **🎯 Selective Replication**: Transform functions can return `null` to skip records
|
|
2548
|
-
|
|
2549
|
-
#### 🐘 PostgreSQL Replicator
|
|
2550
|
-
|
|
2551
|
-
**Operational database integration** for PostgreSQL with support for complex SQL operations, connection pooling, and custom transformations.
|
|
2552
|
-
|
|
2553
|
-
**⚠️ Required Dependency:**
|
|
2554
|
-
```bash
|
|
2555
|
-
pnpm add pg
|
|
2556
|
-
```
|
|
2557
|
-
|
|
2558
|
-
**Basic Configuration:**
|
|
2559
|
-
```javascript
|
|
2560
|
-
{
|
|
2561
|
-
driver: 'postgres',
|
|
2562
|
-
config: {
|
|
2563
|
-
connectionString: 'postgresql://user:pass@localhost:5432/analytics',
|
|
2564
|
-
ssl: { rejectUnauthorized: false },
|
|
2565
|
-
pool: {
|
|
2566
|
-
max: 10,
|
|
2567
|
-
idleTimeoutMillis: 30000,
|
|
2568
|
-
connectionTimeoutMillis: 2000
|
|
2569
|
-
}
|
|
2570
|
-
},
|
|
2571
|
-
resources: {
|
|
2572
|
-
users: [{
|
|
2573
|
-
table: 'users_table',
|
|
2574
|
-
actions: ['insert', 'update', 'delete']
|
|
2575
|
-
}]
|
|
2576
|
-
}
|
|
2577
|
-
}
|
|
2578
|
-
```
|
|
2579
|
-
|
|
2580
|
-
**Advanced Configuration with Custom SQL:**
|
|
2581
|
-
```javascript
|
|
2582
|
-
{
|
|
2583
|
-
driver: 'postgres',
|
|
2584
|
-
config: {
|
|
2585
|
-
connectionString: 'postgresql://analytics:password@localhost:5432/operations',
|
|
2586
|
-
ssl: { rejectUnauthorized: false },
|
|
2587
|
-
logTable: 'replication_audit',
|
|
2588
|
-
pool: {
|
|
2589
|
-
max: 20,
|
|
2590
|
-
min: 5,
|
|
2591
|
-
idleTimeoutMillis: 30000,
|
|
2592
|
-
connectionTimeoutMillis: 2000,
|
|
2593
|
-
acquireTimeoutMillis: 60000
|
|
2594
|
-
}
|
|
2595
|
-
},
|
|
2596
|
-
resources: {
|
|
2597
|
-
// Multi-table replication with different strategies
|
|
2598
|
-
users: [
|
|
2599
|
-
{
|
|
2600
|
-
table: 'operational_users',
|
|
2601
|
-
actions: ['insert', 'update', 'delete'],
|
|
2602
|
-
transform: (data, operation) => {
|
|
2603
|
-
if (operation === 'delete') {
|
|
2604
|
-
return {
|
|
2605
|
-
id: data.id,
|
|
2606
|
-
deleted_at: new Date(),
|
|
2607
|
-
deletion_reason: 'user_requested'
|
|
2608
|
-
};
|
|
2609
|
-
}
|
|
2610
|
-
|
|
2611
|
-
return {
|
|
2612
|
-
...data,
|
|
2613
|
-
sync_timestamp: new Date(),
|
|
2614
|
-
source_system: 's3db',
|
|
2615
|
-
data_version: '1.0',
|
|
2616
|
-
|
|
2617
|
-
// Computed fields
|
|
2618
|
-
full_name: `${data.firstName || ''} ${data.lastName || ''}`.trim(),
|
|
2619
|
-
email_domain: data.email?.split('@')[1] || 'unknown',
|
|
2620
|
-
account_age_days: data.createdAt ?
|
|
2621
|
-
Math.floor((Date.now() - new Date(data.createdAt)) / (1000 * 60 * 60 * 24)) : 0
|
|
2622
|
-
};
|
|
2623
|
-
}
|
|
2624
|
-
},
|
|
2625
|
-
|
|
2626
|
-
// Separate audit trail table
|
|
2627
|
-
{
|
|
2628
|
-
table: 'user_audit_trail',
|
|
2629
|
-
actions: ['insert', 'update', 'delete'],
|
|
2630
|
-
transform: (data, operation) => ({
|
|
2631
|
-
user_id: data.id,
|
|
2632
|
-
operation_type: operation,
|
|
2633
|
-
operation_timestamp: new Date(),
|
|
2634
|
-
data_snapshot: JSON.stringify(data),
|
|
2635
|
-
source_database: 's3db',
|
|
2636
|
-
replication_id: crypto.randomUUID()
|
|
2637
|
-
})
|
|
2638
|
-
}
|
|
2639
|
-
],
|
|
2640
|
-
|
|
2641
|
-
// Orders with complex business logic
|
|
2642
|
-
orders: [
|
|
2643
|
-
{
|
|
2644
|
-
table: 'order_events',
|
|
2645
|
-
actions: ['insert'],
|
|
2646
|
-
transform: (data) => ({
|
|
2647
|
-
event_id: crypto.randomUUID(),
|
|
2648
|
-
order_id: data.id,
|
|
2649
|
-
customer_id: data.userId,
|
|
2650
|
-
event_type: 'order_created',
|
|
2651
|
-
event_timestamp: new Date(),
|
|
2652
|
-
order_amount: parseFloat(data.amount) || 0,
|
|
2653
|
-
order_status: data.status || 'pending',
|
|
2654
|
-
|
|
2655
|
-
// Business metrics
|
|
2656
|
-
is_large_order: data.amount > 500,
|
|
2657
|
-
order_priority: data.amount > 1000 ? 'high' :
|
|
2658
|
-
data.amount > 100 ? 'medium' : 'low',
|
|
2659
|
-
estimated_fulfillment: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days
|
|
2660
|
-
|
|
2661
|
-
// Metadata
|
|
2662
|
-
created_at: new Date(),
|
|
2663
|
-
source_system: 'production_s3db'
|
|
2664
|
-
})
|
|
2665
|
-
},
|
|
2666
|
-
|
|
2667
|
-
{
|
|
2668
|
-
table: 'order_updates',
|
|
2669
|
-
actions: ['update'],
|
|
2670
|
-
transform: (data, operation, beforeData) => ({
|
|
2671
|
-
update_id: crypto.randomUUID(),
|
|
2672
|
-
order_id: data.id,
|
|
2673
|
-
updated_at: new Date(),
|
|
2674
|
-
|
|
2675
|
-
// Track what changed
|
|
2676
|
-
changed_fields: Object.keys(data).filter(key =>
|
|
2677
|
-
beforeData && data[key] !== beforeData[key]
|
|
2678
|
-
),
|
|
2679
|
-
previous_status: beforeData?.status,
|
|
2680
|
-
new_status: data.status,
|
|
2681
|
-
|
|
2682
|
-
// Change metadata
|
|
2683
|
-
status_progression: `${beforeData?.status || 'unknown'} -> ${data.status}`,
|
|
2684
|
-
update_source: 'automated_replication'
|
|
2685
|
-
})
|
|
2686
|
-
}
|
|
2687
|
-
],
|
|
2688
|
-
|
|
2689
|
-
// Conditional replication based on data
|
|
2690
|
-
sensitive_data: {
|
|
2691
|
-
table: 'masked_sensitive_data',
|
|
2692
|
-
actions: ['insert', 'update'],
|
|
2693
|
-
transform: (data) => {
|
|
2694
|
-
// Skip replication for certain sensitive records
|
|
2695
|
-
if (data.privacy_level === 'restricted') {
|
|
2696
|
-
return null;
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
return {
|
|
2700
|
-
id: data.id,
|
|
2701
|
-
// Mask sensitive fields
|
|
2702
|
-
email: data.email ? `${data.email.split('@')[0].slice(0, 3)}***@${data.email.split('@')[1]}` : null,
|
|
2703
|
-
phone: data.phone ? `***-***-${data.phone.slice(-4)}` : null,
|
|
2704
|
-
name: data.name ? `${data.name.charAt(0)}${'*'.repeat(data.name.length - 1)}` : null,
|
|
2705
|
-
|
|
2706
|
-
// Keep non-sensitive data
|
|
2707
|
-
created_at: data.createdAt,
|
|
2708
|
-
status: data.status,
|
|
2709
|
-
category: data.category,
|
|
2710
|
-
|
|
2711
|
-
// Add masking metadata
|
|
2712
|
-
masked_at: new Date(),
|
|
2713
|
-
masking_version: '1.0'
|
|
2714
|
-
};
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
}
|
|
2719
|
-
```
|
|
2720
|
-
|
|
2721
|
-
**Configuration Options:**
|
|
2722
|
-
|
|
2723
|
-
| Parameter | Type | Required | Description |
|
|
2724
|
-
|-----------|------|----------|-------------|
|
|
2725
|
-
| `connectionString` | string | Yes | PostgreSQL connection string |
|
|
2726
|
-
| `ssl` | object | No | SSL configuration options |
|
|
2727
|
-
| `pool` | object | No | Connection pool configuration |
|
|
2728
|
-
| `logTable` | string | No | Table name for operation audit logging |
|
|
2729
|
-
|
|
2730
|
-
**Pool Configuration Options:**
|
|
2731
|
-
|
|
2732
|
-
| Parameter | Type | Default | Description |
|
|
2733
|
-
|-----------|------|---------|-------------|
|
|
2734
|
-
| `max` | number | `10` | Maximum number of connections in pool |
|
|
2735
|
-
| `min` | number | `0` | Minimum number of connections in pool |
|
|
2736
|
-
| `idleTimeoutMillis` | number | `10000` | How long a client is allowed to remain idle |
|
|
2737
|
-
| `connectionTimeoutMillis` | number | `0` | Return an error after timeout |
|
|
2738
|
-
| `acquireTimeoutMillis` | number | `0` | Return an error if no connection available |
|
|
2739
|
-
|
|
2740
|
-
**Resource Configuration:**
|
|
2741
|
-
|
|
2742
|
-
Resources must be configured as arrays of table configurations:
|
|
2743
|
-
|
|
2744
|
-
```javascript
|
|
2745
|
-
resources: {
|
|
2746
|
-
resourceName: [
|
|
2747
|
-
{
|
|
2748
|
-
table: 'destination_table',
|
|
2749
|
-
actions: ['insert', 'update', 'delete'],
|
|
2750
|
-
transform: (data, operation, beforeData) => ({
|
|
2751
|
-
// Return transformed data or null to skip
|
|
2752
|
-
})
|
|
2753
|
-
}
|
|
2754
|
-
]
|
|
2755
|
-
}
|
|
2756
|
-
```
|
|
2757
|
-
|
|
2758
|
-
**Automatic Features:**
|
|
2759
|
-
|
|
2760
|
-
- **🔄 Connection Pooling**: Efficient database connection management
|
|
2761
|
-
- **📋 Audit Logging**: Optional audit trail in specified log table
|
|
2762
|
-
- **🛡️ Error Handling**: Graceful handling of connection failures and SQL errors
|
|
2763
|
-
- **⚡ Bulk Operations**: Optimized for high-throughput replication
|
|
2764
|
-
- **🎯 Flexible Actions**: Support for insert, update, delete operations
|
|
2765
|
-
- **🔧 Custom SQL**: Transform functions receive operation context for complex logic
|
|
2766
|
-
|
|
2767
|
-
### 🔍 Monitoring & Health Checks
|
|
2768
|
-
|
|
2769
|
-
Monitor replication health and performance with built-in tools:
|
|
2770
|
-
|
|
2771
|
-
```javascript
|
|
2772
|
-
// Get replication status for all replicators
|
|
2773
|
-
const replicationStatus = await replicatorPlugin.getReplicationStats();
|
|
2774
|
-
|
|
2775
|
-
console.log('Replication Health:', {
|
|
2776
|
-
totalReplicators: replicationStatus.replicators.length,
|
|
2777
|
-
activeReplicators: replicationStatus.replicators.filter(r => r.status.enabled).length,
|
|
2778
|
-
lastSync: replicationStatus.lastSync,
|
|
2779
|
-
errorRate: replicationStatus.stats.errorRate
|
|
2780
|
-
});
|
|
2781
|
-
|
|
2782
|
-
// Check individual replicator health
|
|
2783
|
-
for (const replicator of replicationStatus.replicators) {
|
|
2784
|
-
console.log(`${replicator.driver} replicator:`, {
|
|
2785
|
-
enabled: replicator.status.enabled,
|
|
2786
|
-
connected: replicator.status.connected,
|
|
2787
|
-
lastActivity: replicator.status.lastActivity,
|
|
2788
|
-
totalOperations: replicator.status.totalOperations,
|
|
2789
|
-
errorCount: replicator.status.errorCount
|
|
2790
|
-
});
|
|
2791
|
-
}
|
|
2792
|
-
|
|
2793
|
-
// Get detailed replication logs
|
|
2794
|
-
const replicationLogs = await replicatorPlugin.getReplicationLogs({
|
|
2795
|
-
resourceName: 'users', // Filter by resource
|
|
2796
|
-
status: 'failed', // Show only failed replications
|
|
2797
|
-
limit: 50, // Limit results
|
|
2798
|
-
offset: 0 // Pagination
|
|
2799
|
-
});
|
|
2800
|
-
|
|
2801
|
-
console.log('Recent failures:', replicationLogs.map(log => ({
|
|
2802
|
-
timestamp: log.timestamp,
|
|
2803
|
-
resource: log.resourceName,
|
|
2804
|
-
operation: log.operation,
|
|
2805
|
-
error: log.error,
|
|
2806
|
-
replicator: log.replicator
|
|
2807
|
-
})));
|
|
2808
|
-
|
|
2809
|
-
// Retry failed replications
|
|
2810
|
-
const retryResult = await replicatorPlugin.retryFailedReplications();
|
|
2811
|
-
console.log(`Retried ${retryResult.retried} failed replications`);
|
|
2812
|
-
```
|
|
2813
|
-
|
|
2814
|
-
### 🚨 Troubleshooting Guide
|
|
2815
|
-
|
|
2816
|
-
#### Common Issues and Solutions
|
|
2817
|
-
|
|
2818
|
-
**1. Replication Not Happening**
|
|
2819
|
-
```javascript
|
|
2820
|
-
// Check if replicator is enabled
|
|
2821
|
-
const plugin = s3db.plugins.find(p => p.constructor.name === 'ReplicatorPlugin');
|
|
2822
|
-
console.log('Plugin enabled:', plugin.config.enabled);
|
|
2823
|
-
|
|
2824
|
-
// Check resource configuration
|
|
2825
|
-
console.log('Resources configured:', Object.keys(plugin.config.replicators[0].resources));
|
|
2826
|
-
|
|
2827
|
-
// Listen for debug events
|
|
2828
|
-
plugin.on('replicator_error', (error) => {
|
|
2829
|
-
console.error('Replication error:', error);
|
|
2830
|
-
});
|
|
2831
|
-
```
|
|
2832
|
-
|
|
2833
|
-
**2. Transform Function Errors**
|
|
2834
|
-
```javascript
|
|
2835
|
-
// Add error handling in transform functions
|
|
2836
|
-
transform: (data) => {
|
|
2837
|
-
try {
|
|
2838
|
-
return {
|
|
2839
|
-
...data,
|
|
2840
|
-
fullName: `${data.firstName} ${data.lastName}`,
|
|
2841
|
-
processedAt: new Date().toISOString()
|
|
2842
|
-
};
|
|
2843
|
-
} catch (error) {
|
|
2844
|
-
console.error('Transform error:', error, 'Data:', data);
|
|
2845
|
-
return data; // Fallback to original data
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
```
|
|
2849
|
-
|
|
2850
|
-
**3. Connection Issues**
|
|
2851
|
-
```javascript
|
|
2852
|
-
// Test replicator connections
|
|
2853
|
-
const testResults = await Promise.allSettled(
|
|
2854
|
-
replicatorPlugin.replicators.map(async (replicator) => {
|
|
2855
|
-
try {
|
|
2856
|
-
const result = await replicator.testConnection();
|
|
2857
|
-
return { replicator: replicator.id, success: result, error: null };
|
|
2858
|
-
} catch (error) {
|
|
2859
|
-
return { replicator: replicator.id, success: false, error: error.message };
|
|
2860
|
-
}
|
|
2861
|
-
})
|
|
2862
|
-
);
|
|
2863
|
-
|
|
2864
|
-
testResults.forEach(result => {
|
|
2865
|
-
if (result.status === 'fulfilled') {
|
|
2866
|
-
console.log(`${result.value.replicator}: ${result.value.success ? '✅' : '❌'} ${result.value.error || ''}`);
|
|
2867
|
-
}
|
|
2868
|
-
});
|
|
2869
|
-
```
|
|
2870
|
-
|
|
2871
|
-
**4. Performance Issues**
|
|
2872
|
-
```javascript
|
|
2873
|
-
// Monitor replication performance
|
|
2874
|
-
const performanceMonitor = setInterval(async () => {
|
|
2875
|
-
const stats = await replicatorPlugin.getReplicationStats();
|
|
2876
|
-
|
|
2877
|
-
console.log('Performance Metrics:', {
|
|
2878
|
-
operationsPerSecond: stats.operationsPerSecond,
|
|
2879
|
-
averageLatency: stats.averageLatency,
|
|
2880
|
-
queueSize: stats.queueSize,
|
|
2881
|
-
memoryUsage: process.memoryUsage()
|
|
2882
|
-
});
|
|
2883
|
-
|
|
2884
|
-
// Alert on performance degradation
|
|
2885
|
-
if (stats.averageLatency > 5000) { // 5 seconds
|
|
2886
|
-
console.warn('⚠️ High replication latency detected:', stats.averageLatency + 'ms');
|
|
2887
|
-
}
|
|
2888
|
-
}, 30000); // Check every 30 seconds
|
|
2889
|
-
```
|
|
2890
|
-
|
|
2891
|
-
### 🎯 Advanced Use Cases
|
|
2892
|
-
|
|
2893
|
-
#### Multi-Environment Replication Pipeline
|
|
2894
|
-
|
|
2895
|
-
```javascript
|
|
2896
|
-
const replicatorPlugin = new ReplicatorPlugin({
|
|
2897
|
-
verbose: true,
|
|
2898
|
-
persistReplicatorLog: true,
|
|
2899
|
-
replicators: [
|
|
2900
|
-
// Production backup
|
|
2901
|
-
{
|
|
2902
|
-
driver: 's3db',
|
|
2903
|
-
config: { connectionString: process.env.BACKUP_DB_URL },
|
|
2904
|
-
resources: ['users', 'orders', 'products']
|
|
2905
|
-
},
|
|
2906
|
-
|
|
2907
|
-
// Staging environment sync
|
|
2908
|
-
{
|
|
2909
|
-
driver: 's3db',
|
|
2910
|
-
config: { connectionString: process.env.STAGING_DB_URL },
|
|
2911
|
-
resources: {
|
|
2912
|
-
users: {
|
|
2913
|
-
resource: 'users',
|
|
2914
|
-
transform: (data) => ({
|
|
2915
|
-
...data,
|
|
2916
|
-
// Remove PII for staging
|
|
2917
|
-
email: data.email?.replace(/(.{2})(.*)(@.*)/, '$1***$3'),
|
|
2918
|
-
phone: '***-***-' + (data.phone?.slice(-4) || '0000')
|
|
2919
|
-
})
|
|
2920
|
-
}
|
|
2921
|
-
}
|
|
2922
|
-
},
|
|
2923
|
-
|
|
2924
|
-
// Analytics warehouse
|
|
2925
|
-
{
|
|
2926
|
-
driver: 'bigquery',
|
|
2927
|
-
config: {
|
|
2928
|
-
projectId: 'analytics-project',
|
|
2929
|
-
datasetId: 'production_data'
|
|
2930
|
-
},
|
|
2931
|
-
resources: {
|
|
2932
|
-
orders: [
|
|
2933
|
-
{ table: 'fact_orders', actions: ['insert'] },
|
|
2934
|
-
{ table: 'daily_revenue', actions: ['insert'], transform: aggregateDaily }
|
|
2935
|
-
]
|
|
2936
|
-
}
|
|
2937
|
-
},
|
|
2938
|
-
|
|
2939
|
-
// Real-time events
|
|
2940
|
-
{
|
|
2941
|
-
driver: 'sqs',
|
|
2942
|
-
config: {
|
|
2943
|
-
region: 'us-east-1',
|
|
2944
|
-
queues: {
|
|
2945
|
-
users: process.env.USER_EVENTS_QUEUE,
|
|
2946
|
-
orders: process.env.ORDER_EVENTS_QUEUE
|
|
2947
|
-
}
|
|
2948
|
-
},
|
|
2949
|
-
resources: ['users', 'orders']
|
|
2950
|
-
}
|
|
2951
|
-
]
|
|
2952
|
-
});
|
|
2953
|
-
```
|
|
2954
|
-
|
|
2955
|
-
#### Conditional Replication Based on Business Rules
|
|
2956
|
-
|
|
2957
|
-
```javascript
|
|
2958
|
-
const businessRulesReplicator = new ReplicatorPlugin({
|
|
2959
|
-
replicators: [
|
|
2960
|
-
{
|
|
2961
|
-
driver: 's3db',
|
|
2962
|
-
config: { connectionString: process.env.COMPLIANCE_DB_URL },
|
|
2963
|
-
resources: {
|
|
2964
|
-
users: {
|
|
2965
|
-
resource: 'compliant_users',
|
|
2966
|
-
transform: (data) => {
|
|
2967
|
-
// Only replicate users from certain regions
|
|
2968
|
-
const allowedRegions = ['US', 'EU', 'CA'];
|
|
2969
|
-
if (!allowedRegions.includes(data.region)) {
|
|
2970
|
-
return null; // Skip replication
|
|
2971
|
-
}
|
|
2972
|
-
|
|
2973
|
-
// Apply data retention rules
|
|
2974
|
-
const accountAge = Date.now() - new Date(data.createdAt);
|
|
2975
|
-
const maxAge = 7 * 365 * 24 * 60 * 60 * 1000; // 7 years
|
|
2976
|
-
|
|
2977
|
-
if (accountAge > maxAge && data.status === 'inactive') {
|
|
2978
|
-
return null; // Skip old inactive accounts
|
|
2979
|
-
}
|
|
2980
|
-
|
|
2981
|
-
return {
|
|
2982
|
-
...data,
|
|
2983
|
-
complianceVersion: '2.0',
|
|
2984
|
-
lastAudit: new Date().toISOString(),
|
|
2985
|
-
retentionCategory: accountAge > maxAge * 0.8 ? 'pending_review' : 'active'
|
|
2986
|
-
};
|
|
2987
|
-
}
|
|
2988
|
-
},
|
|
2989
|
-
|
|
2990
|
-
orders: {
|
|
2991
|
-
resource: 'audit_orders',
|
|
2992
|
-
transform: (data) => {
|
|
2993
|
-
// Only replicate high-value orders for compliance
|
|
2994
|
-
if (data.amount < 10000) return null;
|
|
2995
|
-
|
|
2996
|
-
return {
|
|
2997
|
-
order_id: data.id,
|
|
2998
|
-
customer_id: data.userId,
|
|
2999
|
-
amount: data.amount,
|
|
3000
|
-
currency: data.currency,
|
|
3001
|
-
order_date: data.createdAt,
|
|
3002
|
-
compliance_flag: 'high_value',
|
|
3003
|
-
audit_required: true,
|
|
3004
|
-
retention_years: 10
|
|
3005
|
-
};
|
|
3006
|
-
}
|
|
3007
|
-
}
|
|
3008
|
-
}
|
|
3009
|
-
}
|
|
3010
|
-
]
|
|
3011
|
-
});
|
|
3012
|
-
```
|
|
3013
|
-
|
|
3014
|
-
#### Event-Driven Architecture Integration
|
|
3015
|
-
|
|
3016
|
-
```javascript
|
|
3017
|
-
// Custom event handlers for complex workflows
|
|
3018
|
-
replicatorPlugin.on('replicated', async (event) => {
|
|
3019
|
-
const { resourceName, operation, recordId, replicator } = event;
|
|
3020
|
-
|
|
3021
|
-
// Trigger downstream processes
|
|
3022
|
-
if (resourceName === 'orders' && operation === 'insert') {
|
|
3023
|
-
// Trigger inventory update
|
|
3024
|
-
await inventoryService.updateStock(event.data);
|
|
3025
|
-
|
|
3026
|
-
// Send confirmation email
|
|
3027
|
-
await emailService.sendOrderConfirmation(event.data);
|
|
3028
|
-
|
|
3029
|
-
// Update analytics dashboard
|
|
3030
|
-
await analyticsService.recordSale(event.data);
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3033
|
-
if (resourceName === 'users' && operation === 'update') {
|
|
3034
|
-
// Check for important profile changes
|
|
3035
|
-
const criticalFields = ['email', 'phone', 'address'];
|
|
3036
|
-
const changedFields = Object.keys(event.data);
|
|
3037
|
-
|
|
3038
|
-
if (criticalFields.some(field => changedFields.includes(field))) {
|
|
3039
|
-
await auditService.logCriticalChange({
|
|
3040
|
-
userId: recordId,
|
|
3041
|
-
changedFields: changedFields.filter(f => criticalFields.includes(f)),
|
|
3042
|
-
timestamp: new Date()
|
|
3043
|
-
});
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
|
-
});
|
|
3047
|
-
|
|
3048
|
-
// Handle replication failures with custom logic
|
|
3049
|
-
replicatorPlugin.on('replicator_error', async (error) => {
|
|
3050
|
-
const { resourceName, operation, recordId, replicator, error: errorMessage } = error;
|
|
3051
|
-
|
|
3052
|
-
// Log to external monitoring system
|
|
3053
|
-
await monitoringService.logError({
|
|
3054
|
-
service: 'replication',
|
|
3055
|
-
error: errorMessage,
|
|
3056
|
-
context: { resourceName, operation, recordId, replicator }
|
|
3057
|
-
});
|
|
3058
|
-
|
|
3059
|
-
// Send alerts for critical resources
|
|
3060
|
-
const criticalResources = ['users', 'orders', 'payments'];
|
|
3061
|
-
if (criticalResources.includes(resourceName)) {
|
|
3062
|
-
await alertingService.sendAlert({
|
|
3063
|
-
severity: 'high',
|
|
3064
|
-
message: `Critical replication failure: ${resourceName}`,
|
|
3065
|
-
details: error
|
|
3066
|
-
});
|
|
3067
|
-
}
|
|
3068
|
-
|
|
3069
|
-
// Implement circuit breaker pattern
|
|
3070
|
-
const errorCount = await redis.incr(`replication_errors:${replicator}`);
|
|
3071
|
-
await redis.expire(`replication_errors:${replicator}`, 3600); // 1 hour window
|
|
3072
|
-
|
|
3073
|
-
if (errorCount > 10) {
|
|
3074
|
-
console.warn(`⚠️ Circuit breaker: Disabling ${replicator} due to high error rate`);
|
|
3075
|
-
// Temporarily disable problematic replicator
|
|
3076
|
-
const problematicReplicator = replicatorPlugin.replicators.find(r => r.id === replicator);
|
|
3077
|
-
if (problematicReplicator) {
|
|
3078
|
-
problematicReplicator.enabled = false;
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
|
-
});
|
|
3082
|
-
```
|
|
3083
|
-
|
|
3084
|
-
### 🎛️ Performance Tuning
|
|
3085
|
-
|
|
3086
|
-
#### Optimize Replication Performance
|
|
3087
|
-
|
|
3088
|
-
```javascript
|
|
3089
|
-
// Configure for high-throughput scenarios
|
|
3090
|
-
const highPerformanceReplicator = new ReplicatorPlugin({
|
|
3091
|
-
batchSize: 500, // Larger batches for better throughput
|
|
3092
|
-
maxRetries: 5, // More retries for reliability
|
|
3093
|
-
timeout: 60000, // Longer timeout for large operations
|
|
3094
|
-
|
|
3095
|
-
replicators: [
|
|
3096
|
-
{
|
|
3097
|
-
driver: 'bigquery',
|
|
3098
|
-
config: {
|
|
3099
|
-
projectId: 'analytics',
|
|
3100
|
-
datasetId: 'high_volume_data',
|
|
3101
|
-
// Use streaming inserts for real-time data
|
|
3102
|
-
streamingInserts: true,
|
|
3103
|
-
insertAllTimeout: 30000
|
|
3104
|
-
},
|
|
3105
|
-
resources: {
|
|
3106
|
-
events: {
|
|
3107
|
-
table: 'raw_events',
|
|
3108
|
-
actions: ['insert'],
|
|
3109
|
-
// Optimize transform for performance
|
|
3110
|
-
transform: (data) => {
|
|
3111
|
-
// Pre-compute expensive operations
|
|
3112
|
-
const eventDate = data.timestamp?.split('T')[0];
|
|
3113
|
-
|
|
3114
|
-
return {
|
|
3115
|
-
event_id: data.id,
|
|
3116
|
-
user_id: data.userId,
|
|
3117
|
-
event_type: data.type,
|
|
3118
|
-
event_date: eventDate,
|
|
3119
|
-
properties: JSON.stringify(data.properties || {}),
|
|
3120
|
-
|
|
3121
|
-
// Batch-friendly fields
|
|
3122
|
-
partition_date: eventDate,
|
|
3123
|
-
ingestion_timestamp: Math.floor(Date.now() / 1000) // Unix timestamp
|
|
3124
|
-
};
|
|
3125
|
-
}
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
},
|
|
3129
|
-
|
|
3130
|
-
{
|
|
3131
|
-
driver: 'postgres',
|
|
3132
|
-
config: {
|
|
3133
|
-
connectionString: process.env.POSTGRES_URL,
|
|
3134
|
-
pool: {
|
|
3135
|
-
max: 50, // Larger connection pool
|
|
3136
|
-
min: 10,
|
|
3137
|
-
idleTimeoutMillis: 60000,
|
|
3138
|
-
acquireTimeoutMillis: 10000
|
|
3139
|
-
}
|
|
3140
|
-
},
|
|
3141
|
-
resources: {
|
|
3142
|
-
users: [{
|
|
3143
|
-
table: 'users_cache',
|
|
3144
|
-
actions: ['insert', 'update'],
|
|
3145
|
-
// Use upsert pattern for better performance
|
|
3146
|
-
upsertMode: true,
|
|
3147
|
-
transform: (data) => ({
|
|
3148
|
-
id: data.id,
|
|
3149
|
-
email: data.email,
|
|
3150
|
-
name: data.name,
|
|
3151
|
-
updated_at: new Date(),
|
|
3152
|
-
|
|
3153
|
-
// Optimized for query performance
|
|
3154
|
-
search_vector: `${data.name} ${data.email}`.toLowerCase()
|
|
3155
|
-
})
|
|
3156
|
-
}]
|
|
3157
|
-
}
|
|
3158
|
-
}
|
|
3159
|
-
]
|
|
3160
|
-
});
|
|
3161
|
-
```
|
|
3162
|
-
|
|
3163
|
-
### 🔐 Security Best Practices
|
|
3164
|
-
|
|
3165
|
-
#### Secure Replication Configuration
|
|
3166
|
-
|
|
3167
|
-
```javascript
|
|
3168
|
-
const secureReplicator = new ReplicatorPlugin({
|
|
3169
|
-
replicators: [
|
|
3170
|
-
{
|
|
3171
|
-
driver: 'bigquery',
|
|
3172
|
-
config: {
|
|
3173
|
-
projectId: 'secure-analytics',
|
|
3174
|
-
datasetId: 'encrypted_data',
|
|
3175
|
-
// Use service account with minimal permissions
|
|
3176
|
-
credentials: {
|
|
3177
|
-
type: 'service_account',
|
|
3178
|
-
project_id: 'secure-analytics',
|
|
3179
|
-
private_key: process.env.BIGQUERY_PRIVATE_KEY,
|
|
3180
|
-
client_email: 'replication-service@secure-analytics.iam.gserviceaccount.com'
|
|
3181
|
-
}
|
|
3182
|
-
},
|
|
3183
|
-
resources: {
|
|
3184
|
-
users: {
|
|
3185
|
-
table: 'encrypted_users',
|
|
3186
|
-
actions: ['insert', 'update'],
|
|
3187
|
-
transform: (data) => ({
|
|
3188
|
-
// Hash sensitive identifiers
|
|
3189
|
-
user_hash: crypto.createHash('sha256').update(data.id + process.env.SALT).digest('hex'),
|
|
3190
|
-
|
|
3191
|
-
// Encrypt PII fields
|
|
3192
|
-
encrypted_email: encrypt(data.email),
|
|
3193
|
-
encrypted_phone: data.phone ? encrypt(data.phone) : null,
|
|
3194
|
-
|
|
3195
|
-
// Keep non-sensitive analytics data
|
|
3196
|
-
registration_date: data.createdAt?.split('T')[0],
|
|
3197
|
-
account_type: data.accountType,
|
|
3198
|
-
region: data.region,
|
|
3199
|
-
|
|
3200
|
-
// Add encryption metadata
|
|
3201
|
-
encryption_version: '1.0',
|
|
3202
|
-
processed_at: new Date().toISOString()
|
|
3203
|
-
})
|
|
3204
|
-
}
|
|
3205
|
-
}
|
|
3206
|
-
}
|
|
3207
|
-
]
|
|
3208
|
-
});
|
|
3209
|
-
|
|
3210
|
-
// Encryption utility functions
|
|
3211
|
-
function encrypt(text) {
|
|
3212
|
-
if (!text) return null;
|
|
3213
|
-
const cipher = crypto.createCipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
|
|
3214
|
-
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
3215
|
-
encrypted += cipher.final('hex');
|
|
3216
|
-
return encrypted;
|
|
3217
|
-
}
|
|
3218
|
-
|
|
3219
|
-
function decrypt(encryptedText) {
|
|
3220
|
-
if (!encryptedText) return null;
|
|
3221
|
-
const decipher = crypto.createDecipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
|
|
3222
|
-
let decrypted = decipher.update(encryptedText, 'hex', 'utf8');
|
|
3223
|
-
decrypted += decipher.final('utf8');
|
|
3224
|
-
return decrypted;
|
|
3225
|
-
}
|
|
3226
|
-
```
|
|
3227
|
-
|
|
3228
|
-
### ✅ Best Practices Summary
|
|
3229
|
-
|
|
3230
|
-
1. **Configuration Management**
|
|
3231
|
-
- Use environment variables for connection strings and credentials
|
|
3232
|
-
- Implement proper error handling in transform functions
|
|
3233
|
-
- Test replicator connections during application startup
|
|
3234
|
-
|
|
3235
|
-
2. **Performance Optimization**
|
|
3236
|
-
- Use appropriate batch sizes for your data volume
|
|
3237
|
-
- Configure connection pools for database replicators
|
|
3238
|
-
- Monitor replication lag and throughput
|
|
3239
|
-
|
|
3240
|
-
3. **Security & Compliance**
|
|
3241
|
-
- Encrypt sensitive data before replication
|
|
3242
|
-
- Implement data masking for non-production environments
|
|
3243
|
-
- Use minimal privilege service accounts
|
|
3244
|
-
|
|
3245
|
-
4. **Monitoring & Alerting**
|
|
3246
|
-
- Set up alerts for replication failures
|
|
3247
|
-
- Monitor replication lag and error rates
|
|
3248
|
-
- Implement health checks for all replicators
|
|
3249
|
-
|
|
3250
|
-
5. **Error Handling**
|
|
3251
|
-
- Implement circuit breakers for unreliable targets
|
|
3252
|
-
- Use dead letter queues for failed messages
|
|
3253
|
-
- Log detailed error information for debugging
|
|
3254
|
-
|
|
3255
|
-
### 🔧 Easy Example
|
|
3256
|
-
|
|
3257
|
-
```javascript
|
|
3258
|
-
import { S3db, ReplicatorPlugin } from 's3db.js';
|
|
3259
|
-
|
|
3260
|
-
const s3db = new S3db({
|
|
3261
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
3262
|
-
plugins: [new ReplicatorPlugin({
|
|
3263
|
-
persistReplicatorLog: true,
|
|
3264
|
-
replicators: [
|
|
3265
|
-
{
|
|
3266
|
-
driver: 's3db',
|
|
3267
|
-
resources: ['users', 'products'],
|
|
3268
|
-
config: {
|
|
3269
|
-
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
3270
|
-
}
|
|
3271
|
-
},
|
|
3272
|
-
{
|
|
3273
|
-
driver: 'sqs',
|
|
3274
|
-
resources: ['orders'],
|
|
3275
|
-
config: {
|
|
3276
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/orders-queue.fifo',
|
|
3277
|
-
region: 'us-east-1',
|
|
3278
|
-
messageGroupId: 'order-updates'
|
|
3279
|
-
}
|
|
3280
|
-
}
|
|
3281
|
-
]
|
|
3282
|
-
})]
|
|
3283
|
-
});
|
|
3284
|
-
|
|
3285
|
-
await s3db.connect();
|
|
3286
|
-
|
|
3287
|
-
const users = s3db.resource('users');
|
|
3288
|
-
const orders = s3db.resource('orders');
|
|
3289
|
-
|
|
3290
|
-
// Monitor replication events
|
|
3291
|
-
const replicatorPlugin = s3db.plugins.find(p => p.constructor.name === 'ReplicatorPlugin');
|
|
3292
|
-
|
|
3293
|
-
replicatorPlugin.on('replicator.success', (data) => {
|
|
3294
|
-
console.log(`✅ Replicated: ${data.action} on ${data.resource} to ${data.replicator}`);
|
|
3295
|
-
});
|
|
3296
|
-
|
|
3297
|
-
replicatorPlugin.on('replicator.failed', (data) => {
|
|
3298
|
-
console.error(`❌ Replication failed: ${data.error}`);
|
|
3299
|
-
});
|
|
3300
|
-
|
|
3301
|
-
// Insert data (automatically replicated)
|
|
3302
|
-
const user = await users.insert({
|
|
3303
|
-
name: 'Alice Johnson',
|
|
3304
|
-
email: 'alice@example.com',
|
|
3305
|
-
role: 'customer'
|
|
3306
|
-
});
|
|
3307
|
-
|
|
3308
|
-
const order = await orders.insert({
|
|
3309
|
-
userId: user.id,
|
|
3310
|
-
amount: 99.99,
|
|
3311
|
-
items: ['item1', 'item2']
|
|
3312
|
-
});
|
|
3313
|
-
|
|
3314
|
-
// Check replication logs
|
|
3315
|
-
const replicatorLogs = s3db.resource('replicator_logs');
|
|
3316
|
-
const logs = await replicatorLogs.list();
|
|
3317
|
-
|
|
3318
|
-
console.log('\n=== Replication History ===');
|
|
3319
|
-
logs.forEach(log => {
|
|
3320
|
-
console.log(`${log.timestamp}: ${log.action} ${log.resource} → ${log.replicator}`);
|
|
3321
|
-
});
|
|
3322
|
-
```
|
|
3323
|
-
|
|
3324
|
-
### 🚀 Advanced Multi-Driver Example
|
|
3325
|
-
|
|
3326
|
-
```javascript
|
|
3327
|
-
import { S3db, ReplicatorPlugin } from 's3db.js';
|
|
3328
|
-
|
|
3329
|
-
const s3db = new S3db({
|
|
3330
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
3331
|
-
plugins: [new ReplicatorPlugin({
|
|
3332
|
-
enabled: true,
|
|
3333
|
-
persistReplicatorLog: true,
|
|
3334
|
-
replicatorLogResource: 'replication_audit',
|
|
3335
|
-
batchSize: 25,
|
|
3336
|
-
retryAttempts: 5,
|
|
3337
|
-
retryDelay: 2000,
|
|
3338
|
-
|
|
3339
|
-
replicators: [
|
|
3340
|
-
// Backup to another S3DB instance
|
|
3341
|
-
{
|
|
3342
|
-
driver: 's3db',
|
|
3343
|
-
resources: ['users', 'products', 'orders'],
|
|
3344
|
-
config: {
|
|
3345
|
-
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup",
|
|
3346
|
-
enabled: true,
|
|
3347
|
-
timeout: 30000
|
|
3348
|
-
}
|
|
3349
|
-
},
|
|
3350
|
-
|
|
3351
|
-
// Real-time events to SQS
|
|
3352
|
-
{
|
|
3353
|
-
driver: 'sqs',
|
|
3354
|
-
resources: ['orders', 'users'],
|
|
3355
|
-
config: {
|
|
3356
|
-
region: 'us-east-1',
|
|
3357
|
-
credentials: {
|
|
3358
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
3359
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
3360
|
-
},
|
|
3361
|
-
// Resource-specific queues
|
|
3362
|
-
queues: {
|
|
3363
|
-
orders: 'https://sqs.us-east-1.amazonaws.com/123456789012/order-events.fifo',
|
|
3364
|
-
users: 'https://sqs.us-east-1.amazonaws.com/123456789012/user-events.fifo'
|
|
3365
|
-
},
|
|
3366
|
-
// Default queue for unspecified resources
|
|
3367
|
-
defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/default-events.fifo',
|
|
3368
|
-
messageGroupId: 's3db-replicator',
|
|
3369
|
-
deduplicationId: true,
|
|
3370
|
-
messageAttributes: {
|
|
3371
|
-
source: { StringValue: 'production-db', DataType: 'String' },
|
|
3372
|
-
version: { StringValue: '1.0', DataType: 'String' }
|
|
3373
|
-
}
|
|
3374
|
-
}
|
|
3375
|
-
},
|
|
3376
|
-
|
|
3377
|
-
// Analytics to BigQuery
|
|
3378
|
-
{
|
|
3379
|
-
driver: 'bigquery',
|
|
3380
|
-
config: {
|
|
3381
|
-
projectId: 'my-analytics-project',
|
|
3382
|
-
datasetId: 's3db_analytics',
|
|
3383
|
-
location: 'US',
|
|
3384
|
-
logTable: 'replication_log',
|
|
3385
|
-
credentials: {
|
|
3386
|
-
client_email: 'service-account@project.iam.gserviceaccount.com',
|
|
3387
|
-
private_key: process.env.BIGQUERY_PRIVATE_KEY,
|
|
3388
|
-
project_id: 'my-analytics-project'
|
|
3389
|
-
}
|
|
3390
|
-
},
|
|
3391
|
-
resources: {
|
|
3392
|
-
// Multiple destinations for users
|
|
3393
|
-
users: [
|
|
3394
|
-
{ actions: ['insert', 'update'], table: 'dim_users' },
|
|
3395
|
-
{ actions: ['insert'], table: 'fact_user_activity' }
|
|
3396
|
-
],
|
|
3397
|
-
|
|
3398
|
-
// Orders to analytics tables
|
|
3399
|
-
orders: [
|
|
3400
|
-
{ actions: ['insert'], table: 'fact_orders' },
|
|
3401
|
-
{ actions: ['insert'], table: 'daily_revenue',
|
|
3402
|
-
transformer: (data) => ({
|
|
3403
|
-
date: data.createdAt?.split('T')[0],
|
|
3404
|
-
revenue: data.amount,
|
|
3405
|
-
customer_id: data.userId,
|
|
3406
|
-
order_count: 1
|
|
3407
|
-
})
|
|
3408
|
-
}
|
|
3409
|
-
],
|
|
3410
|
-
|
|
3411
|
-
// Products with transformation
|
|
3412
|
-
products: {
|
|
3413
|
-
table: 'dim_products',
|
|
3414
|
-
actions: ['insert', 'update'],
|
|
3415
|
-
transformer: (data) => ({
|
|
3416
|
-
...data,
|
|
3417
|
-
price_category: data.price > 100 ? 'premium' : 'standard',
|
|
3418
|
-
last_updated: new Date().toISOString()
|
|
3419
|
-
})
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
},
|
|
3423
|
-
|
|
3424
|
-
// Operational database (PostgreSQL)
|
|
3425
|
-
{
|
|
3426
|
-
driver: 'postgres',
|
|
3427
|
-
config: {
|
|
3428
|
-
connectionString: 'postgresql://analytics:password@localhost:5432/operations',
|
|
3429
|
-
ssl: { rejectUnauthorized: false },
|
|
3430
|
-
logTable: 'replication_log',
|
|
3431
|
-
pool: {
|
|
3432
|
-
max: 20,
|
|
3433
|
-
idleTimeoutMillis: 30000,
|
|
3434
|
-
connectionTimeoutMillis: 2000
|
|
3435
|
-
}
|
|
3436
|
-
},
|
|
3437
|
-
resources: {
|
|
3438
|
-
users: [
|
|
3439
|
-
{
|
|
3440
|
-
actions: ['insert', 'update', 'delete'],
|
|
3441
|
-
table: 'operational_users',
|
|
3442
|
-
transformer: (data, action) => {
|
|
3443
|
-
if (action === 'delete') return { id: data.id, deleted_at: new Date() };
|
|
3444
|
-
return {
|
|
3445
|
-
...data,
|
|
3446
|
-
sync_timestamp: new Date(),
|
|
3447
|
-
source_system: 's3db'
|
|
3448
|
-
};
|
|
3449
|
-
}
|
|
3450
|
-
}
|
|
3451
|
-
],
|
|
3452
|
-
|
|
3453
|
-
orders: [
|
|
3454
|
-
{ actions: ['insert'], table: 'order_events' },
|
|
3455
|
-
{
|
|
3456
|
-
actions: ['update'],
|
|
3457
|
-
table: 'order_updates',
|
|
3458
|
-
transformer: (data) => ({
|
|
3459
|
-
order_id: data.id,
|
|
3460
|
-
updated_fields: Object.keys(data),
|
|
3461
|
-
update_timestamp: new Date()
|
|
3462
|
-
})
|
|
3463
|
-
}
|
|
3464
|
-
]
|
|
3465
|
-
}
|
|
3466
|
-
}
|
|
3467
|
-
]
|
|
3468
|
-
})]
|
|
3469
|
-
});
|
|
3470
|
-
|
|
3471
|
-
await s3db.connect();
|
|
3472
|
-
|
|
3473
|
-
// Advanced replicator management
|
|
3474
|
-
class ReplicatorManager {
|
|
3475
|
-
constructor(replicatorPlugin) {
|
|
3476
|
-
this.plugin = replicatorPlugin;
|
|
3477
|
-
this.stats = {
|
|
3478
|
-
totalReplications: 0,
|
|
3479
|
-
successfulReplications: 0,
|
|
3480
|
-
failedReplications: 0,
|
|
3481
|
-
byReplicator: {},
|
|
3482
|
-
byResource: {}
|
|
3483
|
-
};
|
|
3484
|
-
|
|
3485
|
-
this.setupEventListeners();
|
|
3486
|
-
}
|
|
3487
|
-
|
|
3488
|
-
setupEventListeners() {
|
|
3489
|
-
this.plugin.on('replicator.queued', (data) => {
|
|
3490
|
-
this.stats.totalReplications++;
|
|
3491
|
-
this.updateResourceStats(data.resource, 'queued');
|
|
3492
|
-
});
|
|
3493
|
-
|
|
3494
|
-
this.plugin.on('replicator.success', (data) => {
|
|
3495
|
-
this.stats.successfulReplications++;
|
|
3496
|
-
this.updateReplicatorStats(data.replicator, 'success');
|
|
3497
|
-
this.updateResourceStats(data.resource, 'success');
|
|
3498
|
-
});
|
|
3499
|
-
|
|
3500
|
-
this.plugin.on('replicator.failed', (data) => {
|
|
3501
|
-
this.stats.failedReplications++;
|
|
3502
|
-
this.updateReplicatorStats(data.replicator, 'failed');
|
|
3503
|
-
this.updateResourceStats(data.resource, 'failed');
|
|
3504
|
-
|
|
3505
|
-
// Advanced error handling
|
|
3506
|
-
if (data.error.includes('BigQuery')) {
|
|
3507
|
-
console.log('🔧 BigQuery error detected - checking schema compatibility...');
|
|
3508
|
-
} else if (data.error.includes('SQS')) {
|
|
3509
|
-
console.log('📮 SQS error detected - checking queue permissions...');
|
|
3510
|
-
}
|
|
3511
|
-
});
|
|
3512
|
-
}
|
|
3513
|
-
|
|
3514
|
-
updateReplicatorStats(replicator, status) {
|
|
3515
|
-
if (!this.stats.byReplicator[replicator]) {
|
|
3516
|
-
this.stats.byReplicator[replicator] = { success: 0, failed: 0 };
|
|
3517
|
-
}
|
|
3518
|
-
this.stats.byReplicator[replicator][status]++;
|
|
3519
|
-
}
|
|
3520
|
-
|
|
3521
|
-
updateResourceStats(resource, status) {
|
|
3522
|
-
if (!this.stats.byResource[resource]) {
|
|
3523
|
-
this.stats.byResource[resource] = { queued: 0, success: 0, failed: 0 };
|
|
3524
|
-
}
|
|
3525
|
-
this.stats.byResource[resource][status]++;
|
|
3526
|
-
}
|
|
3527
|
-
|
|
3528
|
-
async getReplicationHealth() {
|
|
3529
|
-
const totalAttempts = this.stats.successfulReplications + this.stats.failedReplications;
|
|
3530
|
-
const successRate = totalAttempts > 0 ? this.stats.successfulReplications / totalAttempts : 1;
|
|
3531
|
-
|
|
3532
|
-
return {
|
|
3533
|
-
overall: {
|
|
3534
|
-
successRate: successRate,
|
|
3535
|
-
totalReplications: this.stats.totalReplications,
|
|
3536
|
-
pending: this.stats.totalReplications - totalAttempts,
|
|
3537
|
-
health: successRate > 0.95 ? 'excellent' :
|
|
3538
|
-
successRate > 0.85 ? 'good' :
|
|
3539
|
-
successRate > 0.7 ? 'warning' : 'critical'
|
|
3540
|
-
},
|
|
3541
|
-
byReplicator: this.stats.byReplicator,
|
|
3542
|
-
byResource: this.stats.byResource
|
|
3543
|
-
};
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
|
-
async pauseReplicator(replicatorId) {
|
|
3547
|
-
const replicator = this.plugin.replicators.find(r => r.id === replicatorId);
|
|
3548
|
-
if (replicator) {
|
|
3549
|
-
replicator.enabled = false;
|
|
3550
|
-
console.log(`⏸️ Paused replicator: ${replicatorId}`);
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
async resumeReplicator(replicatorId) {
|
|
3555
|
-
const replicator = this.plugin.replicators.find(r => r.id === replicatorId);
|
|
3556
|
-
if (replicator) {
|
|
3557
|
-
replicator.enabled = true;
|
|
3558
|
-
console.log(`▶️ Resumed replicator: ${replicatorId}`);
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
|
|
3562
|
-
async testReplicatorConnections() {
|
|
3563
|
-
console.log('🔍 Testing replicator connections...');
|
|
3564
|
-
|
|
3565
|
-
for (const replicator of this.plugin.replicators) {
|
|
3566
|
-
try {
|
|
3567
|
-
const result = await replicator.testConnection();
|
|
3568
|
-
console.log(`✅ ${replicator.driver}: ${result.status}`);
|
|
3569
|
-
} catch (error) {
|
|
3570
|
-
console.log(`❌ ${replicator.driver}: ${error.message}`);
|
|
3571
|
-
}
|
|
3572
|
-
}
|
|
3573
|
-
}
|
|
3574
|
-
}
|
|
3575
|
-
|
|
3576
|
-
// Setup sample data and test all replicators
|
|
3577
|
-
const users = s3db.resource('users');
|
|
3578
|
-
const products = s3db.resource('products');
|
|
3579
|
-
const orders = s3db.resource('orders');
|
|
3580
|
-
|
|
3581
|
-
const replicatorPlugin = s3db.plugins.find(p => p.constructor.name === 'ReplicatorPlugin');
|
|
3582
|
-
const manager = new ReplicatorManager(replicatorPlugin);
|
|
3583
|
-
|
|
3584
|
-
// Test connections
|
|
3585
|
-
await manager.testReplicatorConnections();
|
|
3586
|
-
|
|
3587
|
-
// Create sample data
|
|
3588
|
-
console.log('🔄 Creating sample data with multi-driver replication...');
|
|
3589
|
-
|
|
3590
|
-
const sampleUsers = await users.insertMany([
|
|
3591
|
-
{ name: 'John Smith', email: 'john@example.com', role: 'admin' },
|
|
3592
|
-
{ name: 'Jane Doe', email: 'jane@example.com', role: 'user' },
|
|
3593
|
-
{ name: 'Bob Wilson', email: 'bob@example.com', role: 'user' }
|
|
3594
|
-
]);
|
|
3595
|
-
|
|
3596
|
-
const sampleProducts = await products.insertMany([
|
|
3597
|
-
{ name: 'Laptop Pro', price: 1299.99, category: 'electronics' },
|
|
3598
|
-
{ name: 'Wireless Mouse', price: 29.99, category: 'electronics' },
|
|
3599
|
-
{ name: 'Coffee Mug', price: 12.99, category: 'home' }
|
|
3600
|
-
]);
|
|
3601
|
-
|
|
3602
|
-
const sampleOrders = await orders.insertMany([
|
|
3603
|
-
{ userId: sampleUsers[0].id, amount: 1329.98, items: [sampleProducts[0].id, sampleProducts[1].id] },
|
|
3604
|
-
{ userId: sampleUsers[1].id, amount: 29.99, items: [sampleProducts[1].id] },
|
|
3605
|
-
{ userId: sampleUsers[2].id, amount: 12.99, items: [sampleProducts[2].id] }
|
|
3606
|
-
]);
|
|
3607
|
-
|
|
3608
|
-
// Wait for replications to complete
|
|
3609
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
3610
|
-
|
|
3611
|
-
// Get replication statistics
|
|
3612
|
-
const health = await manager.getReplicationHealth();
|
|
3613
|
-
console.log('\n=== Replication Health Report ===');
|
|
3614
|
-
console.log(`Overall success rate: ${(health.overall.successRate * 100).toFixed(1)}%`);
|
|
3615
|
-
console.log(`Health status: ${health.overall.health.toUpperCase()}`);
|
|
3616
|
-
console.log(`Total replications: ${health.overall.totalReplications}`);
|
|
3617
|
-
console.log(`Pending: ${health.overall.pending}`);
|
|
3618
|
-
|
|
3619
|
-
console.log('\n=== By Replicator ===');
|
|
3620
|
-
Object.entries(health.byReplicator).forEach(([replicator, stats]) => {
|
|
3621
|
-
const total = stats.success + stats.failed;
|
|
3622
|
-
const rate = total > 0 ? (stats.success / total * 100).toFixed(1) : 0;
|
|
3623
|
-
console.log(`${replicator}: ${rate}% success (${stats.success}/${total})`);
|
|
3624
|
-
});
|
|
3625
|
-
|
|
3626
|
-
console.log('\n=== By Resource ===');
|
|
3627
|
-
Object.entries(health.byResource).forEach(([resource, stats]) => {
|
|
3628
|
-
console.log(`${resource}: queued ${stats.queued}, success ${stats.success}, failed ${stats.failed}`);
|
|
3629
|
-
});
|
|
3630
|
-
|
|
3631
|
-
// Get detailed replication logs
|
|
3632
|
-
const replicationLogs = await replicatorPlugin.getReplicatorLogs({ limit: 10 });
|
|
3633
|
-
console.log('\n=== Recent Replication Logs ===');
|
|
3634
|
-
replicationLogs.forEach(log => {
|
|
3635
|
-
const status = log.success ? '✅' : '❌';
|
|
3636
|
-
console.log(`${status} ${log.timestamp} | ${log.action} ${log.resource} → ${log.replicator}`);
|
|
3637
|
-
if (!log.success && log.error) {
|
|
3638
|
-
console.log(` Error: ${log.error}`);
|
|
3639
|
-
}
|
|
3640
|
-
});
|
|
3641
|
-
|
|
3642
|
-
console.log('\n✅ Multi-driver replication demonstration completed');
|
|
3643
|
-
```
|
|
3644
|
-
|
|
3645
|
-
---
|
|
3646
|
-
|
|
3647
|
-
## 📬 Queue Consumer Plugin
|
|
3648
|
-
|
|
3649
|
-
Consume messages from external queues (SQS, RabbitMQ) and automatically process them into your s3db resources.
|
|
3650
|
-
|
|
3651
|
-
### ⚡ Quick Start
|
|
3652
|
-
|
|
3653
|
-
```javascript
|
|
3654
|
-
import { S3db, QueueConsumerPlugin } from 's3db.js';
|
|
3655
|
-
|
|
3656
|
-
const s3db = new S3db({
|
|
3657
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
3658
|
-
plugins: [new QueueConsumerPlugin({
|
|
3659
|
-
consumers: [
|
|
3660
|
-
{
|
|
3661
|
-
driver: 'sqs',
|
|
3662
|
-
config: {
|
|
3663
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue',
|
|
3664
|
-
region: 'us-east-1'
|
|
3665
|
-
},
|
|
3666
|
-
consumers: [
|
|
3667
|
-
{ resources: 'users' }
|
|
3668
|
-
]
|
|
3669
|
-
}
|
|
3670
|
-
]
|
|
3671
|
-
})]
|
|
3672
|
-
});
|
|
3673
|
-
|
|
3674
|
-
await s3db.connect();
|
|
3675
|
-
// Queue messages are automatically processed into your resources
|
|
3676
|
-
```
|
|
3677
|
-
|
|
3678
|
-
### ⚙️ Configuration Parameters
|
|
3679
|
-
|
|
3680
|
-
| Parameter | Type | Default | Description |
|
|
3681
|
-
|-----------|------|---------|-------------|
|
|
3682
|
-
| `enabled` | boolean | `true` | Enable/disable queue consumption |
|
|
3683
|
-
| `consumers` | array | `[]` | Array of consumer configurations |
|
|
3684
|
-
| `batchSize` | number | `10` | Messages to process per batch |
|
|
3685
|
-
| `concurrency` | number | `5` | Concurrent message processing |
|
|
3686
|
-
| `retryAttempts` | number | `3` | Retry failed message processing |
|
|
3687
|
-
| `retryDelay` | number | `1000` | Delay between retries (ms) |
|
|
3688
|
-
| `deadLetterQueue` | string | `null` | DLQ for failed messages |
|
|
3689
|
-
|
|
3690
|
-
### Supported Drivers
|
|
3691
|
-
|
|
3692
|
-
#### SQS Consumer
|
|
3693
|
-
|
|
3694
|
-
Consume from AWS SQS queues:
|
|
3695
|
-
|
|
3696
|
-
```javascript
|
|
3697
|
-
{
|
|
3698
|
-
driver: 'sqs',
|
|
3699
|
-
config: {
|
|
3700
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue',
|
|
3701
|
-
region: 'us-east-1',
|
|
3702
|
-
credentials: { accessKeyId: '...', secretAccessKey: '...' },
|
|
3703
|
-
pollingInterval: 1000,
|
|
3704
|
-
maxMessages: 10,
|
|
3705
|
-
visibilityTimeout: 300
|
|
3706
|
-
},
|
|
3707
|
-
consumers: [
|
|
3708
|
-
{ resources: ['users'], queueUrl: 'specific-queue-url' }
|
|
3709
|
-
]
|
|
3710
|
-
}
|
|
3711
|
-
```
|
|
3712
|
-
|
|
3713
|
-
#### RabbitMQ Consumer
|
|
3714
|
-
|
|
3715
|
-
Consume from RabbitMQ queues:
|
|
3716
|
-
|
|
3717
|
-
```javascript
|
|
3718
|
-
{
|
|
3719
|
-
driver: 'rabbitmq',
|
|
3720
|
-
config: {
|
|
3721
|
-
amqpUrl: 'amqp://user:pass@localhost:5672',
|
|
3722
|
-
exchange: 'my-exchange',
|
|
3723
|
-
prefetch: 10,
|
|
3724
|
-
reconnectInterval: 2000
|
|
3725
|
-
},
|
|
3726
|
-
consumers: [
|
|
3727
|
-
{ resources: ['orders'], queue: 'orders-queue' }
|
|
3728
|
-
]
|
|
3729
|
-
}
|
|
3730
|
-
```
|
|
3731
|
-
|
|
3732
|
-
### Message Format
|
|
3733
|
-
|
|
3734
|
-
Expected message structure:
|
|
3735
|
-
|
|
3736
|
-
```javascript
|
|
3737
|
-
{
|
|
3738
|
-
resource: 'users', // Target resource name
|
|
3739
|
-
action: 'insert', // Operation: insert, update, delete
|
|
3740
|
-
data: { // Data payload
|
|
3741
|
-
name: 'John Doe',
|
|
3742
|
-
email: 'john@example.com'
|
|
3743
|
-
}
|
|
3744
|
-
}
|
|
3745
|
-
```
|
|
3746
|
-
|
|
3747
|
-
### 🔧 Easy Example
|
|
3748
|
-
|
|
3749
|
-
```javascript
|
|
3750
|
-
import { S3db, QueueConsumerPlugin } from 's3db.js';
|
|
3751
|
-
|
|
3752
|
-
const s3db = new S3db({
|
|
3753
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
3754
|
-
plugins: [new QueueConsumerPlugin({
|
|
3755
|
-
enabled: true,
|
|
3756
|
-
consumers: [
|
|
3757
|
-
{
|
|
3758
|
-
driver: 'sqs',
|
|
3759
|
-
config: {
|
|
3760
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/user-updates.fifo',
|
|
3761
|
-
region: 'us-east-1',
|
|
3762
|
-
pollingInterval: 2000,
|
|
3763
|
-
maxMessages: 5
|
|
3764
|
-
},
|
|
3765
|
-
consumers: [
|
|
3766
|
-
{ resources: ['users', 'profiles'] }
|
|
3767
|
-
]
|
|
3768
|
-
}
|
|
3769
|
-
]
|
|
3770
|
-
})]
|
|
3771
|
-
});
|
|
3772
|
-
|
|
3773
|
-
await s3db.connect();
|
|
3774
|
-
|
|
3775
|
-
// Messages are automatically consumed and processed
|
|
3776
|
-
console.log('Queue consumer started - listening for messages...');
|
|
3777
|
-
|
|
3778
|
-
// Simulate sending a message (in real use, external systems send these)
|
|
3779
|
-
const testMessage = {
|
|
3780
|
-
resource: 'users',
|
|
3781
|
-
action: 'insert',
|
|
3782
|
-
data: {
|
|
3783
|
-
name: 'Queue User',
|
|
3784
|
-
email: 'queue@example.com',
|
|
3785
|
-
source: 'external-system'
|
|
3786
|
-
}
|
|
3787
|
-
};
|
|
3788
|
-
|
|
3789
|
-
console.log('Processing message:', testMessage);
|
|
3790
|
-
```
|
|
3791
|
-
|
|
3792
|
-
### 🚀 Advanced Multi-Driver Example
|
|
3793
|
-
|
|
3794
|
-
```javascript
|
|
3795
|
-
import { S3db, QueueConsumerPlugin } from 's3db.js';
|
|
3796
|
-
|
|
3797
|
-
const s3db = new S3db({
|
|
3798
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
3799
|
-
plugins: [new QueueConsumerPlugin({
|
|
3800
|
-
enabled: true,
|
|
3801
|
-
batchSize: 20,
|
|
3802
|
-
concurrency: 10,
|
|
3803
|
-
retryAttempts: 5,
|
|
3804
|
-
retryDelay: 2000,
|
|
3805
|
-
|
|
3806
|
-
consumers: [
|
|
3807
|
-
// SQS Consumer for user events
|
|
3808
|
-
{
|
|
3809
|
-
driver: 'sqs',
|
|
3810
|
-
config: {
|
|
3811
|
-
region: 'us-east-1',
|
|
3812
|
-
credentials: {
|
|
3813
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
|
3814
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
|
|
3815
|
-
},
|
|
3816
|
-
pollingInterval: 1000,
|
|
3817
|
-
maxMessages: 10,
|
|
3818
|
-
visibilityTimeout: 300,
|
|
3819
|
-
waitTimeSeconds: 20 // Long polling
|
|
3820
|
-
},
|
|
3821
|
-
consumers: [
|
|
3822
|
-
{
|
|
3823
|
-
resources: ['users'],
|
|
3824
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/user-events.fifo',
|
|
3825
|
-
messageGroupId: 'user-processing'
|
|
3826
|
-
},
|
|
3827
|
-
{
|
|
3828
|
-
resources: ['orders'],
|
|
3829
|
-
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/order-events.fifo',
|
|
3830
|
-
messageGroupId: 'order-processing'
|
|
3831
|
-
}
|
|
3832
|
-
]
|
|
3833
|
-
},
|
|
3834
|
-
|
|
3835
|
-
// RabbitMQ Consumer for analytics events
|
|
3836
|
-
{
|
|
3837
|
-
driver: 'rabbitmq',
|
|
3838
|
-
config: {
|
|
3839
|
-
amqpUrl: 'amqp://analytics:password@localhost:5672',
|
|
3840
|
-
exchange: 'analytics-events',
|
|
3841
|
-
exchangeType: 'topic',
|
|
3842
|
-
prefetch: 15,
|
|
3843
|
-
reconnectInterval: 3000,
|
|
3844
|
-
heartbeat: 60
|
|
3845
|
-
},
|
|
3846
|
-
consumers: [
|
|
3847
|
-
{
|
|
3848
|
-
resources: ['analytics', 'metrics'],
|
|
3849
|
-
queue: 'analytics-queue',
|
|
3850
|
-
routingKey: 'analytics.*',
|
|
3851
|
-
durable: true
|
|
3852
|
-
},
|
|
3853
|
-
{
|
|
3854
|
-
resources: ['logs'],
|
|
3855
|
-
queue: 'logs-queue',
|
|
3856
|
-
routingKey: 'logs.*',
|
|
3857
|
-
durable: true
|
|
3858
|
-
}
|
|
3859
|
-
]
|
|
3860
|
-
}
|
|
3861
|
-
]
|
|
3862
|
-
})]
|
|
3863
|
-
});
|
|
3864
|
-
|
|
3865
|
-
await s3db.connect();
|
|
3866
|
-
|
|
3867
|
-
// Advanced message processing with custom handlers
|
|
3868
|
-
class QueueMessageProcessor {
|
|
3869
|
-
constructor(queuePlugin) {
|
|
3870
|
-
this.plugin = queuePlugin;
|
|
3871
|
-
this.stats = {
|
|
3872
|
-
processed: 0,
|
|
3873
|
-
errors: 0,
|
|
3874
|
-
byResource: {},
|
|
3875
|
-
byAction: {}
|
|
3876
|
-
};
|
|
3877
|
-
|
|
3878
|
-
this.setupEventListeners();
|
|
3879
|
-
}
|
|
3880
|
-
|
|
3881
|
-
setupEventListeners() {
|
|
3882
|
-
// Listen for message processing events
|
|
3883
|
-
this.plugin.on('message.received', (data) => {
|
|
3884
|
-
console.log(`📨 Received message: ${data.action} on ${data.resource}`);
|
|
3885
|
-
});
|
|
3886
|
-
|
|
3887
|
-
this.plugin.on('message.processed', (data) => {
|
|
3888
|
-
this.stats.processed++;
|
|
3889
|
-
this.updateStats(data.resource, data.action, 'success');
|
|
3890
|
-
console.log(`✅ Processed: ${data.action} on ${data.resource}`);
|
|
3891
|
-
});
|
|
3892
|
-
|
|
3893
|
-
this.plugin.on('message.failed', (data) => {
|
|
3894
|
-
this.stats.errors++;
|
|
3895
|
-
this.updateStats(data.resource, data.action, 'error');
|
|
3896
|
-
console.error(`❌ Failed: ${data.error}`);
|
|
3897
|
-
|
|
3898
|
-
// Custom error handling
|
|
3899
|
-
this.handleProcessingError(data);
|
|
3900
|
-
});
|
|
3901
|
-
}
|
|
3902
|
-
|
|
3903
|
-
updateStats(resource, action, status) {
|
|
3904
|
-
if (!this.stats.byResource[resource]) {
|
|
3905
|
-
this.stats.byResource[resource] = { success: 0, error: 0 };
|
|
3906
|
-
}
|
|
3907
|
-
if (!this.stats.byAction[action]) {
|
|
3908
|
-
this.stats.byAction[action] = { success: 0, error: 0 };
|
|
3909
|
-
}
|
|
3910
|
-
|
|
3911
|
-
this.stats.byResource[resource][status]++;
|
|
3912
|
-
this.stats.byAction[action][status]++;
|
|
3913
|
-
}
|
|
3914
|
-
|
|
3915
|
-
handleProcessingError(errorData) {
|
|
3916
|
-
const { resource, action, error, attempts } = errorData;
|
|
3917
|
-
|
|
3918
|
-
// Log to external monitoring system
|
|
3919
|
-
console.log(`🚨 Error processing ${action} on ${resource}: ${error}`);
|
|
3920
|
-
|
|
3921
|
-
// Custom retry logic
|
|
3922
|
-
if (attempts >= 3) {
|
|
3923
|
-
console.log(`💀 Moving to dead letter queue after ${attempts} attempts`);
|
|
3924
|
-
// In real implementation, move to DLQ
|
|
3925
|
-
}
|
|
3926
|
-
|
|
3927
|
-
// Resource-specific error handling
|
|
3928
|
-
if (resource === 'users' && error.includes('validation')) {
|
|
3929
|
-
console.log('👤 User validation error - checking schema compatibility');
|
|
3930
|
-
} else if (resource === 'orders' && error.includes('duplicate')) {
|
|
3931
|
-
console.log('🛒 Duplicate order detected - implementing idempotency check');
|
|
3932
|
-
}
|
|
3933
|
-
}
|
|
3934
|
-
|
|
3935
|
-
getProcessingStats() {
|
|
3936
|
-
const totalMessages = this.stats.processed + this.stats.errors;
|
|
3937
|
-
const successRate = totalMessages > 0 ? this.stats.processed / totalMessages : 1;
|
|
3938
|
-
|
|
3939
|
-
return {
|
|
3940
|
-
summary: {
|
|
3941
|
-
totalProcessed: this.stats.processed,
|
|
3942
|
-
totalErrors: this.stats.errors,
|
|
3943
|
-
successRate: successRate,
|
|
3944
|
-
health: successRate > 0.95 ? 'excellent' :
|
|
3945
|
-
successRate > 0.85 ? 'good' :
|
|
3946
|
-
successRate > 0.7 ? 'warning' : 'critical'
|
|
3947
|
-
},
|
|
3948
|
-
byResource: this.stats.byResource,
|
|
3949
|
-
byAction: this.stats.byAction
|
|
3950
|
-
};
|
|
3951
|
-
}
|
|
3952
|
-
|
|
3953
|
-
async pauseConsumption() {
|
|
3954
|
-
console.log('⏸️ Pausing queue consumption...');
|
|
3955
|
-
await this.plugin.pause();
|
|
3956
|
-
}
|
|
3957
|
-
|
|
3958
|
-
async resumeConsumption() {
|
|
3959
|
-
console.log('▶️ Resuming queue consumption...');
|
|
3960
|
-
await this.plugin.resume();
|
|
3961
|
-
}
|
|
3962
|
-
}
|
|
3963
|
-
|
|
3964
|
-
// Setup message processing
|
|
3965
|
-
const queuePlugin = s3db.plugins.find(p => p.constructor.name === 'QueueConsumerPlugin');
|
|
3966
|
-
const processor = new QueueMessageProcessor(queuePlugin);
|
|
3967
|
-
|
|
3968
|
-
// Simulate processing for demonstration
|
|
3969
|
-
console.log('🔄 Queue consumers started - processing messages...');
|
|
3970
|
-
|
|
3971
|
-
// In real scenario, messages come from external systems
|
|
3972
|
-
// Here we simulate the processing results
|
|
3973
|
-
setTimeout(async () => {
|
|
3974
|
-
const stats = processor.getProcessingStats();
|
|
3975
|
-
|
|
3976
|
-
console.log('\n=== Queue Processing Stats ===');
|
|
3977
|
-
console.log(`Total processed: ${stats.summary.totalProcessed}`);
|
|
3978
|
-
console.log(`Total errors: ${stats.summary.totalErrors}`);
|
|
3979
|
-
console.log(`Success rate: ${(stats.summary.successRate * 100).toFixed(1)}%`);
|
|
3980
|
-
console.log(`Health: ${stats.summary.health.toUpperCase()}`);
|
|
3981
|
-
|
|
3982
|
-
console.log('\n=== By Resource ===');
|
|
3983
|
-
Object.entries(stats.byResource).forEach(([resource, counts]) => {
|
|
3984
|
-
const total = counts.success + counts.error;
|
|
3985
|
-
console.log(`${resource}: ${counts.success}/${total} successful`);
|
|
3986
|
-
});
|
|
3987
|
-
|
|
3988
|
-
console.log('\n=== By Action ===');
|
|
3989
|
-
Object.entries(stats.byAction).forEach(([action, counts]) => {
|
|
3990
|
-
const total = counts.success + counts.error;
|
|
3991
|
-
console.log(`${action}: ${counts.success}/${total} successful`);
|
|
3992
|
-
});
|
|
3993
|
-
|
|
3994
|
-
}, 5000);
|
|
3995
|
-
|
|
3996
|
-
console.log('\n✅ Queue consumer demonstration completed');
|
|
3997
|
-
```
|
|
3998
|
-
|
|
3999
|
-
---
|
|
4000
|
-
|
|
4001
|
-
## 🔧 Plugin Development
|
|
4002
|
-
|
|
4003
|
-
Create custom plugins to extend s3db.js with your specific requirements.
|
|
4004
|
-
|
|
4005
|
-
### Plugin Base Class
|
|
4006
|
-
|
|
4007
|
-
```javascript
|
|
4008
|
-
import { Plugin } from 's3db.js';
|
|
4009
|
-
|
|
4010
|
-
class MyCustomPlugin extends Plugin {
|
|
4011
|
-
constructor(options = {}) {
|
|
4012
|
-
super(options);
|
|
4013
|
-
this.config = {
|
|
4014
|
-
enabled: options.enabled !== false,
|
|
4015
|
-
...options
|
|
4016
|
-
};
|
|
4017
|
-
}
|
|
4018
|
-
|
|
4019
|
-
async onSetup() {
|
|
4020
|
-
// Initialize plugin after database connection
|
|
4021
|
-
console.log('Setting up MyCustomPlugin');
|
|
4022
|
-
}
|
|
4023
|
-
|
|
4024
|
-
async onStart() {
|
|
4025
|
-
// Plugin is ready to operate
|
|
4026
|
-
console.log('MyCustomPlugin started');
|
|
4027
|
-
}
|
|
4028
|
-
|
|
4029
|
-
async onStop() {
|
|
4030
|
-
// Cleanup before shutdown
|
|
4031
|
-
console.log('MyCustomPlugin stopped');
|
|
4032
|
-
}
|
|
4033
|
-
}
|
|
4034
|
-
```
|
|
4035
|
-
|
|
4036
|
-
### Plugin Lifecycle
|
|
4037
|
-
|
|
4038
|
-
1. **Constructor**: Configure plugin options
|
|
4039
|
-
2. **setup()**: Called when database connects
|
|
4040
|
-
3. **onSetup()**: Initialize plugin resources
|
|
4041
|
-
4. **start()**: Called when database is ready
|
|
4042
|
-
5. **onStart()**: Begin plugin operations
|
|
4043
|
-
6. **stop()**: Called during shutdown
|
|
4044
|
-
7. **onStop()**: Cleanup plugin resources
|
|
4045
|
-
|
|
4046
|
-
### Custom Plugin Example
|
|
4047
|
-
|
|
4048
|
-
```javascript
|
|
4049
|
-
class NotificationPlugin extends Plugin {
|
|
4050
|
-
constructor(options = {}) {
|
|
4051
|
-
super(options);
|
|
4052
|
-
this.config = {
|
|
4053
|
-
enabled: options.enabled !== false,
|
|
4054
|
-
webhookUrl: options.webhookUrl,
|
|
4055
|
-
events: options.events || ['insert', 'update', 'delete'],
|
|
4056
|
-
...options
|
|
4057
|
-
};
|
|
4058
|
-
}
|
|
4059
|
-
|
|
4060
|
-
async onSetup() {
|
|
4061
|
-
// Install hooks for all resources
|
|
4062
|
-
for (const resource of Object.values(this.database.resources)) {
|
|
4063
|
-
this.installResourceHooks(resource);
|
|
4064
|
-
}
|
|
4065
|
-
}
|
|
4066
|
-
|
|
4067
|
-
installResourceHooks(resource) {
|
|
4068
|
-
this.config.events.forEach(event => {
|
|
4069
|
-
resource.on(event, async (data) => {
|
|
4070
|
-
await this.sendNotification(event, resource.name, data);
|
|
4071
|
-
});
|
|
4072
|
-
});
|
|
4073
|
-
}
|
|
4074
|
-
|
|
4075
|
-
async sendNotification(event, resourceName, data) {
|
|
4076
|
-
if (!this.config.webhookUrl) return;
|
|
4077
|
-
|
|
4078
|
-
const payload = {
|
|
4079
|
-
event,
|
|
4080
|
-
resource: resourceName,
|
|
4081
|
-
data,
|
|
4082
|
-
timestamp: new Date().toISOString()
|
|
4083
|
-
};
|
|
4084
|
-
|
|
4085
|
-
try {
|
|
4086
|
-
await fetch(this.config.webhookUrl, {
|
|
4087
|
-
method: 'POST',
|
|
4088
|
-
headers: { 'Content-Type': 'application/json' },
|
|
4089
|
-
body: JSON.stringify(payload)
|
|
4090
|
-
});
|
|
4091
|
-
} catch (error) {
|
|
4092
|
-
console.error('Notification failed:', error);
|
|
4093
|
-
}
|
|
4094
|
-
}
|
|
4095
|
-
}
|
|
4096
|
-
```
|
|
4097
|
-
|
|
4098
|
-
---
|
|
4099
|
-
|
|
4100
|
-
## 💡 Plugin Combinations
|
|
4101
|
-
|
|
4102
|
-
Powerful workflows using multiple plugins together.
|
|
4103
|
-
|
|
4104
|
-
### Complete Monitoring Stack
|
|
4105
|
-
|
|
4106
|
-
```javascript
|
|
4107
|
-
import {
|
|
4108
|
-
S3db,
|
|
4109
|
-
CachePlugin,
|
|
4110
|
-
CostsPlugin,
|
|
4111
|
-
AuditPlugin,
|
|
4112
|
-
FullTextPlugin,
|
|
4113
|
-
MetricsPlugin,
|
|
4114
|
-
ReplicatorPlugin
|
|
4115
|
-
} from 's3db.js';
|
|
4116
|
-
|
|
4117
|
-
const s3db = new S3db({
|
|
4118
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
4119
|
-
plugins: [
|
|
4120
|
-
// Performance optimization
|
|
4121
|
-
new CachePlugin({
|
|
4122
|
-
driver: 'memory',
|
|
4123
|
-
ttl: 600000, // 10 minutes
|
|
4124
|
-
maxSize: 1000
|
|
4125
|
-
}),
|
|
4126
|
-
|
|
4127
|
-
// Cost tracking
|
|
4128
|
-
CostsPlugin,
|
|
4129
|
-
|
|
4130
|
-
// Compliance and security
|
|
4131
|
-
new AuditPlugin({
|
|
4132
|
-
enabled: true,
|
|
4133
|
-
includeData: true,
|
|
4134
|
-
trackOperations: ['insert', 'update', 'delete', 'get']
|
|
4135
|
-
}),
|
|
4136
|
-
|
|
4137
|
-
// Search capabilities
|
|
4138
|
-
new FullTextPlugin({
|
|
4139
|
-
enabled: true,
|
|
4140
|
-
fields: ['name', 'description', 'content', 'tags']
|
|
4141
|
-
}),
|
|
4142
|
-
|
|
4143
|
-
// Performance monitoring
|
|
4144
|
-
new MetricsPlugin({
|
|
4145
|
-
enabled: true,
|
|
4146
|
-
collectPerformance: true,
|
|
4147
|
-
collectErrors: true,
|
|
4148
|
-
flushInterval: 30000
|
|
4149
|
-
}),
|
|
4150
|
-
|
|
4151
|
-
// Data replication
|
|
4152
|
-
new ReplicatorPlugin({
|
|
4153
|
-
replicators: [
|
|
4154
|
-
{
|
|
4155
|
-
driver: 's3db',
|
|
4156
|
-
resources: ['users', 'products', 'orders'],
|
|
4157
|
-
config: {
|
|
4158
|
-
connectionString: "s3://BACKUP_KEY:BACKUP_SECRET@BACKUP_BUCKET/backup"
|
|
4159
|
-
}
|
|
4160
|
-
}
|
|
4161
|
-
]
|
|
4162
|
-
})
|
|
4163
|
-
]
|
|
4164
|
-
});
|
|
4165
|
-
|
|
4166
|
-
await s3db.connect();
|
|
4167
|
-
|
|
4168
|
-
// All plugins work seamlessly together
|
|
4169
|
-
const products = s3db.resource('products');
|
|
4170
|
-
|
|
4171
|
-
// This single operation triggers:
|
|
4172
|
-
// - Audit logging
|
|
4173
|
-
// - Cost tracking
|
|
4174
|
-
// - Performance metrics
|
|
4175
|
-
// - Cache invalidation
|
|
4176
|
-
// - Data replication
|
|
4177
|
-
// - Search indexing
|
|
4178
|
-
await products.insert({
|
|
4179
|
-
name: 'New Product',
|
|
4180
|
-
description: 'Amazing new product with great features',
|
|
4181
|
-
price: 99.99,
|
|
4182
|
-
tags: ['new', 'featured', 'electronics']
|
|
4183
|
-
});
|
|
4184
|
-
```
|
|
4185
|
-
|
|
4186
|
-
### E-commerce Analytics Pipeline
|
|
4187
|
-
|
|
4188
|
-
```javascript
|
|
4189
|
-
const s3db = new S3db({
|
|
4190
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/ecommerce",
|
|
4191
|
-
plugins: [
|
|
4192
|
-
// Real-time search
|
|
4193
|
-
new FullTextPlugin({
|
|
4194
|
-
fields: ['name', 'description', 'brand', 'category'],
|
|
4195
|
-
language: 'en-US',
|
|
4196
|
-
stemming: true
|
|
4197
|
-
}),
|
|
4198
|
-
|
|
4199
|
-
// Performance monitoring
|
|
4200
|
-
new MetricsPlugin({
|
|
4201
|
-
collectPerformance: true,
|
|
4202
|
-
slowQueryThreshold: 500
|
|
4203
|
-
}),
|
|
4204
|
-
|
|
4205
|
-
// Multi-destination replication
|
|
4206
|
-
new ReplicatorPlugin({
|
|
4207
|
-
replicators: [
|
|
4208
|
-
// Backup
|
|
4209
|
-
{ driver: 's3db', resources: '*', config: { connectionString: 'backup-db' } },
|
|
4210
|
-
|
|
4211
|
-
// Analytics warehouse
|
|
4212
|
-
{
|
|
4213
|
-
driver: 'bigquery',
|
|
4214
|
-
resources: {
|
|
4215
|
-
orders: 'fact_orders',
|
|
4216
|
-
products: 'dim_products',
|
|
4217
|
-
users: 'dim_customers'
|
|
4218
|
-
},
|
|
4219
|
-
config: { projectId: 'analytics', datasetId: 'ecommerce' }
|
|
4220
|
-
},
|
|
4221
|
-
|
|
4222
|
-
// Real-time events
|
|
4223
|
-
{
|
|
4224
|
-
driver: 'sqs',
|
|
4225
|
-
resources: ['orders', 'cart_events'],
|
|
4226
|
-
config: { queueUrl: 'order-events-queue' }
|
|
4227
|
-
}
|
|
4228
|
-
]
|
|
4229
|
-
}),
|
|
4230
|
-
|
|
4231
|
-
// Comprehensive auditing
|
|
4232
|
-
new AuditPlugin({
|
|
4233
|
-
trackOperations: ['insert', 'update', 'delete'],
|
|
4234
|
-
includeData: true,
|
|
4235
|
-
excludeResources: ['sessions', 'temp_data']
|
|
4236
|
-
})
|
|
4237
|
-
]
|
|
4238
|
-
});
|
|
4239
|
-
```
|
|
4240
|
-
|
|
4241
|
-
---
|
|
4242
|
-
|
|
4243
|
-
## 🤖 State Machine Plugin
|
|
4244
|
-
|
|
4245
|
-
Finite state machine capabilities for managing complex workflows and business processes with well-defined states and transitions.
|
|
4246
|
-
|
|
4247
|
-
### ⚡ Quick Start
|
|
4248
|
-
|
|
4249
|
-
```javascript
|
|
4250
|
-
import { S3db, StateMachinePlugin } from 's3db.js';
|
|
4251
|
-
|
|
4252
|
-
const s3db = new S3db({
|
|
4253
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
4254
|
-
plugins: [
|
|
4255
|
-
new StateMachinePlugin({
|
|
4256
|
-
stateMachines: {
|
|
4257
|
-
order_processing: {
|
|
4258
|
-
initialState: 'pending',
|
|
4259
|
-
states: {
|
|
4260
|
-
pending: {
|
|
4261
|
-
on: { CONFIRM: 'confirmed', CANCEL: 'cancelled' }
|
|
4262
|
-
},
|
|
4263
|
-
confirmed: {
|
|
4264
|
-
on: { PREPARE: 'preparing', CANCEL: 'cancelled' },
|
|
4265
|
-
entry: 'onConfirmed'
|
|
4266
|
-
},
|
|
4267
|
-
preparing: {
|
|
4268
|
-
on: { SHIP: 'shipped', CANCEL: 'cancelled' },
|
|
4269
|
-
guards: { SHIP: 'canShip' }
|
|
4270
|
-
},
|
|
4271
|
-
shipped: {
|
|
4272
|
-
on: { DELIVER: 'delivered', RETURN: 'returned' }
|
|
4273
|
-
},
|
|
4274
|
-
delivered: { type: 'final' },
|
|
4275
|
-
cancelled: { type: 'final' },
|
|
4276
|
-
returned: { type: 'final' }
|
|
4277
|
-
}
|
|
4278
|
-
}
|
|
4279
|
-
},
|
|
4280
|
-
actions: {
|
|
4281
|
-
onConfirmed: async (context, event, machine) => {
|
|
4282
|
-
console.log(`Order ${context.id} confirmed!`);
|
|
4283
|
-
return { action: 'confirmed', timestamp: new Date() };
|
|
4284
|
-
}
|
|
4285
|
-
},
|
|
4286
|
-
guards: {
|
|
4287
|
-
canShip: async (context, event, machine) => {
|
|
4288
|
-
const inventory = await machine.database.resource('inventory').get(context.productId);
|
|
4289
|
-
return inventory && inventory.quantity >= context.quantity;
|
|
4290
|
-
}
|
|
4291
|
-
}
|
|
4292
|
-
})
|
|
4293
|
-
]
|
|
4294
|
-
});
|
|
4295
|
-
|
|
4296
|
-
await s3db.connect();
|
|
4297
|
-
|
|
4298
|
-
// Initialize entity with state machine
|
|
4299
|
-
await s3db.plugins.stateMachine.initializeEntity('order_processing', 'order123');
|
|
4300
|
-
|
|
4301
|
-
// Send events to trigger transitions
|
|
4302
|
-
await s3db.plugins.stateMachine.send('order_processing', 'order123', 'CONFIRM', {
|
|
4303
|
-
id: 'order123',
|
|
4304
|
-
productId: 'prod1',
|
|
4305
|
-
quantity: 2
|
|
4306
|
-
});
|
|
4307
|
-
|
|
4308
|
-
// Get current state
|
|
4309
|
-
const state = await s3db.plugins.stateMachine.getState('order_processing', 'order123');
|
|
4310
|
-
console.log('Current state:', state); // 'confirmed'
|
|
4311
|
-
```
|
|
4312
|
-
|
|
4313
|
-
### ⚙️ Configuration Parameters
|
|
4314
|
-
|
|
4315
|
-
| Parameter | Type | Default | Description |
|
|
4316
|
-
|-----------|------|---------|-------------|
|
|
4317
|
-
| `stateMachines` | object | `{}` | State machine definitions |
|
|
4318
|
-
| `actions` | object | `{}` | Action functions for state entry/exit |
|
|
4319
|
-
| `guards` | object | `{}` | Guard functions for transition validation |
|
|
4320
|
-
| `persistTransitions` | boolean | `true` | Store transition history in database |
|
|
4321
|
-
| `transitionLogResource` | string | `'transitions'` | Resource name for transition log |
|
|
4322
|
-
| `stateResource` | string | `'entity_states'` | Resource name for current states |
|
|
4323
|
-
| `verbose` | boolean | `false` | Enable detailed logging |
|
|
4324
|
-
|
|
4325
|
-
### State Machine Definition Structure
|
|
4326
|
-
|
|
4327
|
-
```javascript
|
|
4328
|
-
stateMachines: {
|
|
4329
|
-
machine_name: {
|
|
4330
|
-
initialState: 'start_state',
|
|
4331
|
-
states: {
|
|
4332
|
-
state_name: {
|
|
4333
|
-
on: { EVENT_NAME: 'target_state' }, // Transitions
|
|
4334
|
-
entry: 'action_name', // Action on state entry
|
|
4335
|
-
exit: 'action_name', // Action on state exit
|
|
4336
|
-
guards: { EVENT_NAME: 'guard_name' }, // Transition guards
|
|
4337
|
-
meta: { custom: 'metadata' }, // Additional metadata
|
|
4338
|
-
type: 'final' // Mark as final state
|
|
4339
|
-
}
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
```
|
|
4344
|
-
|
|
4345
|
-
### API Methods
|
|
4346
|
-
|
|
4347
|
-
```javascript
|
|
4348
|
-
// Entity management
|
|
4349
|
-
await stateMachine.initializeEntity(machineId, entityId, context);
|
|
4350
|
-
const state = await stateMachine.getState(machineId, entityId);
|
|
4351
|
-
const result = await stateMachine.send(machineId, entityId, event, context);
|
|
4352
|
-
|
|
4353
|
-
// State information
|
|
4354
|
-
const events = stateMachine.getValidEvents(machineId, entityId);
|
|
4355
|
-
const definition = stateMachine.getMachineDefinition(machineId);
|
|
4356
|
-
const machines = stateMachine.getMachines();
|
|
4357
|
-
|
|
4358
|
-
// History and visualization
|
|
4359
|
-
const history = await stateMachine.getTransitionHistory(machineId, entityId);
|
|
4360
|
-
const dot = stateMachine.visualize(machineId); // Graphviz DOT format
|
|
4361
|
-
```
|
|
4362
|
-
|
|
4363
|
-
### Events
|
|
4364
|
-
|
|
4365
|
-
```javascript
|
|
4366
|
-
stateMachine.on('initialized', ({ machines }) => {
|
|
4367
|
-
console.log('Initialized machines:', machines);
|
|
4368
|
-
});
|
|
4369
|
-
|
|
4370
|
-
stateMachine.on('transition', ({ machineId, entityId, from, to, event, context }) => {
|
|
4371
|
-
console.log(`${entityId}: ${from} → ${to} via ${event}`);
|
|
4372
|
-
});
|
|
4373
|
-
|
|
4374
|
-
stateMachine.on('action_error', ({ actionName, error, machineId, entityId }) => {
|
|
4375
|
-
console.error(`Action ${actionName} failed:`, error);
|
|
4376
|
-
});
|
|
4377
|
-
```
|
|
4378
|
-
|
|
4379
|
-
---
|
|
4380
|
-
|
|
4381
|
-
## 💾 Backup Plugin
|
|
4382
|
-
|
|
4383
|
-
**Driver-Based Backup System** - Comprehensive database backup and restore capabilities with configurable drivers, compression, encryption, and retention policies.
|
|
4384
|
-
|
|
4385
|
-
> ⚡ **NEW**: Driver-based architecture supports filesystem, S3, and multi-destination backups with flexible strategies.
|
|
4386
|
-
|
|
4387
|
-
### 🚀 Quick Start
|
|
4388
|
-
|
|
4389
|
-
#### Single Driver (Filesystem)
|
|
4390
|
-
```javascript
|
|
4391
|
-
import { S3db, BackupPlugin } from 's3db.js';
|
|
4392
|
-
|
|
4393
|
-
const s3db = new S3db({
|
|
4394
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp"
|
|
4395
|
-
});
|
|
4396
|
-
|
|
4397
|
-
await s3db.connect();
|
|
4398
|
-
|
|
4399
|
-
// Install backup plugin with filesystem driver
|
|
4400
|
-
const backupPlugin = new BackupPlugin({
|
|
4401
|
-
driver: 'filesystem',
|
|
4402
|
-
config: {
|
|
4403
|
-
path: './backups/{date}/',
|
|
4404
|
-
compression: 'gzip'
|
|
4405
|
-
},
|
|
4406
|
-
retention: {
|
|
4407
|
-
daily: 7,
|
|
4408
|
-
weekly: 4,
|
|
4409
|
-
monthly: 12
|
|
4410
|
-
}
|
|
4411
|
-
});
|
|
4412
|
-
|
|
4413
|
-
await s3db.usePlugin(backupPlugin);
|
|
4414
|
-
|
|
4415
|
-
// Create backups
|
|
4416
|
-
const fullBackup = await backupPlugin.backup('full');
|
|
4417
|
-
console.log('Backup ID:', fullBackup.id);
|
|
4418
|
-
|
|
4419
|
-
// List and restore
|
|
4420
|
-
const backups = await backupPlugin.listBackups();
|
|
4421
|
-
await backupPlugin.restore(fullBackup.id);
|
|
4422
|
-
```
|
|
4423
|
-
|
|
4424
|
-
#### Single Driver (S3)
|
|
4425
|
-
```javascript
|
|
4426
|
-
const backupPlugin = new BackupPlugin({
|
|
4427
|
-
driver: 's3',
|
|
4428
|
-
config: {
|
|
4429
|
-
bucket: 'my-backup-bucket',
|
|
4430
|
-
path: 'database/{date}/',
|
|
4431
|
-
storageClass: 'STANDARD_IA',
|
|
4432
|
-
serverSideEncryption: 'AES256'
|
|
4433
|
-
},
|
|
4434
|
-
compression: 'gzip',
|
|
4435
|
-
verification: true
|
|
4436
|
-
});
|
|
4437
|
-
```
|
|
4438
|
-
|
|
4439
|
-
#### Multi-Driver (Multiple Destinations)
|
|
4440
|
-
```javascript
|
|
4441
|
-
const backupPlugin = new BackupPlugin({
|
|
4442
|
-
driver: 'multi',
|
|
4443
|
-
config: {
|
|
4444
|
-
strategy: 'all', // 'all', 'any', 'priority'
|
|
4445
|
-
destinations: [
|
|
4446
|
-
{
|
|
4447
|
-
driver: 'filesystem',
|
|
4448
|
-
config: { path: '/local/backups/{date}/' }
|
|
4449
|
-
},
|
|
4450
|
-
{
|
|
4451
|
-
driver: 's3',
|
|
4452
|
-
config: {
|
|
4453
|
-
bucket: 'remote-backups',
|
|
4454
|
-
storageClass: 'GLACIER'
|
|
4455
|
-
}
|
|
4456
|
-
}
|
|
4457
|
-
]
|
|
4458
|
-
}
|
|
4459
|
-
});
|
|
4460
|
-
```
|
|
4461
|
-
|
|
4462
|
-
### 🎯 Driver Types
|
|
4463
|
-
|
|
4464
|
-
#### 📁 Filesystem Driver
|
|
4465
|
-
**Perfect for**: Local backups, network storage, development
|
|
4466
|
-
|
|
4467
|
-
```javascript
|
|
4468
|
-
{
|
|
4469
|
-
driver: 'filesystem',
|
|
4470
|
-
config: {
|
|
4471
|
-
path: '/backups/{date}/', // Template path with variables
|
|
4472
|
-
permissions: 0o644, // File permissions
|
|
4473
|
-
directoryPermissions: 0o755 // Directory permissions
|
|
4474
|
-
}
|
|
4475
|
-
}
|
|
4476
|
-
```
|
|
4477
|
-
|
|
4478
|
-
**Path Templates:**
|
|
4479
|
-
- `{date}` → `2024-03-15`
|
|
4480
|
-
- `{time}` → `14-30-45`
|
|
4481
|
-
- `{year}` → `2024`
|
|
4482
|
-
- `{month}` → `03`
|
|
4483
|
-
- `{day}` → `15`
|
|
4484
|
-
- `{backupId}` → `full-2024-03-15T14-30-45-abc123`
|
|
4485
|
-
- `{type}` → `full` | `incremental`
|
|
4486
|
-
|
|
4487
|
-
#### ☁️ S3 Driver
|
|
4488
|
-
**Perfect for**: Cloud backups, long-term storage, disaster recovery
|
|
4489
|
-
|
|
4490
|
-
```javascript
|
|
4491
|
-
{
|
|
4492
|
-
driver: 's3',
|
|
4493
|
-
config: {
|
|
4494
|
-
bucket: 'my-backup-bucket', // S3 bucket (optional, uses database bucket)
|
|
4495
|
-
path: 'backups/{date}/', // S3 key prefix with templates
|
|
4496
|
-
storageClass: 'STANDARD_IA', // S3 storage class
|
|
4497
|
-
serverSideEncryption: 'AES256', // Server-side encryption
|
|
4498
|
-
client: customS3Client // Custom S3 client (optional)
|
|
4499
|
-
}
|
|
4500
|
-
}
|
|
4501
|
-
```
|
|
4502
|
-
|
|
4503
|
-
**Storage Classes:** `STANDARD`, `STANDARD_IA`, `ONEZONE_IA`, `REDUCED_REDUNDANCY`, `GLACIER`, `DEEP_ARCHIVE`
|
|
4504
|
-
|
|
4505
|
-
#### 🔄 Multi Driver
|
|
4506
|
-
**Perfect for**: Redundancy, hybrid storage, complex backup strategies
|
|
4507
|
-
|
|
4508
|
-
```javascript
|
|
4509
|
-
{
|
|
4510
|
-
driver: 'multi',
|
|
4511
|
-
config: {
|
|
4512
|
-
strategy: 'all', // Backup strategy
|
|
4513
|
-
concurrency: 3, // Max concurrent uploads
|
|
4514
|
-
destinations: [
|
|
4515
|
-
{ driver: 'filesystem', config: {...} },
|
|
4516
|
-
{ driver: 's3', config: {...} }
|
|
4517
|
-
]
|
|
4518
|
-
}
|
|
4519
|
-
}
|
|
4520
|
-
```
|
|
4521
|
-
|
|
4522
|
-
**Strategies:**
|
|
4523
|
-
- **`all`**: Upload to all destinations (fail if any fails)
|
|
4524
|
-
- **`any`**: Upload to all, succeed if at least one succeeds
|
|
4525
|
-
- **`priority`**: Try destinations in order, stop on first success
|
|
4526
|
-
|
|
4527
|
-
### 🔧 Configuration Parameters
|
|
4528
|
-
|
|
4529
|
-
| Parameter | Type | Default | Description |
|
|
4530
|
-
|-----------|------|---------|-------------|
|
|
4531
|
-
| **`driver`** | `string` | `'filesystem'` | Driver type: `filesystem`, `s3`, `multi` |
|
|
4532
|
-
| **`config`** | `object` | `{}` | Driver-specific configuration |
|
|
4533
|
-
| `retention` | `object` | `{}` | Retention policy (GFS rotation) |
|
|
4534
|
-
| `include` | `array` | `null` | Resources to include (null = all) |
|
|
4535
|
-
| `exclude` | `array` | `[]` | Resources to exclude |
|
|
4536
|
-
| `compression` | `string` | `'gzip'` | `'none'`, `'gzip'`, `'brotli'`, `'deflate'` |
|
|
4537
|
-
| `encryption` | `object` | `null` | Encryption configuration |
|
|
4538
|
-
| `verification` | `boolean` | `true` | Verify backup integrity |
|
|
4539
|
-
| `tempDir` | `string` | `'./tmp/backups'` | Temporary working directory |
|
|
4540
|
-
| `verbose` | `boolean` | `false` | Enable detailed logging |
|
|
4541
|
-
|
|
4542
|
-
### 🎛️ Backup Types & Operations
|
|
4543
|
-
|
|
4544
|
-
```javascript
|
|
4545
|
-
// Full backup - complete database snapshot
|
|
4546
|
-
const fullBackup = await backupPlugin.backup('full');
|
|
4547
|
-
console.log(`✓ Full backup: ${fullBackup.id} (${fullBackup.size} bytes)`);
|
|
4548
|
-
|
|
4549
|
-
// Incremental backup - changes since last backup
|
|
4550
|
-
const incrementalBackup = await backupPlugin.backup('incremental');
|
|
4551
|
-
|
|
4552
|
-
// Selective backup - specific resources only
|
|
4553
|
-
const selectiveBackup = await backupPlugin.backup('full', {
|
|
4554
|
-
resources: ['users', 'posts']
|
|
4555
|
-
});
|
|
4556
|
-
|
|
4557
|
-
// Custom backup type
|
|
4558
|
-
const customBackup = await backupPlugin.backup('weekly-snapshot');
|
|
4559
|
-
```
|
|
4560
|
-
|
|
4561
|
-
### 📋 Backup Management
|
|
4562
|
-
|
|
4563
|
-
```javascript
|
|
4564
|
-
// List all backups
|
|
4565
|
-
const allBackups = await backupPlugin.listBackups();
|
|
4566
|
-
|
|
4567
|
-
// List with filters
|
|
4568
|
-
const recentBackups = await backupPlugin.listBackups({
|
|
4569
|
-
limit: 10,
|
|
4570
|
-
prefix: 'full-2024'
|
|
4571
|
-
});
|
|
4572
|
-
|
|
4573
|
-
// Get backup status
|
|
4574
|
-
const status = await backupPlugin.getBackupStatus(backupId);
|
|
4575
|
-
console.log(`Status: ${status.status}, Size: ${status.size}`);
|
|
4576
|
-
|
|
4577
|
-
// Restore operations
|
|
4578
|
-
await backupPlugin.restore(backupId); // Full restore
|
|
4579
|
-
await backupPlugin.restore(backupId, { overwrite: true }); // Overwrite existing
|
|
4580
|
-
await backupPlugin.restore(backupId, {
|
|
4581
|
-
resources: ['users']
|
|
4582
|
-
}); // Selective restore
|
|
4583
|
-
```
|
|
4584
|
-
|
|
4585
|
-
### 🔄 Legacy Format Support
|
|
4586
|
-
|
|
4587
|
-
The plugin automatically converts legacy `destinations` format:
|
|
4588
|
-
|
|
4589
|
-
```javascript
|
|
4590
|
-
// ❌ Old format (still works)
|
|
4591
|
-
new BackupPlugin({
|
|
4592
|
-
destinations: [
|
|
4593
|
-
{ type: 'filesystem', path: '/backups/' }
|
|
4594
|
-
]
|
|
4595
|
-
});
|
|
4596
|
-
|
|
4597
|
-
// ✅ Automatically converted to:
|
|
4598
|
-
// driver: 'multi'
|
|
4599
|
-
// config: { destinations: [{ driver: 'filesystem', config: { path: '/backups/' } }] }
|
|
4600
|
-
```
|
|
4601
|
-
|
|
4602
|
-
### 📊 Retention Policies (GFS)
|
|
4603
|
-
|
|
4604
|
-
Grandfather-Father-Son rotation keeps backups efficiently:
|
|
4605
|
-
|
|
4606
|
-
```javascript
|
|
4607
|
-
retention: {
|
|
4608
|
-
daily: 7, // Keep 7 daily backups
|
|
4609
|
-
weekly: 4, // Keep 4 weekly backups
|
|
4610
|
-
monthly: 12, // Keep 12 monthly backups
|
|
4611
|
-
yearly: 3 // Keep 3 yearly backups
|
|
4612
|
-
}
|
|
4613
|
-
```
|
|
4614
|
-
|
|
4615
|
-
### 🎣 Hooks & Events
|
|
4616
|
-
|
|
4617
|
-
```javascript
|
|
4618
|
-
const backupPlugin = new BackupPlugin({
|
|
4619
|
-
driver: 'filesystem',
|
|
4620
|
-
config: { path: './backups/' },
|
|
4621
|
-
|
|
4622
|
-
// Lifecycle hooks
|
|
4623
|
-
onBackupStart: async (type, { backupId }) => {
|
|
4624
|
-
console.log(`🚀 Starting ${type} backup: ${backupId}`);
|
|
4625
|
-
await notifySlack(`Backup ${backupId} started`);
|
|
4626
|
-
},
|
|
4627
|
-
|
|
4628
|
-
onBackupComplete: async (type, stats) => {
|
|
4629
|
-
console.log(`✅ ${type} backup completed:`, {
|
|
4630
|
-
id: stats.backupId,
|
|
4631
|
-
size: `${Math.round(stats.size / 1024)}KB`,
|
|
4632
|
-
duration: `${stats.duration}ms`,
|
|
4633
|
-
destinations: stats.driverInfo
|
|
4634
|
-
});
|
|
4635
|
-
},
|
|
4636
|
-
|
|
4637
|
-
onBackupError: async (type, { backupId, error }) => {
|
|
4638
|
-
console.error(`❌ Backup ${backupId} failed:`, error.message);
|
|
4639
|
-
await alertOps(error);
|
|
4640
|
-
}
|
|
4641
|
-
});
|
|
4642
|
-
|
|
4643
|
-
// Event listeners
|
|
4644
|
-
backupPlugin.on('backup_start', ({ id, type }) => {
|
|
4645
|
-
updateDashboard(`Backup ${id} started`);
|
|
4646
|
-
});
|
|
4647
|
-
|
|
4648
|
-
backupPlugin.on('backup_complete', ({ id, type, size, duration }) => {
|
|
4649
|
-
metrics.record('backup.completed', { type, size, duration });
|
|
4650
|
-
});
|
|
4651
|
-
|
|
4652
|
-
backupPlugin.on('restore_complete', ({ id, restored }) => {
|
|
4653
|
-
console.log(`Restored ${restored.length} resources from ${id}`);
|
|
4654
|
-
});
|
|
4655
|
-
```
|
|
4656
|
-
|
|
4657
|
-
### 🔒 Advanced Security
|
|
4658
|
-
|
|
4659
|
-
```javascript
|
|
4660
|
-
const secureBackupPlugin = new BackupPlugin({
|
|
4661
|
-
driver: 's3',
|
|
4662
|
-
config: {
|
|
4663
|
-
bucket: 'secure-backups',
|
|
4664
|
-
storageClass: 'STANDARD_IA',
|
|
4665
|
-
serverSideEncryption: 'aws:kms',
|
|
4666
|
-
kmsKeyId: 'arn:aws:kms:region:account:key/key-id'
|
|
4667
|
-
},
|
|
4668
|
-
|
|
4669
|
-
// Client-side encryption (before upload)
|
|
4670
|
-
encryption: {
|
|
4671
|
-
algorithm: 'AES-256-GCM',
|
|
4672
|
-
key: process.env.BACKUP_ENCRYPTION_KEY,
|
|
4673
|
-
keyDerivation: {
|
|
4674
|
-
algorithm: 'PBKDF2',
|
|
4675
|
-
iterations: 100000,
|
|
4676
|
-
salt: 'backup-salt-2024'
|
|
4677
|
-
}
|
|
4678
|
-
},
|
|
4679
|
-
|
|
4680
|
-
// Integrity verification
|
|
4681
|
-
verification: true,
|
|
4682
|
-
|
|
4683
|
-
// Compression for efficiency
|
|
4684
|
-
compression: 'gzip'
|
|
4685
|
-
});
|
|
4686
|
-
```
|
|
4687
|
-
|
|
4688
|
-
### 🚀 Production Examples
|
|
4689
|
-
|
|
4690
|
-
#### Enterprise Multi-Region Setup
|
|
4691
|
-
```javascript
|
|
4692
|
-
const enterpriseBackup = new BackupPlugin({
|
|
4693
|
-
driver: 'multi',
|
|
4694
|
-
config: {
|
|
4695
|
-
strategy: 'all',
|
|
4696
|
-
destinations: [
|
|
4697
|
-
{
|
|
4698
|
-
driver: 's3',
|
|
4699
|
-
config: {
|
|
4700
|
-
bucket: 'backups-us-east-1',
|
|
4701
|
-
path: 'production/{date}/',
|
|
4702
|
-
storageClass: 'STANDARD_IA'
|
|
4703
|
-
}
|
|
4704
|
-
},
|
|
4705
|
-
{
|
|
4706
|
-
driver: 's3',
|
|
4707
|
-
config: {
|
|
4708
|
-
bucket: 'backups-eu-west-1',
|
|
4709
|
-
path: 'production/{date}/',
|
|
4710
|
-
storageClass: 'STANDARD_IA'
|
|
4711
|
-
}
|
|
4712
|
-
},
|
|
4713
|
-
{
|
|
4714
|
-
driver: 'filesystem',
|
|
4715
|
-
config: {
|
|
4716
|
-
path: '/mnt/backup-nas/s3db/{date}/'
|
|
4717
|
-
}
|
|
4718
|
-
}
|
|
4719
|
-
]
|
|
4720
|
-
},
|
|
4721
|
-
retention: {
|
|
4722
|
-
daily: 30,
|
|
4723
|
-
weekly: 12,
|
|
4724
|
-
monthly: 24,
|
|
4725
|
-
yearly: 7
|
|
4726
|
-
},
|
|
4727
|
-
verification: true,
|
|
4728
|
-
compression: 'gzip'
|
|
4729
|
-
});
|
|
4730
|
-
```
|
|
4731
|
-
|
|
4732
|
-
#### Development Quick Backup
|
|
4733
|
-
```javascript
|
|
4734
|
-
const devBackup = new BackupPlugin({
|
|
4735
|
-
driver: 'filesystem',
|
|
4736
|
-
config: {
|
|
4737
|
-
path: './dev-backups/{date}/'
|
|
4738
|
-
},
|
|
4739
|
-
compression: 'none',
|
|
4740
|
-
verification: false,
|
|
4741
|
-
verbose: true,
|
|
4742
|
-
retention: { daily: 3 }
|
|
4743
|
-
});
|
|
4744
|
-
```
|
|
4745
|
-
|
|
4746
|
-
### 🎯 CLI Integration
|
|
4747
|
-
|
|
4748
|
-
The BackupPlugin works with s3db CLI commands:
|
|
4749
|
-
|
|
4750
|
-
```bash
|
|
4751
|
-
# Create backups
|
|
4752
|
-
s3db backup full --connection "s3://key:secret@bucket"
|
|
4753
|
-
s3db backup incremental --connection "s3://key:secret@bucket"
|
|
4754
|
-
|
|
4755
|
-
# List and status
|
|
4756
|
-
s3db backup --list --connection "s3://key:secret@bucket"
|
|
4757
|
-
s3db backup --status backup-id --connection "s3://key:secret@bucket"
|
|
4758
|
-
|
|
4759
|
-
# Restore operations
|
|
4760
|
-
s3db restore backup-id --connection "s3://key:secret@bucket"
|
|
4761
|
-
s3db restore backup-id --overwrite --connection "s3://key:secret@bucket"
|
|
4762
|
-
```
|
|
4763
|
-
|
|
4764
|
-
> **Note**: CLI requires the BackupPlugin to be installed in the database instance.
|
|
4765
|
-
|
|
4766
|
-
### 🔍 Driver Information
|
|
4767
|
-
|
|
4768
|
-
```javascript
|
|
4769
|
-
// Get driver details
|
|
4770
|
-
const driverInfo = backupPlugin.driver.getStorageInfo();
|
|
4771
|
-
console.log('Driver type:', driverInfo.type);
|
|
4772
|
-
console.log('Configuration:', driverInfo.config);
|
|
4773
|
-
|
|
4774
|
-
// Multi-driver details
|
|
4775
|
-
if (driverInfo.type === 'multi') {
|
|
4776
|
-
console.log('Strategy:', driverInfo.strategy);
|
|
4777
|
-
driverInfo.destinations.forEach((dest, i) => {
|
|
4778
|
-
console.log(`Destination ${i}:`, dest.driver, dest.info);
|
|
4779
|
-
});
|
|
4780
|
-
}
|
|
4781
|
-
```
|
|
4782
|
-
|
|
4783
|
-
---
|
|
4784
|
-
|
|
4785
|
-
## ⏰ Scheduler Plugin
|
|
4786
|
-
|
|
4787
|
-
Robust job scheduling capabilities with cron expressions, retry logic, and comprehensive monitoring for automated tasks.
|
|
4788
|
-
|
|
4789
|
-
### ⚡ Quick Start
|
|
4790
|
-
|
|
4791
|
-
```javascript
|
|
4792
|
-
import { S3db, SchedulerPlugin } from 's3db.js';
|
|
4793
|
-
|
|
4794
|
-
const s3db = new S3db({
|
|
4795
|
-
connectionString: "s3://ACCESS_KEY:SECRET_KEY@BUCKET_NAME/databases/myapp",
|
|
4796
|
-
plugins: [
|
|
4797
|
-
new SchedulerPlugin({
|
|
4798
|
-
timezone: 'America/Sao_Paulo',
|
|
4799
|
-
jobs: {
|
|
4800
|
-
daily_cleanup: {
|
|
4801
|
-
schedule: '0 3 * * *', // 3 AM daily
|
|
4802
|
-
description: 'Clean up expired sessions',
|
|
4803
|
-
action: async (database, context) => {
|
|
4804
|
-
const expired = await database.resource('sessions').list({
|
|
4805
|
-
where: { expiresAt: { $lt: new Date() } }
|
|
4806
|
-
});
|
|
4807
|
-
|
|
4808
|
-
for (const session of expired) {
|
|
4809
|
-
await database.resource('sessions').delete(session.id);
|
|
4810
|
-
}
|
|
4811
|
-
|
|
4812
|
-
return { deleted: expired.length };
|
|
4813
|
-
},
|
|
4814
|
-
enabled: true,
|
|
4815
|
-
retries: 2,
|
|
4816
|
-
timeout: 30000
|
|
4817
|
-
},
|
|
4818
|
-
|
|
4819
|
-
hourly_metrics: {
|
|
4820
|
-
schedule: '@hourly',
|
|
4821
|
-
description: 'Collect system metrics',
|
|
4822
|
-
action: async (database) => {
|
|
4823
|
-
const metrics = {
|
|
4824
|
-
timestamp: new Date().toISOString(),
|
|
4825
|
-
memory: process.memoryUsage(),
|
|
4826
|
-
uptime: process.uptime()
|
|
4827
|
-
};
|
|
4828
|
-
|
|
4829
|
-
await database.resource('metrics').insert({
|
|
4830
|
-
id: `metrics_${Date.now()}`,
|
|
4831
|
-
...metrics
|
|
4832
|
-
});
|
|
4833
|
-
|
|
4834
|
-
return metrics;
|
|
4835
|
-
}
|
|
4836
|
-
}
|
|
4837
|
-
},
|
|
4838
|
-
onJobComplete: (jobName, result, duration) => {
|
|
4839
|
-
console.log(`Job ${jobName} completed in ${duration}ms`);
|
|
4840
|
-
}
|
|
4841
|
-
})
|
|
4842
|
-
]
|
|
4843
|
-
});
|
|
4844
|
-
|
|
4845
|
-
await s3db.connect();
|
|
4846
|
-
|
|
4847
|
-
// Jobs run automatically based on schedule
|
|
4848
|
-
// Manual execution
|
|
4849
|
-
await s3db.plugins.scheduler.runJob('daily_cleanup');
|
|
4850
|
-
|
|
4851
|
-
// Get job status
|
|
4852
|
-
const allJobs = s3db.plugins.scheduler.getAllJobsStatus();
|
|
4853
|
-
console.log('Scheduled jobs:', allJobs.length);
|
|
4854
|
-
```
|
|
4855
|
-
|
|
4856
|
-
### ⚙️ Configuration Parameters
|
|
4857
|
-
|
|
4858
|
-
| Parameter | Type | Default | Description |
|
|
4859
|
-
|-----------|------|---------|-------------|
|
|
4860
|
-
| `timezone` | string | `'UTC'` | IANA timezone identifier |
|
|
4861
|
-
| `jobs` | object | `{}` | Job definitions |
|
|
4862
|
-
| `defaultTimeout` | number | `60000` | Default job timeout (ms) |
|
|
4863
|
-
| `defaultRetries` | number | `1` | Default retry attempts |
|
|
4864
|
-
| `persistJobs` | boolean | `true` | Store job execution history |
|
|
4865
|
-
| `jobHistoryResource` | string | `'job_history'` | Resource for job history |
|
|
4866
|
-
| `onJobStart` | function | `null` | Callback when job starts |
|
|
4867
|
-
| `onJobComplete` | function | `null` | Callback when job completes |
|
|
4868
|
-
| `onJobError` | function | `null` | Callback when job fails |
|
|
4869
|
-
| `verbose` | boolean | `false` | Enable detailed logging |
|
|
4870
|
-
|
|
4871
|
-
### Job Configuration
|
|
4872
|
-
|
|
4873
|
-
```javascript
|
|
4874
|
-
jobs: {
|
|
4875
|
-
job_name: {
|
|
4876
|
-
schedule: '0 0 * * *', // Cron expression (required)
|
|
4877
|
-
description: 'Job description', // Human-readable description
|
|
4878
|
-
action: async (database, context, schedulerPlugin) => {
|
|
4879
|
-
// Job implementation (required)
|
|
4880
|
-
return { success: true };
|
|
4881
|
-
},
|
|
4882
|
-
enabled: true, // Enable/disable job
|
|
4883
|
-
retries: 2, // Retry attempts on failure
|
|
4884
|
-
timeout: 30000, // Timeout in milliseconds
|
|
4885
|
-
meta: { priority: 'high' } // Custom metadata
|
|
4886
|
-
}
|
|
4887
|
-
}
|
|
4888
|
-
```
|
|
4889
|
-
|
|
4890
|
-
### Cron Expressions
|
|
4891
|
-
|
|
4892
|
-
#### Standard Format
|
|
4893
|
-
```
|
|
4894
|
-
* * * * *
|
|
4895
|
-
│ │ │ │ │
|
|
4896
|
-
│ │ │ │ └─── Day of week (0-7, Sunday = 0 or 7)
|
|
4897
|
-
│ │ │ └───── Month (1-12)
|
|
4898
|
-
│ │ └─────── Day of month (1-31)
|
|
4899
|
-
│ └───────── Hour (0-23)
|
|
4900
|
-
└─────────── Minute (0-59)
|
|
4901
|
-
```
|
|
4902
|
-
|
|
4903
|
-
#### Examples
|
|
4904
|
-
```javascript
|
|
4905
|
-
'0 0 * * *' // Daily at midnight
|
|
4906
|
-
'0 9 * * MON' // Every Monday at 9 AM
|
|
4907
|
-
'*/15 * * * *' // Every 15 minutes
|
|
4908
|
-
'0 2 1 * *' // First day of month at 2 AM
|
|
4909
|
-
```
|
|
4910
|
-
|
|
4911
|
-
#### Shorthand Expressions
|
|
4912
|
-
```javascript
|
|
4913
|
-
'@yearly' // Once a year at midnight on January 1st
|
|
4914
|
-
'@monthly' // Once a month at midnight on the 1st
|
|
4915
|
-
'@weekly' // Once a week at midnight on Sunday
|
|
4916
|
-
'@daily' // Once a day at midnight
|
|
4917
|
-
'@hourly' // Once an hour at the beginning of the hour
|
|
4918
|
-
```
|
|
4919
|
-
|
|
4920
|
-
### API Methods
|
|
4921
|
-
|
|
4922
|
-
```javascript
|
|
4923
|
-
// Job execution
|
|
4924
|
-
const result = await scheduler.runJob(jobName, context);
|
|
4925
|
-
const stats = scheduler.getJobStatus(jobName);
|
|
4926
|
-
const allJobs = scheduler.getAllJobsStatus();
|
|
4927
|
-
|
|
4928
|
-
// Job management
|
|
4929
|
-
scheduler.enableJob(jobName);
|
|
4930
|
-
scheduler.disableJob(jobName);
|
|
4931
|
-
scheduler.addJob(jobName, jobConfig);
|
|
4932
|
-
scheduler.removeJob(jobName);
|
|
4933
|
-
|
|
4934
|
-
// History and monitoring
|
|
4935
|
-
const history = await scheduler.getJobHistory(jobName, { status: 'success', limit: 10 });
|
|
4936
|
-
const stats = scheduler.getJobStatistics(jobName);
|
|
4937
|
-
```
|
|
4938
|
-
|
|
4939
|
-
### Events
|
|
4940
|
-
|
|
4941
|
-
```javascript
|
|
4942
|
-
scheduler.on('job_start', ({ jobName, context }) => {
|
|
4943
|
-
console.log(`Job ${jobName} started`);
|
|
4944
|
-
});
|
|
4945
|
-
|
|
4946
|
-
scheduler.on('job_complete', ({ jobName, result, duration }) => {
|
|
4947
|
-
console.log(`Job ${jobName} completed in ${duration}ms`);
|
|
4948
|
-
});
|
|
4949
|
-
|
|
4950
|
-
scheduler.on('job_error', ({ jobName, error, retryCount }) => {
|
|
4951
|
-
console.error(`Job ${jobName} failed (attempt ${retryCount}):`, error);
|
|
4952
|
-
});
|
|
4953
|
-
|
|
4954
|
-
scheduler.on('job_enabled', ({ jobName }) => {
|
|
4955
|
-
console.log(`Job ${jobName} enabled`);
|
|
4956
|
-
});
|
|
4957
|
-
```
|
|
4958
|
-
|
|
4959
|
-
### Integration with Other Plugins
|
|
4960
|
-
|
|
4961
|
-
```javascript
|
|
4962
|
-
// Scheduled backups
|
|
4963
|
-
jobs: {
|
|
4964
|
-
daily_backup: {
|
|
4965
|
-
schedule: '0 1 * * *',
|
|
4966
|
-
action: async (database) => {
|
|
4967
|
-
const backup = database.getPlugin('BackupPlugin');
|
|
4968
|
-
return await backup.backup('full');
|
|
4969
|
-
}
|
|
4970
|
-
}
|
|
4971
|
-
}
|
|
4972
|
-
|
|
4973
|
-
// Process state machine entities
|
|
4974
|
-
jobs: {
|
|
4975
|
-
process_pending_orders: {
|
|
4976
|
-
schedule: '*/10 * * * *',
|
|
4977
|
-
action: async (database) => {
|
|
4978
|
-
const stateMachine = database.getPlugin('StateMachinePlugin');
|
|
4979
|
-
const orders = await database.resource('orders').list({
|
|
4980
|
-
where: { status: 'pending' }
|
|
4981
|
-
});
|
|
4982
|
-
|
|
4983
|
-
for (const order of orders) {
|
|
4984
|
-
await stateMachine.send('order_processing', order.id, 'AUTO_PROCESS');
|
|
4985
|
-
}
|
|
4986
|
-
|
|
4987
|
-
return { processed: orders.length };
|
|
4988
|
-
}
|
|
4989
|
-
}
|
|
4990
|
-
}
|
|
4991
|
-
```
|
|
4992
|
-
|
|
4993
|
-
---
|
|
4994
|
-
|
|
4995
|
-
## 🎯 Best Practices
|
|
4996
|
-
|
|
4997
|
-
### Plugin Performance
|
|
4998
|
-
|
|
4999
|
-
1. **Enable caching** for read-heavy workloads
|
|
5000
|
-
2. **Monitor costs** in production environments
|
|
5001
|
-
3. **Use appropriate sampling** for metrics collection
|
|
5002
|
-
4. **Configure retention policies** for audit logs
|
|
5003
|
-
5. **Test replicator connections** before deployment
|
|
5004
|
-
6. **Design efficient state machines** with minimal guards and actions
|
|
5005
|
-
7. **Schedule backups during low-traffic periods** to minimize impact
|
|
5006
|
-
8. **Use appropriate job timeouts** to prevent resource exhaustion
|
|
5007
|
-
|
|
5008
|
-
### Plugin Security
|
|
5009
|
-
|
|
5010
|
-
1. **Exclude sensitive resources** from full-text indexing
|
|
5011
|
-
2. **Limit audit data size** to prevent information leakage
|
|
5012
|
-
3. **Use IAM roles** instead of access keys when possible
|
|
5013
|
-
4. **Encrypt replication data** in transit and at rest
|
|
5014
|
-
5. **Validate message sources** in queue consumers
|
|
5015
|
-
6. **Secure state machine actions** to prevent unauthorized transitions
|
|
5016
|
-
7. **Encrypt backup data** for sensitive databases
|
|
5017
|
-
8. **Restrict job execution permissions** to necessary resources only
|
|
5018
|
-
|
|
5019
|
-
### Plugin Monitoring
|
|
5020
|
-
|
|
5021
|
-
1. **Set up alerting** for replication failures
|
|
5022
|
-
2. **Monitor plugin health** with metrics
|
|
5023
|
-
3. **Track error rates** across all plugins
|
|
5024
|
-
4. **Use structured logging** for debugging
|
|
5025
|
-
5. **Implement circuit breakers** for external services
|
|
5026
|
-
6. **Monitor state machine transition rates** and error patterns
|
|
5027
|
-
7. **Track backup success rates** and storage usage
|
|
5028
|
-
8. **Alert on job failures** and execution delays
|
|
5029
|
-
|
|
5030
|
-
---
|
|
5031
|
-
|
|
5032
|
-
**🎉 That's a wrap!** You now have comprehensive documentation for all s3db.js plugins. Each plugin is designed to work independently or in combination with others, providing a powerful and flexible foundation for your database needs.
|
|
5033
|
-
|
|
5034
|
-
For more examples and advanced use cases, check out the `/examples` directory in the s3db.js repository.
|
|
5035
|
-
|
|
5036
|
-
**Happy coding with s3db.js! 🚀**
|