s3db.js 6.2.0 → 7.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.
Files changed (60) hide show
  1. package/PLUGINS.md +2724 -0
  2. package/README.md +372 -469
  3. package/UNLICENSE +24 -0
  4. package/dist/s3db.cjs.js +30057 -18387
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +30043 -18384
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +29730 -18061
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -69
  12. package/src/behaviors/body-only.js +110 -0
  13. package/src/behaviors/body-overflow.js +153 -0
  14. package/src/behaviors/enforce-limits.js +195 -0
  15. package/src/behaviors/index.js +39 -0
  16. package/src/behaviors/truncate-data.js +204 -0
  17. package/src/behaviors/user-managed.js +147 -0
  18. package/src/client.class.js +515 -0
  19. package/src/concerns/base62.js +61 -0
  20. package/src/concerns/calculator.js +204 -0
  21. package/src/concerns/crypto.js +142 -0
  22. package/src/concerns/id.js +8 -0
  23. package/src/concerns/index.js +5 -0
  24. package/src/concerns/try-fn.js +151 -0
  25. package/src/connection-string.class.js +75 -0
  26. package/src/database.class.js +599 -0
  27. package/src/errors.js +261 -0
  28. package/src/index.js +17 -0
  29. package/src/plugins/audit.plugin.js +442 -0
  30. package/src/plugins/cache/cache.class.js +53 -0
  31. package/src/plugins/cache/index.js +6 -0
  32. package/src/plugins/cache/memory-cache.class.js +164 -0
  33. package/src/plugins/cache/s3-cache.class.js +189 -0
  34. package/src/plugins/cache.plugin.js +275 -0
  35. package/src/plugins/consumers/index.js +24 -0
  36. package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
  37. package/src/plugins/consumers/sqs-consumer.js +102 -0
  38. package/src/plugins/costs.plugin.js +81 -0
  39. package/src/plugins/fulltext.plugin.js +473 -0
  40. package/src/plugins/index.js +12 -0
  41. package/src/plugins/metrics.plugin.js +603 -0
  42. package/src/plugins/plugin.class.js +210 -0
  43. package/src/plugins/plugin.obj.js +13 -0
  44. package/src/plugins/queue-consumer.plugin.js +134 -0
  45. package/src/plugins/replicator.plugin.js +769 -0
  46. package/src/plugins/replicators/base-replicator.class.js +85 -0
  47. package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
  48. package/src/plugins/replicators/index.js +44 -0
  49. package/src/plugins/replicators/postgres-replicator.class.js +427 -0
  50. package/src/plugins/replicators/s3db-replicator.class.js +352 -0
  51. package/src/plugins/replicators/sqs-replicator.class.js +427 -0
  52. package/src/resource.class.js +2626 -0
  53. package/src/s3db.d.ts +1263 -0
  54. package/src/schema.class.js +706 -0
  55. package/src/stream/index.js +16 -0
  56. package/src/stream/resource-ids-page-reader.class.js +10 -0
  57. package/src/stream/resource-ids-reader.class.js +63 -0
  58. package/src/stream/resource-reader.class.js +81 -0
  59. package/src/stream/resource-writer.class.js +92 -0
  60. package/src/validator.class.js +97 -0
