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.
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 +12105 -19396
  5. package/dist/s3db.cjs.min.js +1 -1
  6. package/dist/s3db.d.ts +373 -72
  7. package/dist/s3db.es.js +12090 -19393
  8. package/dist/s3db.es.min.js +1 -1
  9. package/dist/s3db.iife.js +12103 -19398
  10. package/dist/s3db.iife.min.js +1 -1
  11. package/package.json +44 -38
  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 +159 -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,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;