s3db.js 7.2.0 → 7.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,134 +1,34 @@
1
+ import tryFn from "#src/concerns/try-fn.js";
2
+ import BaseReplicator from './base-replicator.class.js';
3
+
1
4
  /**
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:
5
+ * SQS Replicator - Send data changes to AWS SQS queues
9
6
  *
7
+ * ⚠️ REQUIRED DEPENDENCY: You must install the AWS SQS SDK:
10
8
  * ```bash
11
- * npm install @aws-sdk/client-sqs
12
- * # or
13
- * yarn add @aws-sdk/client-sqs
14
- * # or
15
9
  * pnpm add @aws-sdk/client-sqs
16
10
  * ```
17
11
  *
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
- * }
12
+ * Configuration:
13
+ * @param {string} region - AWS region (required)
14
+ * @param {string} queueUrl - Single queue URL for all resources
15
+ * @param {Object} queues - Resource-specific queue mapping { resource: queueUrl }
16
+ * @param {string} defaultQueueUrl - Fallback queue URL
17
+ * @param {string} messageGroupId - Message group ID for FIFO queues
18
+ * @param {boolean} deduplicationId - Enable deduplication for FIFO queues
19
+ * @param {Object} credentials - AWS credentials (optional, uses default if omitted)
79
20
  *
80
21
  * @example
81
- * // Minimal configuration using IAM roles
82
- * {
22
+ * new SqsReplicator({
83
23
  * region: 'us-east-1',
84
- * defaultQueueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue'
85
- * }
24
+ * queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/events-queue'
25
+ * }, ['users', 'orders'])
86
26
  *
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
- * }
27
+ * See PLUGINS.md for comprehensive configuration documentation.
127
28
  */