@@ -0,0 +1,427 @@
1
+ /**
2
+ * SQS Replicator Configuration Documentation
3
+ *
4
+ * This replicator sends replicator events to Amazon SQS queues. It supports both
5
+ * resource-specific queues and a single queue for all events, with a flexible message
6
+ * structure that includes operation details and data.
7
+ *
8
+ * ⚠️ REQUIRED DEPENDENCY: You must install the AWS SQS SDK to use this replicator:
9
+ *
10
+ * ```bash
11
+ * npm install @aws-sdk/client-sqs
12
+ * # or
13
+ * yarn add @aws-sdk/client-sqs
14
+ * # or
15
+ * pnpm add @aws-sdk/client-sqs
16
+ * ```
17
+ *
18
+ * @typedef {Object} SQSReplicatorConfig
19
+ * @property {string} region - AWS region where the SQS queues are located
20
+ * @property {string} [accessKeyId] - AWS access key ID (if not using IAM roles)
21
+ * @property {string} [secretAccessKey] - AWS secret access key (if not using IAM roles)
22
+ * @property {string} [sessionToken] - AWS session token for temporary credentials
23
+ * @property {string} [defaultQueueUrl] - Default SQS queue URL for all events when resource-specific queues are not configured
24
+ * @property {Object.<string, string>} [resourceQueues] - Maps s3db resource names to specific SQS queue URLs
25
+ * - Key: s3db resource name (e.g., 'users', 'orders')
26
+ * - Value: SQS queue URL (e.g., 'https://sqs.us-east-1.amazonaws.com/123456789012/users-queue')
27
+ * - If not provided, defaultQueueUrl is used for all resources
28
+ * @property {number} [maxRetries=3] - Maximum number of retry attempts for failed message sends
29
+ * @property {number} [retryDelay=1000] - Delay in milliseconds between retry attempts
30
+ * @property {boolean} [logMessages=false] - Whether to log message details to console for debugging
31
+ * @property {number} [messageDelaySeconds=0] - Delay in seconds before messages become visible in queue
32
+ * @property {Object} [messageAttributes] - Additional attributes to include with every SQS message
33
+ * - Key: attribute name (e.g., 'environment', 'version')
34
+ * - Value: attribute value (e.g., 'production', '1.0.0')
35
+ * @property {string} [messageGroupId] - Message group ID for FIFO queues (required for FIFO queues)
36
+ * @property {boolean} [useFIFO=false] - Whether the target queues are FIFO queues
37
+ * @property {number} [batchSize=10] - Number of messages to send in a single batch (for batch operations)
38
+ * @property {boolean} [compressMessages=false] - Whether to compress message bodies using gzip
39
+ * @property {string} [messageFormat='json'] - Format for message body: 'json' or 'stringified'
40
+ * @property {Object} [sqsClientOptions] - Additional options to pass to the SQS client constructor
41
+ *
42
+ * @example
43
+ * // Configuration with resource-specific queues
44
+ * {
45
+ * region: 'us-east-1',
46
+ * accessKeyId: 'AKIAIOSFODNN7EXAMPLE',
47
+ * secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
48
+ * resourceQueues: {
49
+ * 'users': 'https://sqs.us-east-1.amazonaws.com/123456789012/users-events',
50
+ * 'orders': 'https://sqs.us-east-1.amazonaws.com/123456789012/orders-events',
51
+ * 'products': 'https://sqs.us-east-1.amazonaws.com/123456789012/products-events'
52
+ * },
53
+ * logMessages: true,
54
+ * messageAttributes: {
55
+ * 'environment': 'production',
56
+ * 'source': 's3db-replicator'
57
+ * }
58
+ * }
59
+ *
60
+ * @example
61
+ * // Configuration with single default queue
62
+ * {
63
+ * region: 'us-west-2',
64
+ * defaultQueueUrl: 'https://sqs.us-west-2.amazonaws.com/123456789012/all-events',
65
+ * maxRetries: 5,
66
+ * retryDelay: 2000,
67
+ * compressMessages: true
68
+ * }
69
+ *
70
+ * @example
71
+ * // FIFO queue configuration
72
+ * {
73
+ * region: 'eu-west-1',
74
+ * defaultQueueUrl: 'https://sqs.eu-west-1.amazonaws.com/123456789012/events.fifo',
75
+ * useFIFO: true,
76
+ * messageGroupId: 's3db-events',
77
+ * messageDelaySeconds: 5
78
+ * }
79
+ *
80
+ * @example
81
+ * // Minimal configuration using IAM roles
82
+ * {
83
+ * region: 'us-east-1',
84
+ * defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue'
85
+ * }
86
+ *
87
+ * @notes
88
+ * - Requires AWS credentials with SQS SendMessage permissions
89
+ * - Resource-specific queues take precedence over defaultQueueUrl
90
+ * - Message structure includes: resource, action, data, before (for updates), timestamp, source
91
+ * - FIFO queues require messageGroupId and ensure strict ordering
92
+ * - Message compression reduces bandwidth but increases CPU usage
93
+ * - Batch operations improve performance but may fail if any message in batch fails
94
+ * - Retry mechanism uses exponential backoff for failed sends
95
+ * - Message attributes are useful for filtering and routing in SQS
96
+ * - Message delay is useful for implementing eventual consistency patterns
97
+ * - SQS client options allow for custom endpoint, credentials, etc.
98
+ */
99
+ import BaseReplicator from './base-replicator.class.js';
100
+ import tryFn from "../../concerns/try-fn.js";
101
+
102
+ /**
103
+ * SQS Replicator - Sends data to AWS SQS queues with support for resource-specific queues
104
+ *
105
+ * Configuration options:
106
+ * - queueUrl: Single queue URL for all resources
107
+ * - queues: Object mapping resource names to specific queue URLs
108
+ * - defaultQueueUrl: Fallback queue URL when resource-specific queue is not found
109
+ * - messageGroupId: For FIFO queues
110
+ * - deduplicationId: For FIFO queues
111
+ *
112
+ * Example configurations:
113
+ *
114
+ * // Single queue for all resources
115
+ * {
116
+ * queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue'
117
+ * }
118
+ *
119
+ * // Resource-specific queues
120
+ * {
121
+ * queues: {
122
+ * users: 'https://sqs.us-east-1.amazonaws.com/123456789012/users-queue',
123
+ * orders: 'https://sqs.us-east-1.amazonaws.com/123456789012/orders-queue'
124
+ * },
125
+ * defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/default-queue'
126
+ * }
127
+ */
128
+ class SqsReplicator extends BaseReplicator {
129
+ constructor(config = {}, resources = [], client = null) {
130
+ super(config);
131
+ this.resources = resources;
132
+ this.client = client;
133
+ this.queueUrl = config.queueUrl;
134
+ this.queues = config.queues || {};
135
+ this.defaultQueue = config.defaultQueue || config.defaultQueueUrl || config.queueUrlDefault;
136
+ this.region = config.region || 'us-east-1';
137
+ this.sqsClient = client || null;
138
+ this.messageGroupId = config.messageGroupId;
139
+ this.deduplicationId = config.deduplicationId;
140
+
141
+ // Build queues from resources configuration
142
+ if (resources && typeof resources === 'object') {
143
+ for (const [resourceName, resourceConfig] of Object.entries(resources)) {
144
+ if (resourceConfig.queueUrl) {
145
+ this.queues[resourceName] = resourceConfig.queueUrl;
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ validateConfig() {
152
+ const errors = [];
153
+ if (!this.queueUrl && Object.keys(this.queues).length === 0 && !this.defaultQueue && !this.resourceQueueMap) {
154
+ errors.push('Either queueUrl, queues object, defaultQueue, or resourceQueueMap must be provided');
155
+ }
156
+ return {
157
+ isValid: errors.length === 0,
158
+ errors
159
+ };
160
+ }
161
+
162
+ getQueueUrlsForResource(resource) {
163
+ // Prefer resourceQueueMap if present
164
+ if (this.resourceQueueMap && this.resourceQueueMap[resource]) {
165
+ return this.resourceQueueMap[resource];
166
+ }
167
+ if (this.queues[resource]) {
168
+ return [this.queues[resource]];
169
+ }
170
+ if (this.queueUrl) {
171
+ return [this.queueUrl];
172
+ }
173
+ if (this.defaultQueue) {
174
+ return [this.defaultQueue];
175
+ }
176
+ throw new Error(`No queue URL found for resource '${resource}'`);
177
+ }
178
+
179
+ _applyTransformer(resource, data) {
180
+ const entry = this.resources[resource];
181
+ let result = data;
182
+
183
+ if (!entry) return data;
184
+
185
+ // Check for transform function in resource config
186
+ if (typeof entry.transform === 'function') {
187
+ result = entry.transform(data);
188
+ } else if (typeof entry.transformer === 'function') {
189
+ result = entry.transformer(data);
190
+ }
191
+
192
+ return result || data;
193
+ }
194
+
195
+ /**
196
+ * Create standardized message structure
197
+ */
198
+ createMessage(resource, operation, data, id, beforeData = null) {
199
+ const baseMessage = {
200
+ resource: resource, // padronizado para 'resource'
201
+ action: operation,
202
+ timestamp: new Date().toISOString(),
203
+ source: 's3db-replicator'
204
+ };
205
+
206
+ switch (operation) {
207
+ case 'insert':
208
+ return {
209
+ ...baseMessage,
210
+ data: data
211
+ };
212
+ case 'update':
213
+ return {
214
+ ...baseMessage,
215
+ before: beforeData,
216
+ data: data
217
+ };
218
+ case 'delete':
219
+ return {
220
+ ...baseMessage,
221
+ data: data
222
+ };
223
+ default:
224
+ return {
225
+ ...baseMessage,
226
+ data: data
227
+ };
228
+ }
229
+ }
230
+
231
+ async initialize(database, client) {
232
+ await super.initialize(database);
233
+ if (!this.sqsClient) {
234
+ const [ok, err, sdk] = await tryFn(() => import('@aws-sdk/client-sqs'));
235
+ if (!ok) {
236
+ this.emit('initialization_error', {
237
+ replicator: this.name,
238
+ error: err.message
239
+ });
240
+ throw err;
241
+ }
242
+ const { SQSClient } = sdk;
243
+ this.sqsClient = client || new SQSClient({
244
+ region: this.region,
245
+ credentials: this.config.credentials
246
+ });
247
+ this.emit('initialized', {
248
+ replicator: this.name,
249
+ queueUrl: this.queueUrl,
250
+ queues: this.queues,
251
+ defaultQueue: this.defaultQueue
252
+ });
253
+ }
254
+ }
255
+
256
+ async replicate(resource, operation, data, id, beforeData = null) {
257
+ if (!this.enabled || !this.shouldReplicateResource(resource)) {
258
+ return { skipped: true, reason: 'resource_not_included' };
259
+ }
260
+ const [ok, err, result] = await tryFn(async () => {
261
+ const { SendMessageCommand } = await import('@aws-sdk/client-sqs');
262
+ const queueUrls = this.getQueueUrlsForResource(resource);
263
+ // Apply transformation before creating message
264
+ const transformedData = this._applyTransformer(resource, data);
265
+ const message = this.createMessage(resource, operation, transformedData, id, beforeData);
266
+ const results = [];
267
+ for (const queueUrl of queueUrls) {
268
+ const command = new SendMessageCommand({
269
+ QueueUrl: queueUrl,
270
+ MessageBody: JSON.stringify(message),
271
+ MessageGroupId: this.messageGroupId,
272
+ MessageDeduplicationId: this.deduplicationId ? `${resource}:${operation}:${id}` : undefined
273
+ });
274
+ const result = await this.sqsClient.send(command);
275
+ results.push({ queueUrl, messageId: result.MessageId });
276
+ this.emit('replicated', {
277
+ replicator: this.name,
278
+ resource,
279
+ operation,
280
+ id,
281
+ queueUrl,
282
+ messageId: result.MessageId,
283
+ success: true
284
+ });
285
+ }
286
+ return { success: true, results };
287
+ });
288
+ if (ok) return result;
289
+ this.emit('replicator_error', {
290
+ replicator: this.name,
291
+ resource,
292
+ operation,
293
+ id,
294
+ error: err.message
295
+ });
296
+ return { success: false, error: err.message };
297
+ }
298
+
299
+ async replicateBatch(resource, records) {
300
+ if (!this.enabled || !this.shouldReplicateResource(resource)) {
301
+ return { skipped: true, reason: 'resource_not_included' };
302
+ }
303
+ const [ok, err, result] = await tryFn(async () => {
304
+ const { SendMessageBatchCommand } = await import('@aws-sdk/client-sqs');
305
+ const queueUrls = this.getQueueUrlsForResource(resource);
306
+ // SQS batch limit is 10 messages
307
+ const batchSize = 10;
308
+ const batches = [];
309
+ for (let i = 0; i < records.length; i += batchSize) {
310
+ batches.push(records.slice(i, i + batchSize));
311
+ }
312
+ const results = [];
313
+ const errors = [];
314
+ for (const batch of batches) {
315
+ const [okBatch, errBatch] = await tryFn(async () => {
316
+ const entries = batch.map((record, index) => ({
317
+ Id: `${record.id}-${index}`,
318
+ MessageBody: JSON.stringify(this.createMessage(
319
+ resource,
320
+ record.operation,
321
+ record.data,
322
+ record.id,
323
+ record.beforeData
324
+ )),
325
+ MessageGroupId: this.messageGroupId,
326
+ MessageDeduplicationId: this.deduplicationId ?
327
+ `${resource}:${record.operation}:${record.id}` : undefined
328
+ }));
329
+ const command = new SendMessageBatchCommand({
330
+ QueueUrl: queueUrls[0], // Assuming all queueUrls in a batch are the same for batching
331
+ Entries: entries
332
+ });
333
+ const result = await this.sqsClient.send(command);
334
+ results.push(result);
335
+ });
336
+ if (!okBatch) {
337
+ errors.push({ batch: batch.length, error: errBatch.message });
338
+ // If this is a critical error (like connection failure), fail the entire operation
339
+ if (errBatch.message && (errBatch.message.includes('Batch error') || errBatch.message.includes('Connection') || errBatch.message.includes('Network'))) {
340
+ throw errBatch;
341
+ }
342
+ }
343
+ }
344
+ this.emit('batch_replicated', {
345
+ replicator: this.name,
346
+ resource,
347
+ queueUrl: queueUrls[0], // Assuming all queueUrls in a batch are the same for batching
348
+ total: records.length,
349
+ successful: results.length,
350
+ errors: errors.length
351
+ });
352
+ return {
353
+ success: errors.length === 0,
354
+ results,
355
+ errors,
356
+ total: records.length,
357
+ queueUrl: queueUrls[0] // Assuming all queueUrls in a batch are the same for batching
358
+ };
359
+ });
360
+ if (ok) return result;
361
+ const errorMessage = err?.message || err || 'Unknown error';
362
+ this.emit('batch_replicator_error', {
363
+ replicator: this.name,
364
+ resource,
365
+ error: errorMessage
366
+ });
367
+ return { success: false, error: errorMessage };
368
+ }
369
+
370
+ async testConnection() {
371
+ const [ok, err] = await tryFn(async () => {
372
+ if (!this.sqsClient) {
373
+ await this.initialize(this.database);
374
+ }
375
+ // Try to get queue attributes to test connection
376
+ const { GetQueueAttributesCommand } = await import('@aws-sdk/client-sqs');
377
+ const command = new GetQueueAttributesCommand({
378
+ QueueUrl: this.queueUrl,
379
+ AttributeNames: ['QueueArn']
380
+ });
381
+ await this.sqsClient.send(command);
382
+ return true;
383
+ });
384
+ if (ok) return true;
385
+ this.emit('connection_error', {
386
+ replicator: this.name,
387
+ error: err.message
388
+ });
389
+ return false;
390
+ }
391
+
392
+ async getStatus() {
393
+ const baseStatus = await super.getStatus();
394
+ return {
395
+ ...baseStatus,
396
+ connected: !!this.sqsClient,
397
+ queueUrl: this.queueUrl,
398
+ region: this.region,
399
+ resources: this.resources,
400
+ totalreplicators: this.listenerCount('replicated'),
401
+ totalErrors: this.listenerCount('replicator_error')
402
+ };
403
+ }
404
+
405
+ async cleanup() {
406
+ if (this.sqsClient) {
407
+ this.sqsClient.destroy();
408
+ }
409
+ await super.cleanup();
410
+ }
411
+
412
+ shouldReplicateResource(resource) {
413
+ // Return true if:
414
+ // 1. Resource has a specific queue mapping, OR
415
+ // 2. Resource has a queue in the queues object, OR
416
+ // 3. A default queue is configured (accepts all resources), OR
417
+ // 4. Resource is in the resources list (if provided)
418
+ const result = (this.resourceQueueMap && Object.keys(this.resourceQueueMap).includes(resource))
419
+ || (this.queues && Object.keys(this.queues).includes(resource))
420
+ || !!(this.defaultQueue || this.queueUrl) // Default queue accepts all resources
421
+ || (this.resources && Object.keys(this.resources).includes(resource))
422
+ || false;
423
+ return result;
424
+ }
425
+ }
426
+
427
+ export default SqsReplicator;