s3db.js 6.2.0 → 7.0.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 +2724 -0
- package/README.md +372 -469
- package/UNLICENSE +24 -0
- package/dist/s3db.cjs.js +12105 -19396
- package/dist/s3db.cjs.min.js +1 -1
- package/dist/s3db.d.ts +373 -72
- package/dist/s3db.es.js +12090 -19393
- package/dist/s3db.es.min.js +1 -1
- package/dist/s3db.iife.js +12103 -19398
- package/dist/s3db.iife.min.js +1 -1
- package/package.json +44 -38
- package/src/behaviors/body-only.js +110 -0
- package/src/behaviors/body-overflow.js +153 -0
- package/src/behaviors/enforce-limits.js +195 -0
- package/src/behaviors/index.js +39 -0
- package/src/behaviors/truncate-data.js +204 -0
- package/src/behaviors/user-managed.js +147 -0
- package/src/client.class.js +515 -0
- package/src/concerns/base62.js +61 -0
- package/src/concerns/calculator.js +204 -0
- package/src/concerns/crypto.js +159 -0
- package/src/concerns/id.js +8 -0
- package/src/concerns/index.js +5 -0
- package/src/concerns/try-fn.js +151 -0
- package/src/connection-string.class.js +75 -0
- package/src/database.class.js +599 -0
- package/src/errors.js +261 -0
- package/src/index.js +17 -0
- package/src/plugins/audit.plugin.js +442 -0
- package/src/plugins/cache/cache.class.js +53 -0
- package/src/plugins/cache/index.js +6 -0
- package/src/plugins/cache/memory-cache.class.js +164 -0
- package/src/plugins/cache/s3-cache.class.js +189 -0
- package/src/plugins/cache.plugin.js +275 -0
- package/src/plugins/consumers/index.js +24 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +56 -0
- package/src/plugins/consumers/sqs-consumer.js +102 -0
- package/src/plugins/costs.plugin.js +81 -0
- package/src/plugins/fulltext.plugin.js +473 -0
- package/src/plugins/index.js +12 -0
- package/src/plugins/metrics.plugin.js +603 -0
- package/src/plugins/plugin.class.js +210 -0
- package/src/plugins/plugin.obj.js +13 -0
- package/src/plugins/queue-consumer.plugin.js +134 -0
- package/src/plugins/replicator.plugin.js +769 -0
- package/src/plugins/replicators/base-replicator.class.js +85 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +328 -0
- package/src/plugins/replicators/index.js +44 -0
- package/src/plugins/replicators/postgres-replicator.class.js +427 -0
- package/src/plugins/replicators/s3db-replicator.class.js +352 -0
- package/src/plugins/replicators/sqs-replicator.class.js +427 -0
- package/src/resource.class.js +2626 -0
- package/src/s3db.d.ts +1263 -0
- package/src/schema.class.js +706 -0
- package/src/stream/index.js +16 -0
- package/src/stream/resource-ids-page-reader.class.js +10 -0
- package/src/stream/resource-ids-reader.class.js +63 -0
- package/src/stream/resource-reader.class.js +81 -0
- package/src/stream/resource-writer.class.js +92 -0
- package/src/validator.class.js +97 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S3DB Replicator Configuration Documentation
|
|
3
|
+
*
|
|
4
|
+
* This replicator supports highly flexible resource mapping and transformer configuration. You can specify the resources to replicate using any of the following syntaxes:
|
|
5
|
+
*
|
|
6
|
+
* 1. Array of resource names (replicate resource to itself):
|
|
7
|
+
* resources: ['users']
|
|
8
|
+
* // Replicates 'users' to 'users' in the destination
|
|
9
|
+
*
|
|
10
|
+
* 2. Map: source resource → destination resource name:
|
|
11
|
+
* resources: { users: 'people' }
|
|
12
|
+
* // Replicates 'users' to 'people' in the destination
|
|
13
|
+
*
|
|
14
|
+
* 3. Map: source resource → array of destination resource names and/or transformers:
|
|
15
|
+
* resources: { users: ['people', (el) => ({ ...el, fullName: el.name })] }
|
|
16
|
+
* // Replicates 'users' to 'people' and also applies the transformer
|
|
17
|
+
*
|
|
18
|
+
* 4. Map: source resource → object with resource and transformer:
|
|
19
|
+
* resources: { users: { resource: 'people', transformer: (el) => ({ ...el, fullName: el.name }) } }
|
|
20
|
+
* // Replicates 'users' to 'people' with a custom transformer
|
|
21
|
+
*
|
|
22
|
+
* 5. Map: source resource → array of objects with resource and transformer (multi-destination):
|
|
23
|
+
* resources: { users: [ { resource: 'people', transformer: (el) => ({ ...el, fullName: el.name }) } ] }
|
|
24
|
+
* // Replicates 'users' to multiple destinations, each with its own transformer
|
|
25
|
+
*
|
|
26
|
+
* 6. Map: source resource → function (rare, but supported):
|
|
27
|
+
* resources: { users: (el) => ... }
|
|
28
|
+
* // Replicates 'users' to 'users' with a custom transformer
|
|
29
|
+
*
|
|
30
|
+
* All forms can be mixed and matched for different resources. The transformer is always available (default: identity function).
|
|
31
|
+
*
|
|
32
|
+
* Example:
|
|
33
|
+
* resources: {
|
|
34
|
+
* users: [
|
|
35
|
+
* 'people',
|
|
36
|
+
* { resource: 'people', transformer: (el) => ({ ...el, fullName: el.name }) },
|
|
37
|
+
* (el) => ({ ...el, fullName: el.name })
|
|
38
|
+
* ],
|
|
39
|
+
* orders: 'orders_copy',
|
|
40
|
+
* products: { resource: 'products_copy' }
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* The replicator always uses the provided client as the destination.
|
|
44
|
+
*
|
|
45
|
+
* See tests/examples for all supported syntaxes.
|
|
46
|
+
*/
|
|
47
|
+
import BaseReplicator from './base-replicator.class.js';
|
|
48
|
+
import { S3db } from '../../database.class.js';
|
|
49
|
+
import tryFn from "../../concerns/try-fn.js";
|
|
50
|
+
|
|
51
|
+
function normalizeResourceName(name) {
|
|
52
|
+
return typeof name === 'string' ? name.trim().toLowerCase() : name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* S3DB Replicator - Replicates data to another s3db instance
|
|
57
|
+
*/
|
|
58
|
+
class S3dbReplicator extends BaseReplicator {
|
|
59
|
+
constructor(config = {}, resources = [], client = null) {
|
|
60
|
+
super(config);
|
|
61
|
+
this.instanceId = Math.random().toString(36).slice(2, 10);
|
|
62
|
+
this.client = client;
|
|
63
|
+
this.connectionString = config.connectionString;
|
|
64
|
+
// Robustness: ensure object
|
|
65
|
+
let normalizedResources = resources;
|
|
66
|
+
if (!resources) normalizedResources = {};
|
|
67
|
+
else if (Array.isArray(resources)) {
|
|
68
|
+
normalizedResources = {};
|
|
69
|
+
for (const res of resources) {
|
|
70
|
+
if (typeof res === 'string') normalizedResources[normalizeResourceName(res)] = res;
|
|
71
|
+
}
|
|
72
|
+
} else if (typeof resources === 'string') {
|
|
73
|
+
normalizedResources[normalizeResourceName(resources)] = resources;
|
|
74
|
+
}
|
|
75
|
+
this.resourcesMap = this._normalizeResources(normalizedResources);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_normalizeResources(resources) {
|
|
79
|
+
// Suporta array, objeto, função, string
|
|
80
|
+
if (!resources) return {};
|
|
81
|
+
if (Array.isArray(resources)) {
|
|
82
|
+
const map = {};
|
|
83
|
+
for (const res of resources) {
|
|
84
|
+
if (typeof res === 'string') map[normalizeResourceName(res)] = res;
|
|
85
|
+
else if (Array.isArray(res) && typeof res[0] === 'string') map[normalizeResourceName(res[0])] = res;
|
|
86
|
+
else if (typeof res === 'object' && res.resource) {
|
|
87
|
+
// Array of objects with resource/action/transformer
|
|
88
|
+
map[normalizeResourceName(res.resource)] = { ...res };
|
|
89
|
+
}
|
|
90
|
+
// Do NOT set actions: ['insert'] or any default actions here
|
|
91
|
+
}
|
|
92
|
+
return map;
|
|
93
|
+
}
|
|
94
|
+
if (typeof resources === 'object') {
|
|
95
|
+
const map = {};
|
|
96
|
+
for (const [src, dest] of Object.entries(resources)) {
|
|
97
|
+
const normSrc = normalizeResourceName(src);
|
|
98
|
+
if (typeof dest === 'string') map[normSrc] = dest;
|
|
99
|
+
else if (Array.isArray(dest)) {
|
|
100
|
+
// Array of destinations/objects/transformers
|
|
101
|
+
map[normSrc] = dest.map(item => {
|
|
102
|
+
if (typeof item === 'string') return item;
|
|
103
|
+
if (typeof item === 'function') return item;
|
|
104
|
+
if (typeof item === 'object' && item.resource) {
|
|
105
|
+
// Copy all fields (resource, transformer, actions, etc.)
|
|
106
|
+
return { ...item };
|
|
107
|
+
}
|
|
108
|
+
return item;
|
|
109
|
+
});
|
|
110
|
+
} else if (typeof dest === 'function') map[normSrc] = dest;
|
|
111
|
+
else if (typeof dest === 'object' && dest.resource) {
|
|
112
|
+
// Copy all fields (resource, transformer, actions, etc.)
|
|
113
|
+
map[normSrc] = { ...dest };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return map;
|
|
117
|
+
}
|
|
118
|
+
if (typeof resources === 'function') {
|
|
119
|
+
return resources;
|
|
120
|
+
}
|
|
121
|
+
if (typeof resources === 'string') {
|
|
122
|
+
const map = { [normalizeResourceName(resources)]: resources };
|
|
123
|
+
return map;
|
|
124
|
+
}
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
validateConfig() {
|
|
129
|
+
const errors = [];
|
|
130
|
+
// Accept both arrays and objects for resources
|
|
131
|
+
if (!this.client && !this.connectionString) {
|
|
132
|
+
errors.push('You must provide a client or a connectionString');
|
|
133
|
+
}
|
|
134
|
+
if (!this.resourcesMap || (typeof this.resourcesMap === 'object' && Object.keys(this.resourcesMap).length === 0)) {
|
|
135
|
+
errors.push('You must provide a resources map or array');
|
|
136
|
+
}
|
|
137
|
+
return { isValid: errors.length === 0, errors };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async initialize(database) {
|
|
141
|
+
try {
|
|
142
|
+
await super.initialize(database);
|
|
143
|
+
if (this.client) {
|
|
144
|
+
this.targetDatabase = this.client;
|
|
145
|
+
} else if (this.connectionString) {
|
|
146
|
+
const targetConfig = {
|
|
147
|
+
connectionString: this.connectionString,
|
|
148
|
+
region: this.region,
|
|
149
|
+
keyPrefix: this.keyPrefix,
|
|
150
|
+
verbose: this.config.verbose || false
|
|
151
|
+
};
|
|
152
|
+
this.targetDatabase = new S3db(targetConfig);
|
|
153
|
+
await this.targetDatabase.connect();
|
|
154
|
+
} else {
|
|
155
|
+
throw new Error('S3dbReplicator: No client or connectionString provided');
|
|
156
|
+
}
|
|
157
|
+
this.emit('connected', {
|
|
158
|
+
replicator: this.name,
|
|
159
|
+
target: this.connectionString || 'client-provided'
|
|
160
|
+
});
|
|
161
|
+
} catch (err) {
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Change signature to accept id
|
|
167
|
+
async replicate({ resource, operation, data, id: explicitId }) {
|
|
168
|
+
const normResource = normalizeResourceName(resource);
|
|
169
|
+
const destResource = this._resolveDestResource(normResource, data);
|
|
170
|
+
const destResourceObj = this._getDestResourceObj(destResource);
|
|
171
|
+
|
|
172
|
+
// Apply transformer before replicating
|
|
173
|
+
const transformedData = this._applyTransformer(normResource, data);
|
|
174
|
+
|
|
175
|
+
let result;
|
|
176
|
+
if (operation === 'insert') {
|
|
177
|
+
result = await destResourceObj.insert(transformedData);
|
|
178
|
+
} else if (operation === 'update') {
|
|
179
|
+
result = await destResourceObj.update(explicitId, transformedData);
|
|
180
|
+
} else if (operation === 'delete') {
|
|
181
|
+
result = await destResourceObj.delete(explicitId);
|
|
182
|
+
} else {
|
|
183
|
+
throw new Error(`Invalid operation: ${operation}. Supported operations are: insert, update, delete`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_applyTransformer(resource, data) {
|
|
190
|
+
const normResource = normalizeResourceName(resource);
|
|
191
|
+
const entry = this.resourcesMap[normResource];
|
|
192
|
+
let result;
|
|
193
|
+
if (!entry) return data;
|
|
194
|
+
// Array: [resource, transformer]
|
|
195
|
+
if (Array.isArray(entry) && typeof entry[1] === 'function') {
|
|
196
|
+
result = entry[1](data);
|
|
197
|
+
} else if (typeof entry === 'function') {
|
|
198
|
+
result = entry(data);
|
|
199
|
+
} else if (typeof entry === 'object') {
|
|
200
|
+
if (typeof entry.transform === 'function') result = entry.transform(data);
|
|
201
|
+
else if (typeof entry.transformer === 'function') result = entry.transformer(data);
|
|
202
|
+
} else {
|
|
203
|
+
result = data;
|
|
204
|
+
}
|
|
205
|
+
// Garante que id sempre está presente
|
|
206
|
+
if (result && data && data.id && !result.id) result.id = data.id;
|
|
207
|
+
// Fallback: if transformer returns undefined/null, use original data
|
|
208
|
+
if (!result && data) result = data;
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
_resolveDestResource(resource, data) {
|
|
213
|
+
const normResource = normalizeResourceName(resource);
|
|
214
|
+
const entry = this.resourcesMap[normResource];
|
|
215
|
+
if (!entry) return resource;
|
|
216
|
+
// Array: [resource, transformer]
|
|
217
|
+
if (Array.isArray(entry)) {
|
|
218
|
+
if (typeof entry[0] === 'string') return entry[0];
|
|
219
|
+
if (typeof entry[0] === 'object' && entry[0].resource) return entry[0].resource;
|
|
220
|
+
if (typeof entry[0] === 'function') return resource; // fallback
|
|
221
|
+
}
|
|
222
|
+
// String mapping
|
|
223
|
+
if (typeof entry === 'string') return entry;
|
|
224
|
+
// Função mapping
|
|
225
|
+
if (typeof entry === 'function') return resource;
|
|
226
|
+
// Objeto: { resource, transform }
|
|
227
|
+
if (typeof entry === 'object' && entry.resource) return entry.resource;
|
|
228
|
+
return resource;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
_getDestResourceObj(resource) {
|
|
232
|
+
if (!this.client || !this.client.resources) return null;
|
|
233
|
+
const available = Object.keys(this.client.resources);
|
|
234
|
+
const norm = normalizeResourceName(resource);
|
|
235
|
+
const found = available.find(r => normalizeResourceName(r) === norm);
|
|
236
|
+
if (!found) {
|
|
237
|
+
throw new Error(`[S3dbReplicator] Destination resource not found: ${resource}. Available: ${available.join(', ')}`);
|
|
238
|
+
}
|
|
239
|
+
return this.client.resources[found];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async replicateBatch(resourceName, records) {
|
|
243
|
+
if (!this.enabled || !this.shouldReplicateResource(resourceName)) {
|
|
244
|
+
return { skipped: true, reason: 'resource_not_included' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const results = [];
|
|
248
|
+
const errors = [];
|
|
249
|
+
|
|
250
|
+
for (const record of records) {
|
|
251
|
+
const [ok, err, result] = await tryFn(() => this.replicate({
|
|
252
|
+
resource: resourceName,
|
|
253
|
+
operation: record.operation,
|
|
254
|
+
id: record.id,
|
|
255
|
+
data: record.data,
|
|
256
|
+
beforeData: record.beforeData
|
|
257
|
+
}));
|
|
258
|
+
if (ok) results.push(result);
|
|
259
|
+
else errors.push({ id: record.id, error: err.message });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.emit('batch_replicated', {
|
|
263
|
+
replicator: this.name,
|
|
264
|
+
resourceName,
|
|
265
|
+
total: records.length,
|
|
266
|
+
successful: results.length,
|
|
267
|
+
errors: errors.length
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
success: errors.length === 0,
|
|
272
|
+
results,
|
|
273
|
+
errors,
|
|
274
|
+
total: records.length
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async testConnection() {
|
|
279
|
+
const [ok, err] = await tryFn(async () => {
|
|
280
|
+
if (!this.targetDatabase) {
|
|
281
|
+
await this.initialize(this.database);
|
|
282
|
+
}
|
|
283
|
+
// Try to list resources to test connection
|
|
284
|
+
await this.targetDatabase.listResources();
|
|
285
|
+
return true;
|
|
286
|
+
});
|
|
287
|
+
if (ok) return true;
|
|
288
|
+
this.emit('connection_error', {
|
|
289
|
+
replicator: this.name,
|
|
290
|
+
error: err.message
|
|
291
|
+
});
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async getStatus() {
|
|
296
|
+
const baseStatus = await super.getStatus();
|
|
297
|
+
return {
|
|
298
|
+
...baseStatus,
|
|
299
|
+
connected: !!this.targetDatabase,
|
|
300
|
+
targetDatabase: this.connectionString || 'client-provided',
|
|
301
|
+
resources: Object.keys(this.resourcesMap || {}),
|
|
302
|
+
totalreplicators: this.listenerCount('replicated'),
|
|
303
|
+
totalErrors: this.listenerCount('replicator_error')
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async cleanup() {
|
|
308
|
+
if (this.targetDatabase) {
|
|
309
|
+
// Close target database connection
|
|
310
|
+
this.targetDatabase.removeAllListeners();
|
|
311
|
+
}
|
|
312
|
+
await super.cleanup();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
shouldReplicateResource(resource, action) {
|
|
316
|
+
const normResource = normalizeResourceName(resource);
|
|
317
|
+
const entry = this.resourcesMap[normResource];
|
|
318
|
+
if (!entry) return false;
|
|
319
|
+
|
|
320
|
+
// If no action is specified, just check if resource is configured
|
|
321
|
+
if (!action) return true;
|
|
322
|
+
|
|
323
|
+
// Suporte a todos os estilos de configuração
|
|
324
|
+
// Se for array de objetos, checar actions
|
|
325
|
+
if (Array.isArray(entry)) {
|
|
326
|
+
for (const item of entry) {
|
|
327
|
+
if (typeof item === 'object' && item.resource) {
|
|
328
|
+
if (item.actions && Array.isArray(item.actions)) {
|
|
329
|
+
if (item.actions.includes(action)) return true;
|
|
330
|
+
} else {
|
|
331
|
+
return true; // Se não há actions, aceita todas
|
|
332
|
+
}
|
|
333
|
+
} else if (typeof item === 'string' || typeof item === 'function') {
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
if (typeof entry === 'object' && entry.resource) {
|
|
340
|
+
if (entry.actions && Array.isArray(entry.actions)) {
|
|
341
|
+
return entry.actions.includes(action);
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (typeof entry === 'string' || typeof entry === 'function') {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export default S3dbReplicator;
|