128
29
  class SqsReplicator extends BaseReplicator {
129
30
  constructor(config = {}, resources = [], client = null) {
130
31
  super(config);
131
- this.resources = resources;
132
32
  this.client = client;
133
33
  this.queueUrl = config.queueUrl;
134
34
  this.queues = config.queues || {};
@@ -138,13 +38,26 @@ class SqsReplicator extends BaseReplicator {
138
38
  this.messageGroupId = config.messageGroupId;
139
39
  this.deduplicationId = config.deduplicationId;
140
40
 
141
- // Build queues from resources configuration
142
- if (resources && typeof resources === 'object') {
41
+ // Normalize resources to object format
42
+ if (Array.isArray(resources)) {
43
+ this.resources = {};
44
+ for (const resource of resources) {
45
+ if (typeof resource === 'string') {
46
+ this.resources[resource] = true;
47
+ } else if (typeof resource === 'object' && resource.name) {
48
+ this.resources[resource.name] = resource;
49
+ }
50
+ }
51
+ } else if (typeof resources === 'object') {
52
+ this.resources = resources;
53
+ // Build queues from resources configuration
143
54
  for (const [resourceName, resourceConfig] of Object.entries(resources)) {
144
- if (resourceConfig.queueUrl) {
55
+ if (resourceConfig && resourceConfig.queueUrl) {
145
56
  this.queues[resourceName] = resourceConfig.queueUrl;
146
57
  }
147
58
  }
59
+ } else {
60
+ this.resources = {};
148
61
  }
149
62
  }
150
63
 
@@ -396,7 +309,7 @@ class SqsReplicator extends BaseReplicator {
396
309
  connected: !!this.sqsClient,
397
310
  queueUrl: this.queueUrl,
398
311
  region: this.region,
399
- resources: this.resources,
312
+ resources: Object.keys(this.resources || {}),
400
313
  totalreplicators: this.listenerCount('replicated'),
401
314
  totalErrors: this.listenerCount('replicator_error')
402
315
  };
@@ -171,7 +171,7 @@ export class Resource extends EventEmitter {
171
171
  if (typeof fn === 'function') {
172
172
  this.hooks[event].push(fn.bind(this));
173
173
  }
174
- // Se não for função, ignore silenciosamente
174
+ // If not a function, ignore silently
175
175
  }
176
176
  }
177
177
  }
@@ -704,7 +704,7 @@ export class Resource extends EventEmitter {
704
704
  // LOG: body e contentType antes do putObject
705
705
  // Only throw if behavior is 'body-only' and body is empty
706
706
  if (this.behavior === 'body-only' && (!body || body === "")) {
707
- throw new Error(`[Resource.insert] Tentativa de gravar objeto sem body! Dados: id=${finalId}, resource=${this.name}`);
707
+ throw new Error(`[Resource.insert] Attempt to save object without body! Data: id=${finalId}, resource=${this.name}`);
708
708
  }
709
709
  // For other behaviors, allow empty body (all data in metadata)
710
710
  // Before putObject in insert
@@ -753,7 +753,7 @@ export class Resource extends EventEmitter {
753
753
 
754
754
  // Execute afterInsert hooks
755
755
  const finalResult = await this.executeHooks('afterInsert', insertedData);
756
- // Emit event com dados antes dos hooks afterInsert
756
+ // Emit event with data before afterInsert hooks
757
757
  this.emit("insert", {
758
758
  ...insertedData,
759
759
  $before: { ...completeData },
@@ -774,7 +774,7 @@ export class Resource extends EventEmitter {
774
774
  if (isEmpty(id)) throw new Error('id cannot be empty');
775
775
 
776
776
  const key = this.getResourceKey(id);
777
- // LOG: início do get
777
+ // LOG: start of get
778
778
  // eslint-disable-next-line no-console
779
779
  const [ok, err, request] = await tryFn(() => this.client.getObject(key));
780
780
  // LOG: resultado do headObject
@@ -788,7 +788,7 @@ export class Resource extends EventEmitter {
788
788
  id
789
789
  });
790
790
  }
791
- // Se o objeto existe mas não tem conteúdo, lançar erro NoSuchKey
791
+ // If object exists but has no content, throw NoSuchKey error
792
792
  if (request.ContentLength === 0) {
793
793
  const noContentErr = new Error(`No such key: ${key} [bucket:${this.client.config.bucket}]`);
794
794
  noContentErr.name = 'NoSuchKey';
@@ -1346,7 +1346,7 @@ export class Resource extends EventEmitter {
1346
1346
  prefix = `resource=${this.name}/partition=${partition}`;
1347
1347
  }
1348
1348
  } else {
1349
- // List from main resource (sem versão no path)
1349
+ // List from main resource (without version in path)
1350
1350
  prefix = `resource=${this.name}/data`;
1351
1351
  }
1352
1352
  // Use getKeysPage for real pagination support
@@ -1892,7 +1892,7 @@ export class Resource extends EventEmitter {
1892
1892
  for (const [partitionName, partition] of Object.entries(partitions)) {
1893
1893
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
1894
1894
  if (partitionKey) {
1895
- // Salvar apenas a versão como metadado, nunca atributos do objeto
1895
+ // Save only version as metadata, never object attributes
1896
1896
  const partitionMetadata = {
1897
1897
  _v: String(this.version)
1898
1898
  };
@@ -2082,7 +2082,7 @@ export class Resource extends EventEmitter {
2082
2082
  // Create new partition reference if new key exists
2083
2083
  if (newPartitionKey) {
2084
2084
  const [ok, err] = await tryFn(async () => {
2085
- // Salvar apenas a versão como metadado
2085
+ // Save only version as metadata
2086
2086
  const partitionMetadata = {
2087
2087
  _v: String(this.version)
2088
2088
  };
@@ -2101,7 +2101,7 @@ export class Resource extends EventEmitter {
2101
2101
  } else if (newPartitionKey) {
2102
2102
  // If partition keys are the same, just update the existing reference
2103
2103
  const [ok, err] = await tryFn(async () => {
2104
- // Salvar apenas a versão como metadado
2104
+ // Save only version as metadata
2105
2105
  const partitionMetadata = {
2106
2106
  _v: String(this.version)
2107
2107
  };
@@ -2138,7 +2138,7 @@ export class Resource extends EventEmitter {
2138
2138
  }
2139
2139
  const partitionKey = this.getPartitionKey({ partitionName, id: data.id, data });
2140
2140
  if (partitionKey) {
2141
- // Salvar apenas a versão como metadado
2141
+ // Save only version as metadata
2142
2142
  const partitionMetadata = {
2143
2143
  _v: String(this.version)
2144
2144
  };
@@ -2306,8 +2306,8 @@ export class Resource extends EventEmitter {
2306
2306
  }
2307
2307
 
2308
2308
  /**
2309
- * Compose the full object (metadata + body) as retornado por .get(),
2310
- * usando os dados em memória após insert/update, de acordo com o behavior
2309
+ * Compose the full object (metadata + body) as returned by .get(),
2310
+ * using in-memory data after insert/update, according to behavior
2311
2311
  */
2312
2312
  async composeFullObjectFromWrite({ id, metadata, body, behavior }) {
2313
2313
  // Preserve behavior flags before unmapping
@@ -2464,7 +2464,7 @@ export class Resource extends EventEmitter {
2464
2464
  this._middlewares.get(method).push(fn);
2465
2465
  }
2466
2466
 
2467
- // Utilitário para aplicar valores default do schema
2467
+ // Utility to apply schema default values
2468
2468
  applyDefaults(data) {
2469
2469
  const out = { ...data };
2470
2470
  for (const [key, def] of Object.entries(this.attributes)) {
@@ -2473,7 +2473,7 @@ export class Resource extends EventEmitter {
2473
2473
  const match = def.match(/default:([^|]+)/);
2474
2474
  if (match) {
2475
2475
  let val = match[1];
2476
- // Conversão para boolean/number se necessário
2476
+ // Convert to boolean/number if necessary
2477
2477
  if (def.includes('boolean')) val = val === 'true';
2478
2478
  else if (def.includes('number')) val = Number(val);
2479
2479
  out[key] = val;
package/src/s3db.d.ts CHANGED
@@ -418,23 +418,22 @@ declare module 's3db.js' {
418
418
  export interface BigQueryReplicatorConfig {
419
419
  projectId: string;
420
420
  datasetId: string;
421
- keyFilename?: string;
422
421
  credentials?: Record<string, any>;
423
- tableMapping?: Record<string, string>;
424
- logOperations?: boolean;
422
+ location?: string;
423
+ logTable?: string;
425
424
  batchSize?: number;
426
425
  maxRetries?: number;
427
- retryDelay?: number;
428
- writeDisposition?: 'WRITE_TRUNCATE' | 'WRITE_APPEND' | 'WRITE_EMPTY';
429
- createDisposition?: 'CREATE_IF_NEEDED' | 'CREATE_NEVER';
430
- schema?: Record<string, any>[];
431
- location?: string;
432
- clustering?: string[];
433
- partitioning?: {
434
- type: 'DAY' | 'HOUR' | 'MONTH' | 'YEAR';
435
- field?: string;
436
- };
437
- labels?: Record<string, string>;
426
+ writeDisposition?: string;
427
+ createDisposition?: string;
428
+ tableMapping?: Record<string, string>;
429
+ logOperations?: boolean;
430
+ }
431
+
432
+ /** BigQuery Resource Configuration */
433
+ export interface BigQueryResourceConfig {
434
+ table: string;
435
+ actions?: ('insert' | 'update' | 'delete')[];
436
+ transform?: (data: any) => any;
438
437
  }
439
438
 
440
439
  /** Postgres Replicator config */
@@ -1041,7 +1040,7 @@ declare module 's3db.js' {
1041
1040
 
1042
1041
  /** BigQuery Replicator class */
1043
1042
  export class BigqueryReplicator extends BaseReplicator {
1044
- constructor(config: BigQueryReplicatorConfig);
1043
+ constructor(config: BigQueryReplicatorConfig, resources: Record<string, string | BigQueryResourceConfig | BigQueryResourceConfig[]>);
1045
1044
  }
1046
1045
 
1047
1046
  /** Postgres Replicator class */
@@ -484,7 +484,7 @@ export class Schema {
484
484
  */
485
485
  static _importAttributes(attrs) {
486
486
  if (typeof attrs === 'string') {
487
- // Tenta detectar se é um objeto serializado como string JSON
487
+ // Try to detect if it's an object serialized as JSON string
488
488
  const [ok, err, parsed] = tryFnSync(() => JSON.parse(attrs));
489
489
  if (ok && typeof parsed === 'object' && parsed !== null) {
490
490
  const [okNested, errNested, nested] = tryFnSync(() => Schema._importAttributes(parsed));
@@ -687,9 +687,9 @@ export class Schema {
687
687
  properties: this.preprocessAttributesForValidation(value),
688
688
  strict: false
689
689
  };
690
- // Se for explicitamente required, não marca como opcional
690
+ // If explicitly required, don't mark as optional
691
691
  if (isExplicitRequired) {
692
- // nada
692
+ // nothing
693
693
  } else if (isExplicitOptional || this.allNestedObjectsOptional) {
694
694
  objectConfig.optional = true;
695
695
  }