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.
- package/PLUGINS.md +200 -10
- package/dist/s3db.cjs.js +149 -1458
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +14 -15
- package/dist/s3db.es.js +150 -1459
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +149 -1458
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +19 -8
- package/src/behaviors/body-only.js +2 -2
- package/src/behaviors/truncate-data.js +2 -2
- package/src/client.class.js +1 -1
- package/src/database.class.js +1 -1
- package/src/errors.js +1 -1
- package/src/plugins/audit.plugin.js +5 -5
- package/src/plugins/cache/filesystem-cache.class.js +661 -0
- package/src/plugins/cache/index.js +4 -0
- package/src/plugins/cache/partition-aware-filesystem-cache.class.js +480 -0
- package/src/plugins/cache.plugin.js +159 -9
- package/src/plugins/consumers/index.js +3 -3
- package/src/plugins/consumers/sqs-consumer.js +2 -2
- package/src/plugins/fulltext.plugin.js +5 -5
- package/src/plugins/metrics.plugin.js +2 -2
- package/src/plugins/queue-consumer.plugin.js +3 -3
- package/src/plugins/replicator.plugin.js +259 -362
- package/src/plugins/replicators/bigquery-replicator.class.js +102 -68
- package/src/plugins/replicators/postgres-replicator.class.js +19 -109
- package/src/plugins/replicators/s3db-replicator.class.js +56 -68
- package/src/plugins/replicators/sqs-replicator.class.js +34 -121
- package/src/resource.class.js +14 -14
- package/src/s3db.d.ts +14 -15
- package/src/schema.class.js +3 -3
|
@@ -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
|
|
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
|
-
*
|
|
19
|
-
* @
|
|
20
|
-
* @
|
|
21
|
-
* @
|
|
22
|
-
* @
|
|
23
|
-
* @
|
|
24
|
-
* @
|
|
25
|
-
*
|
|
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
|
-
*
|
|
82
|
-
* {
|
|
22
|
+
* new SqsReplicator({
|
|
83
23
|
* region: 'us-east-1',
|
|
84
|
-
*
|
|
85
|
-
* }
|
|
24
|
+
* queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789012/events-queue'
|
|
25
|
+
* }, ['users', 'orders'])
|
|
86
26
|
*
|
|
87
|
-
*
|
|
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
|
-
//
|
|
142
|
-
if (resources
|
|
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
|
};
|
package/src/resource.class.js
CHANGED
|
@@ -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
|
-
//
|
|
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]
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
2310
|
-
*
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
424
|
-
|
|
422
|
+
location?: string;
|
|
423
|
+
logTable?: string;
|
|
425
424
|
batchSize?: number;
|
|
426
425
|
maxRetries?: number;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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 */
|
package/src/schema.class.js
CHANGED
|
@@ -484,7 +484,7 @@ export class Schema {
|
|
|
484
484
|
*/
|
|
485
485
|
static _importAttributes(attrs) {
|
|
486
486
|
if (typeof attrs === 'string') {
|
|
487
|
-
//
|
|
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
|
-
//
|
|
690
|
+
// If explicitly required, don't mark as optional
|
|
691
691
|
if (isExplicitRequired) {
|
|
692
|
-
//
|
|
692
|
+
// nothing
|
|
693
693
|
} else if (isExplicitOptional || this.allNestedObjectsOptional) {
|
|
694
694
|
objectConfig.optional = true;
|
|
695
695
|
}
|