s3db.js 13.3.0 → 13.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,27 +1,615 @@
1
+ /**
2
+ * # QueueConsumerPlugin - Queue Message Consumer for s3db.js
3
+ *
4
+ * ## Overview
5
+ *
6
+ * The QueueConsumerPlugin consumes messages from queue services (AWS SQS, RabbitMQ)
7
+ * and automatically maps them to s3db.js resource operations (insert, update, delete).
8
+ * Perfect for event-driven architectures and asynchronous data processing.
9
+ *
10
+ * ## Features
11
+ *
12
+ * 1. **Multi-Driver Support** - SQS, RabbitMQ, and custom drivers
13
+ * 2. **Automatic Operation Mapping** - Messages automatically execute resource operations
14
+ * 3. **Flexible Configuration** - Configure multiple consumers with different queues
15
+ * 4. **Error Handling** - Built-in error handling with custom hooks
16
+ * 5. **Message Format** - Simple JSON format: { resource, action, data }
17
+ * 6. **Resource Routing** - Route messages to specific resources
18
+ * 7. **Driver-Specific Options** - AWS credentials, RabbitMQ URLs, prefetch, etc.
19
+ *
20
+ * ## Configuration
21
+ *
22
+ * ```javascript
23
+ * import { Database } from 's3db.js';
24
+ * import { QueueConsumerPlugin } from 's3db.js/plugins/queue-consumer';
25
+ *
26
+ * const db = new Database({
27
+ * connectionString: 's3://bucket/db'
28
+ * });
29
+ *
30
+ * // SQS Configuration
31
+ * await db.use(new QueueConsumerPlugin({
32
+ * consumers: [
33
+ * {
34
+ * driver: 'sqs',
35
+ * config: {
36
+ * region: 'us-east-1',
37
+ * credentials: {
38
+ * accessKeyId: 'YOUR_ACCESS_KEY',
39
+ * secretAccessKey: 'YOUR_SECRET_KEY'
40
+ * },
41
+ * pollingInterval: 1000, // Poll every 1 second
42
+ * maxMessages: 10 // Max messages per poll
43
+ * },
44
+ * consumers: [
45
+ * {
46
+ * resources: 'users',
47
+ * queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/users-queue'
48
+ * },
49
+ * {
50
+ * resources: ['orders', 'shipments'],
51
+ * queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/orders-queue'
52
+ * }
53
+ * ]
54
+ * }
55
+ * ]
56
+ * }));
57
+ *
58
+ * // RabbitMQ Configuration
59
+ * await db.use(new QueueConsumerPlugin({
60
+ * consumers: [
61
+ * {
62
+ * driver: 'rabbitmq',
63
+ * config: {
64
+ * amqpUrl: 'amqp://user:pass@localhost:5672',
65
+ * prefetch: 10,
66
+ * reconnectInterval: 2000
67
+ * },
68
+ * consumers: [
69
+ * {
70
+ * resources: 'users',
71
+ * queueName: 'users-queue'
72
+ * }
73
+ * ]
74
+ * }
75
+ * ]
76
+ * }));
77
+ * ```
78
+ *
79
+ * ## Usage Examples
80
+ *
81
+ * ### Basic Queue Consumer (SQS)
82
+ *
83
+ * ```javascript
84
+ * const db = new Database({ connectionString: 's3://bucket/db' });
85
+ *
86
+ * await db.use(new QueueConsumerPlugin({
87
+ * consumers: [
88
+ * {
89
+ * driver: 'sqs',
90
+ * config: {
91
+ * region: 'us-east-1',
92
+ * credentials: {
93
+ * accessKeyId: process.env.AWS_ACCESS_KEY_ID,
94
+ * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
95
+ * }
96
+ * },
97
+ * consumers: [
98
+ * {
99
+ * resources: 'users',
100
+ * queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/users-queue'
101
+ * }
102
+ * ]
103
+ * }
104
+ * ]
105
+ * }));
106
+ *
107
+ * await db.start();
108
+ *
109
+ * // Plugin will now consume messages from the queue
110
+ * // Message format: { resource: 'users', action: 'insert', data: { ... } }
111
+ * ```
112
+ *
113
+ * ### Message Format
114
+ *
115
+ * ```javascript
116
+ * // INSERT operation
117
+ * {
118
+ * "resource": "users",
119
+ * "action": "insert",
120
+ * "data": {
121
+ * "id": "u1",
122
+ * "name": "John Doe",
123
+ * "email": "john@example.com"
124
+ * }
125
+ * }
126
+ *
127
+ * // UPDATE operation
128
+ * {
129
+ * "resource": "users",
130
+ * "action": "update",
131
+ * "data": {
132
+ * "id": "u1",
133
+ * "name": "Jane Doe"
134
+ * }
135
+ * }
136
+ *
137
+ * // DELETE operation
138
+ * {
139
+ * "resource": "users",
140
+ * "action": "delete",
141
+ * "data": {
142
+ * "id": "u1"
143
+ * }
144
+ * }
145
+ *
146
+ * // Messages can be nested in $body for SQS SNS integration
147
+ * {
148
+ * "$body": {
149
+ * "resource": "users",
150
+ * "action": "insert",
151
+ * "data": { ... }
152
+ * }
153
+ * }
154
+ * ```
155
+ *
156
+ * ### Multiple Consumers
157
+ *
158
+ * ```javascript
159
+ * await db.use(new QueueConsumerPlugin({
160
+ * consumers: [
161
+ * // SQS Consumer
162
+ * {
163
+ * driver: 'sqs',
164
+ * config: {
165
+ * region: 'us-east-1',
166
+ * credentials: { ... }
167
+ * },
168
+ * consumers: [
169
+ * { resources: 'users', queueUrl: 'https://...' },
170
+ * { resources: 'orders', queueUrl: 'https://...' }
171
+ * ]
172
+ * },
173
+ * // RabbitMQ Consumer
174
+ * {
175
+ * driver: 'rabbitmq',
176
+ * config: {
177
+ * amqpUrl: 'amqp://localhost:5672'
178
+ * },
179
+ * consumers: [
180
+ * { resources: 'notifications', queueName: 'notifications-queue' }
181
+ * ]
182
+ * }
183
+ * ]
184
+ * }));
185
+ * ```
186
+ *
187
+ * ### Sending Messages to Queue
188
+ *
189
+ * ```javascript
190
+ * // AWS SQS Example (using AWS SDK)
191
+ * import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
192
+ *
193
+ * const sqsClient = new SQSClient({ region: 'us-east-1' });
194
+ *
195
+ * await sqsClient.send(new SendMessageCommand({
196
+ * QueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/users-queue',
197
+ * MessageBody: JSON.stringify({
198
+ * resource: 'users',
199
+ * action: 'insert',
200
+ * data: {
201
+ * id: 'u1',
202
+ * name: 'John Doe',
203
+ * email: 'john@example.com'
204
+ * }
205
+ * })
206
+ * }));
207
+ *
208
+ * // RabbitMQ Example (using amqplib)
209
+ * import amqp from 'amqplib';
210
+ *
211
+ * const connection = await amqp.connect('amqp://localhost');
212
+ * const channel = await connection.createChannel();
213
+ *
214
+ * await channel.sendToQueue(
215
+ * 'users-queue',
216
+ * Buffer.from(JSON.stringify({
217
+ * resource: 'users',
218
+ * action: 'insert',
219
+ * data: {
220
+ * id: 'u1',
221
+ * name: 'John Doe',
222
+ * email: 'john@example.com'
223
+ * }
224
+ * }))
225
+ * );
226
+ * ```
227
+ *
228
+ * ## Best Practices
229
+ *
230
+ * ### 1. Use Resource-Specific Queues
231
+ *
232
+ * ```javascript
233
+ * // GOOD: Separate queues per resource
234
+ * await db.use(new QueueConsumerPlugin({
235
+ * consumers: [
236
+ * {
237
+ * driver: 'sqs',
238
+ * config: { region: 'us-east-1' },
239
+ * consumers: [
240
+ * { resources: 'users', queueUrl: 'https://.../users-queue' },
241
+ * { resources: 'orders', queueUrl: 'https://.../orders-queue' },
242
+ * { resources: 'products', queueUrl: 'https://.../products-queue' }
243
+ * ]
244
+ * }
245
+ * ]
246
+ * }));
247
+ *
248
+ * // OK: Single queue for multiple related resources
249
+ * await db.use(new QueueConsumerPlugin({
250
+ * consumers: [
251
+ * {
252
+ * driver: 'sqs',
253
+ * config: { region: 'us-east-1' },
254
+ * consumers: [
255
+ * {
256
+ * resources: ['orders', 'order_items', 'shipments'],
257
+ * queueUrl: 'https://.../order-processing-queue'
258
+ * }
259
+ * ]
260
+ * }
261
+ * ]
262
+ * }));
263
+ * ```
264
+ *
265
+ * ### 2. Configure Appropriate Polling Intervals
266
+ *
267
+ * ```javascript
268
+ * // High-throughput (frequent polling)
269
+ * await db.use(new QueueConsumerPlugin({
270
+ * consumers: [{
271
+ * driver: 'sqs',
272
+ * config: {
273
+ * pollingInterval: 500, // Poll every 500ms
274
+ * maxMessages: 10 // Process up to 10 messages
275
+ * },
276
+ * consumers: [...]
277
+ * }]
278
+ * }));
279
+ *
280
+ * // Low-throughput (less frequent polling)
281
+ * await db.use(new QueueConsumerPlugin({
282
+ * consumers: [{
283
+ * driver: 'sqs',
284
+ * config: {
285
+ * pollingInterval: 5000, // Poll every 5 seconds
286
+ * maxMessages: 1 // Process 1 message at a time
287
+ * },
288
+ * consumers: [...]
289
+ * }]
290
+ * }));
291
+ * ```
292
+ *
293
+ * ### 3. Validate Messages Before Processing
294
+ *
295
+ * ```javascript
296
+ * // The plugin automatically validates message structure
297
+ * // Ensure your messages include:
298
+ * // - resource: string (required)
299
+ * // - action: 'insert' | 'update' | 'delete' (required)
300
+ * // - data: object (required)
301
+ *
302
+ * // Example of invalid message (will throw QueueError)
303
+ * {
304
+ * "action": "insert", // ❌ Missing 'resource'
305
+ * "data": { ... }
306
+ * }
307
+ *
308
+ * // Example of valid message
309
+ * {
310
+ * "resource": "users", // ✅
311
+ * "action": "insert", // ✅
312
+ * "data": { ... } // ✅
313
+ * }
314
+ * ```
315
+ *
316
+ * ### 4. Use Dead Letter Queues (DLQ)
317
+ *
318
+ * ```javascript
319
+ * // Configure DLQ in AWS SQS Console or via AWS SDK
320
+ * // Messages that fail repeatedly will be sent to DLQ for manual review
321
+ *
322
+ * // Example: Configure DLQ with AWS CDK
323
+ * const dlq = new sqs.Queue(this, 'UsersDLQ', {
324
+ * queueName: 'users-dlq'
325
+ * });
326
+ *
327
+ * const queue = new sqs.Queue(this, 'UsersQueue', {
328
+ * queueName: 'users-queue',
329
+ * deadLetterQueue: {
330
+ * queue: dlq,
331
+ * maxReceiveCount: 3 // Retry 3 times before sending to DLQ
332
+ * }
333
+ * });
334
+ * ```
335
+ *
336
+ * ## Performance Considerations
337
+ *
338
+ * ### Message Processing Throughput
339
+ *
340
+ * - **SQS**: Up to 100 messages/second with default settings
341
+ * - **RabbitMQ**: Up to 1000+ messages/second with prefetch=10
342
+ * - Processing time depends on resource operation complexity
343
+ *
344
+ * ### Optimization Tips
345
+ *
346
+ * ```javascript
347
+ * // 1. Increase maxMessages for batch processing (SQS)
348
+ * await db.use(new QueueConsumerPlugin({
349
+ * consumers: [{
350
+ * driver: 'sqs',
351
+ * config: {
352
+ * maxMessages: 10 // Process 10 messages per poll
353
+ * },
354
+ * consumers: [...]
355
+ * }]
356
+ * }));
357
+ *
358
+ * // 2. Increase prefetch for higher throughput (RabbitMQ)
359
+ * await db.use(new QueueConsumerPlugin({
360
+ * consumers: [{
361
+ * driver: 'rabbitmq',
362
+ * config: {
363
+ * prefetch: 20 // Process 20 messages concurrently
364
+ * },
365
+ * consumers: [...]
366
+ * }]
367
+ * }));
368
+ *
369
+ * // 3. Use multiple consumers for parallel processing
370
+ * await db.use(new QueueConsumerPlugin({
371
+ * consumers: [
372
+ * { driver: 'sqs', config: {...}, consumers: [{resources: 'users', queueUrl: '...'}] },
373
+ * { driver: 'sqs', config: {...}, consumers: [{resources: 'orders', queueUrl: '...'}] }
374
+ * ]
375
+ * }));
376
+ * ```
377
+ *
378
+ * ## Troubleshooting
379
+ *
380
+ * ### Messages Not Being Consumed
381
+ *
382
+ * ```javascript
383
+ * // Check if plugin is started
384
+ * await db.start(); // Must call start() to begin consuming
385
+ *
386
+ * // Check if consumers are running
387
+ * const plugin = db.plugins.QueueConsumerPlugin;
388
+ * console.log(plugin.consumers); // Should show active consumers
389
+ *
390
+ * // Check queue URL/name is correct
391
+ * console.log(plugin.driversConfig);
392
+ * ```
393
+ *
394
+ * ### Resource Not Found Error
395
+ *
396
+ * ```javascript
397
+ * // Error: Resource 'users' not found
398
+ *
399
+ * // Ensure resource is created before starting plugin
400
+ * await db.createResource({
401
+ * name: 'users',
402
+ * attributes: { ... }
403
+ * });
404
+ *
405
+ * await db.use(new QueueConsumerPlugin({...}));
406
+ * await db.start();
407
+ * ```
408
+ *
409
+ * ### Invalid Message Format
410
+ *
411
+ * ```javascript
412
+ * // Error: Resource not found in message
413
+ * // Ensure message includes 'resource' field
414
+ *
415
+ * // Error: Action not found in message
416
+ * // Ensure message includes 'action' field
417
+ *
418
+ * // Error: Unsupported action 'create'
419
+ * // Use 'insert', 'update', or 'delete' only
420
+ *
421
+ * // Check message format
422
+ * console.log(JSON.stringify({
423
+ * resource: 'users', // ✅ Required
424
+ * action: 'insert', // ✅ Required (insert/update/delete)
425
+ * data: { id: 'u1', ... } // ✅ Required
426
+ * }, null, 2));
427
+ * ```
428
+ *
429
+ * ### SQS Credentials Issues
430
+ *
431
+ * ```javascript
432
+ * // Use environment variables
433
+ * await db.use(new QueueConsumerPlugin({
434
+ * consumers: [{
435
+ * driver: 'sqs',
436
+ * config: {
437
+ * region: process.env.AWS_REGION || 'us-east-1',
438
+ * credentials: {
439
+ * accessKeyId: process.env.AWS_ACCESS_KEY_ID,
440
+ * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
441
+ * }
442
+ * },
443
+ * consumers: [...]
444
+ * }]
445
+ * }));
446
+ *
447
+ * // Or use IAM role (no credentials needed)
448
+ * await db.use(new QueueConsumerPlugin({
449
+ * consumers: [{
450
+ * driver: 'sqs',
451
+ * config: { region: 'us-east-1' }, // Uses IAM role automatically
452
+ * consumers: [...]
453
+ * }]
454
+ * }));
455
+ * ```
456
+ *
457
+ * ## Real-World Use Cases
458
+ *
459
+ * ### 1. Event-Driven Data Sync
460
+ *
461
+ * ```javascript
462
+ * // Sync data from external system to s3db via queue
463
+ * await db.use(new QueueConsumerPlugin({
464
+ * consumers: [{
465
+ * driver: 'sqs',
466
+ * config: { region: 'us-east-1' },
467
+ * consumers: [
468
+ * { resources: 'users', queueUrl: 'https://.../external-users-queue' },
469
+ * { resources: 'products', queueUrl: 'https://.../external-products-queue' }
470
+ * ]
471
+ * }]
472
+ * }));
473
+ *
474
+ * // External system sends messages to SQS
475
+ * // s3db automatically processes them
476
+ * ```
477
+ *
478
+ * ### 2. Asynchronous Writes
479
+ *
480
+ * ```javascript
481
+ * // Handle high-volume writes asynchronously
482
+ * // API enqueues writes → Queue Consumer processes them
483
+ *
484
+ * // In API
485
+ * await sqsClient.send(new SendMessageCommand({
486
+ * QueueUrl: 'https://.../analytics-queue',
487
+ * MessageBody: JSON.stringify({
488
+ * resource: 'page_views',
489
+ * action: 'insert',
490
+ * data: { page: '/home', timestamp: new Date(), userId: 'u1' }
491
+ * })
492
+ * }));
493
+ *
494
+ * // Queue Consumer processes asynchronously
495
+ * await db.use(new QueueConsumerPlugin({
496
+ * consumers: [{
497
+ * driver: 'sqs',
498
+ * config: { pollingInterval: 100, maxMessages: 10 },
499
+ * consumers: [{ resources: 'page_views', queueUrl: '...' }]
500
+ * }]
501
+ * }));
502
+ * ```
503
+ *
504
+ * ### 3. Microservices Integration
505
+ *
506
+ * ```javascript
507
+ * // Multiple microservices send events to shared queue
508
+ * // s3db consumes and stores all events
509
+ *
510
+ * await db.use(new QueueConsumerPlugin({
511
+ * consumers: [{
512
+ * driver: 'rabbitmq',
513
+ * config: { amqpUrl: 'amqp://localhost' },
514
+ * consumers: [
515
+ * { resources: 'events', queueName: 'service-events' }
516
+ * ]
517
+ * }]
518
+ * }));
519
+ * ```
520
+ *
521
+ * ### 4. ETL Pipeline
522
+ *
523
+ * ```javascript
524
+ * // Extract → Transform → Load pipeline
525
+ * // Extract: External system → SQS
526
+ * // Transform: Lambda/Worker → Modified message → SQS
527
+ * // Load: s3db consumes and stores
528
+ *
529
+ * await db.use(new QueueConsumerPlugin({
530
+ * consumers: [{
531
+ * driver: 'sqs',
532
+ * config: { region: 'us-east-1' },
533
+ * consumers: [
534
+ * { resources: 'raw_data', queueUrl: 'https://.../raw-queue' },
535
+ * { resources: 'processed_data', queueUrl: 'https://.../processed-queue' }
536
+ * ]
537
+ * }]
538
+ * }));
539
+ * ```
540
+ *
541
+ * ## API Reference
542
+ *
543
+ * ### Constructor Options
544
+ *
545
+ * ```typescript
546
+ * interface QueueConsumerPluginOptions {
547
+ * consumers: Array<{
548
+ * driver: 'sqs' | 'rabbitmq' | string;
549
+ * config: DriverConfig;
550
+ * consumers: Array<{
551
+ * resources: string | string[];
552
+ * queueUrl?: string; // For SQS
553
+ * queueName?: string; // For RabbitMQ
554
+ * [key: string]: any; // Driver-specific options
555
+ * }>;
556
+ * }>;
557
+ * }
558
+ *
559
+ * // SQS Driver Config
560
+ * interface SQSDriverConfig {
561
+ * region: string;
562
+ * credentials?: {
563
+ * accessKeyId: string;
564
+ * secretAccessKey: string;
565
+ * };
566
+ * pollingInterval?: number; // Default: 1000ms
567
+ * maxMessages?: number; // Default: 10
568
+ * }
569
+ *
570
+ * // RabbitMQ Driver Config
571
+ * interface RabbitMQDriverConfig {
572
+ * amqpUrl: string;
573
+ * prefetch?: number; // Default: 10
574
+ * reconnectInterval?: number; // Default: 2000ms
575
+ * }
576
+ * ```
577
+ *
578
+ * ### Message Structure
579
+ *
580
+ * ```typescript
581
+ * interface QueueMessage {
582
+ * resource: string; // Resource name
583
+ * action: 'insert' | 'update' | 'delete'; // Operation
584
+ * data: object; // Operation data
585
+ * }
586
+ *
587
+ * // Optional: Nested in $body
588
+ * interface NestedQueueMessage {
589
+ * $body: QueueMessage;
590
+ * }
591
+ * ```
592
+ *
593
+ * ### Supported Actions
594
+ *
595
+ * - `insert` - Creates new record (calls `resource.insert(data)`)
596
+ * - `update` - Updates existing record (calls `resource.update(data.id, data)`)
597
+ * - `delete` - Deletes record (calls `resource.delete(data.id)`)
598
+ *
599
+ * ## Notes
600
+ *
601
+ * - Messages are processed sequentially per consumer
602
+ * - Failed messages are retried based on queue configuration
603
+ * - Plugin automatically stops all consumers on `db.stop()`
604
+ * - Double-nested messages (SNS→SQS) are automatically unwrapped
605
+ * - Error handling can be customized via `onError` callback
606
+ */
607
+
1
608
  import { Plugin } from './plugin.class.js';
2
609
  import { createConsumer } from './consumers/index.js';
3
610
  import tryFn from "../concerns/try-fn.js";
4
611
  import { QueueError } from "./queue.errors.js";
5
612
 
6
- // Example configuration for SQS:
7
- // const plugin = new QueueConsumerPlugin({
8
- // driver: 'sqs',
9
- // queues: { users: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue' },
10
- // region: 'us-east-1',
11
- // credentials: { accessKeyId: '...', secretAccessKey: '...' },
12
- // poolingInterval: 1000,
13
- // maxMessages: 10,
14
- // });
15
- //
16
- // Example configuration for RabbitMQ:
17
- // const plugin = new QueueConsumerPlugin({
18
- // driver: 'rabbitmq',
19
- // queues: { users: 'users-queue' },
20
- // amqpUrl: 'amqp://user:pass@localhost:5672',
21
- // prefetch: 10,
22
- // reconnectInterval: 2000,
23
- // });
24
-
25
613
  export class QueueConsumerPlugin extends Plugin {
26
614
  constructor(options = {}) {
27
615
  super(options);