s3db.js 11.3.2 → 12.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 (82) hide show
  1. package/README.md +102 -8
  2. package/dist/s3db.cjs.js +36664 -15480
  3. package/dist/s3db.cjs.js.map +1 -1
  4. package/dist/s3db.d.ts +57 -0
  5. package/dist/s3db.es.js +36661 -15531
  6. package/dist/s3db.es.js.map +1 -1
  7. package/mcp/entrypoint.js +58 -0
  8. package/mcp/tools/documentation.js +434 -0
  9. package/mcp/tools/index.js +4 -0
  10. package/package.json +27 -6
  11. package/src/behaviors/user-managed.js +13 -6
  12. package/src/client.class.js +41 -46
  13. package/src/concerns/base62.js +85 -0
  14. package/src/concerns/dictionary-encoding.js +294 -0
  15. package/src/concerns/geo-encoding.js +256 -0
  16. package/src/concerns/high-performance-inserter.js +34 -30
  17. package/src/concerns/ip.js +325 -0
  18. package/src/concerns/metadata-encoding.js +345 -66
  19. package/src/concerns/money.js +193 -0
  20. package/src/concerns/partition-queue.js +7 -4
  21. package/src/concerns/plugin-storage.js +39 -19
  22. package/src/database.class.js +76 -74
  23. package/src/errors.js +0 -4
  24. package/src/plugins/api/auth/api-key-auth.js +88 -0
  25. package/src/plugins/api/auth/basic-auth.js +154 -0
  26. package/src/plugins/api/auth/index.js +112 -0
  27. package/src/plugins/api/auth/jwt-auth.js +169 -0
  28. package/src/plugins/api/index.js +539 -0
  29. package/src/plugins/api/middlewares/index.js +15 -0
  30. package/src/plugins/api/middlewares/validator.js +185 -0
  31. package/src/plugins/api/routes/auth-routes.js +241 -0
  32. package/src/plugins/api/routes/resource-routes.js +304 -0
  33. package/src/plugins/api/server.js +350 -0
  34. package/src/plugins/api/utils/error-handler.js +147 -0
  35. package/src/plugins/api/utils/openapi-generator.js +1240 -0
  36. package/src/plugins/api/utils/response-formatter.js +218 -0
  37. package/src/plugins/backup/streaming-exporter.js +132 -0
  38. package/src/plugins/backup.plugin.js +103 -50
  39. package/src/plugins/cache/s3-cache.class.js +95 -47
  40. package/src/plugins/cache.plugin.js +107 -9
  41. package/src/plugins/concerns/plugin-dependencies.js +313 -0
  42. package/src/plugins/concerns/prometheus-formatter.js +255 -0
  43. package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
  44. package/src/plugins/consumers/sqs-consumer.js +4 -0
  45. package/src/plugins/costs.plugin.js +255 -39
  46. package/src/plugins/eventual-consistency/helpers.js +15 -1
  47. package/src/plugins/geo.plugin.js +873 -0
  48. package/src/plugins/importer/index.js +1020 -0
  49. package/src/plugins/index.js +11 -0
  50. package/src/plugins/metrics.plugin.js +163 -4
  51. package/src/plugins/queue-consumer.plugin.js +6 -27
  52. package/src/plugins/relation.errors.js +139 -0
  53. package/src/plugins/relation.plugin.js +1242 -0
  54. package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
  55. package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
  56. package/src/plugins/replicators/index.js +28 -3
  57. package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
  58. package/src/plugins/replicators/mysql-replicator.class.js +558 -0
  59. package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
  60. package/src/plugins/replicators/postgres-replicator.class.js +182 -7
  61. package/src/plugins/replicators/s3db-replicator.class.js +1 -12
  62. package/src/plugins/replicators/schema-sync.helper.js +601 -0
  63. package/src/plugins/replicators/sqs-replicator.class.js +11 -9
  64. package/src/plugins/replicators/turso-replicator.class.js +416 -0
  65. package/src/plugins/replicators/webhook-replicator.class.js +612 -0
  66. package/src/plugins/state-machine.plugin.js +122 -68
  67. package/src/plugins/tfstate/README.md +745 -0
  68. package/src/plugins/tfstate/base-driver.js +80 -0
  69. package/src/plugins/tfstate/errors.js +112 -0
  70. package/src/plugins/tfstate/filesystem-driver.js +129 -0
  71. package/src/plugins/tfstate/index.js +2660 -0
  72. package/src/plugins/tfstate/s3-driver.js +192 -0
  73. package/src/plugins/ttl.plugin.js +536 -0
  74. package/src/resource.class.js +14 -10
  75. package/src/s3db.d.ts +57 -0
  76. package/src/schema.class.js +366 -32
  77. package/SECURITY.md +0 -76
  78. package/src/partition-drivers/base-partition-driver.js +0 -106
  79. package/src/partition-drivers/index.js +0 -66
  80. package/src/partition-drivers/memory-partition-driver.js +0 -289
  81. package/src/partition-drivers/sqs-partition-driver.js +0 -337
  82. package/src/partition-drivers/sync-partition-driver.js +0 -38
@@ -0,0 +1,391 @@
1
+ import tryFn from "#src/concerns/try-fn.js";
2
+ import requirePluginDependency from "#src/plugins/concerns/plugin-dependencies.js";
3
+ import BaseReplicator from './base-replicator.class.js';
4
+ import { ReplicationError } from '../replicator.errors.js';
5
+
6
+ /**
7
+ * MongoDB Replicator - Replicate data to MongoDB collections
8
+ *
9
+ * ⚠️ REQUIRED DEPENDENCY: You must install the MongoDB driver:
10
+ * ```bash
11
+ * pnpm add mongodb
12
+ * ```
13
+ *
14
+ * Configuration:
15
+ * @param {string} connectionString - MongoDB connection string (optional)
16
+ * @param {string} host - Database host (default: localhost)
17
+ * @param {number} port - Database port (default: 27017)
18
+ * @param {string} database - Database name (required)
19
+ * @param {string} username - Database username (optional)
20
+ * @param {string} password - Database password (optional)
21
+ * @param {Object} options - Additional MongoDB client options (optional)
22
+ * @param {string} logCollection - Collection name for operation logging (optional)
23
+ *
24
+ * @example
25
+ * new MongoDBReplicator({
26
+ * host: 'localhost',
27
+ * port: 27017,
28
+ * database: 'analytics',
29
+ * username: 'replicator',
30
+ * password: 'secret',
31
+ * logCollection: 'replication_log'
32
+ * }, {
33
+ * users: [{ actions: ['insert', 'update'], collection: 'users_collection' }],
34
+ * orders: 'orders_collection'
35
+ * })
36
+ *
37
+ * // Connection string example
38
+ * new MongoDBReplicator({
39
+ * connectionString: 'mongodb://user:pass@localhost:27017/analytics',
40
+ * logCollection: 'replication_log'
41
+ * }, {
42
+ * users: 'users_collection'
43
+ * })
44
+ *
45
+ * See PLUGINS.md for comprehensive configuration documentation.
46
+ */
47
+ class MongoDBReplicator extends BaseReplicator {
48
+ constructor(config = {}, resources = {}) {
49
+ super(config);
50
+ this.connectionString = config.connectionString;
51
+ this.host = config.host || 'localhost';
52
+ this.port = config.port || 27017;
53
+ this.database = config.database;
54
+ this.username = config.username;
55
+ this.password = config.password;
56
+ this.options = config.options || {};
57
+ this.client = null;
58
+ this.db = null;
59
+ this.logCollection = config.logCollection;
60
+
61
+ // Parse resources configuration
62
+ this.resources = this.parseResourcesConfig(resources);
63
+ }
64
+
65
+ parseResourcesConfig(resources) {
66
+ const parsed = {};
67
+
68
+ for (const [resourceName, config] of Object.entries(resources)) {
69
+ if (typeof config === 'string') {
70
+ // Short form: just collection name
71
+ parsed[resourceName] = [{
72
+ collection: config,
73
+ actions: ['insert']
74
+ }];
75
+ } else if (Array.isArray(config)) {
76
+ // Array form: multiple collection mappings
77
+ parsed[resourceName] = config.map(item => {
78
+ if (typeof item === 'string') {
79
+ return { collection: item, actions: ['insert'] };
80
+ }
81
+ return {
82
+ collection: item.collection,
83
+ actions: item.actions || ['insert']
84
+ };
85
+ });
86
+ } else if (typeof config === 'object') {
87
+ // Single object form
88
+ parsed[resourceName] = [{
89
+ collection: config.collection,
90
+ actions: config.actions || ['insert']
91
+ }];
92
+ }
93
+ }
94
+
95
+ return parsed;
96
+ }
97
+
98
+ validateConfig() {
99
+ const errors = [];
100
+ if (!this.connectionString && !this.database) {
101
+ errors.push('Database name or connection string is required');
102
+ }
103
+ if (Object.keys(this.resources).length === 0) {
104
+ errors.push('At least one resource must be configured');
105
+ }
106
+
107
+ // Validate resource configurations
108
+ for (const [resourceName, collections] of Object.entries(this.resources)) {
109
+ for (const collectionConfig of collections) {
110
+ if (!collectionConfig.collection) {
111
+ errors.push(`Collection name is required for resource '${resourceName}'`);
112
+ }
113
+ if (!Array.isArray(collectionConfig.actions) || collectionConfig.actions.length === 0) {
114
+ errors.push(`Actions array is required for resource '${resourceName}'`);
115
+ }
116
+ }
117
+ }
118
+
119
+ return { isValid: errors.length === 0, errors };
120
+ }
121
+
122
+ async initialize(database) {
123
+ await super.initialize(database);
124
+
125
+ // Load mongodb dependency
126
+ const { MongoClient } = requirePluginDependency('mongodb', 'MongoDBReplicator');
127
+
128
+ // Create connection
129
+ const [ok, err] = await tryFn(async () => {
130
+ let uri;
131
+ if (this.connectionString) {
132
+ uri = this.connectionString;
133
+ } else {
134
+ const auth = this.username && this.password
135
+ ? `${encodeURIComponent(this.username)}:${encodeURIComponent(this.password)}@`
136
+ : '';
137
+ uri = `mongodb://${auth}${this.host}:${this.port}/${this.database}`;
138
+ }
139
+
140
+ this.client = new MongoClient(uri, {
141
+ ...this.options,
142
+ useUnifiedTopology: true,
143
+ useNewUrlParser: true
144
+ });
145
+
146
+ await this.client.connect();
147
+ this.db = this.client.db(this.database);
148
+
149
+ // Test connection
150
+ await this.db.admin().ping();
151
+ });
152
+
153
+ if (!ok) {
154
+ throw new ReplicationError('Failed to connect to MongoDB database', {
155
+ operation: 'initialize',
156
+ replicatorClass: 'MongoDBReplicator',
157
+ host: this.host,
158
+ port: this.port,
159
+ database: this.database,
160
+ original: err,
161
+ suggestion: 'Check MongoDB connection credentials and ensure database is accessible'
162
+ });
163
+ }
164
+
165
+ // Create log collection if configured
166
+ if (this.logCollection) {
167
+ await this._createLogCollection();
168
+ }
169
+
170
+ this.emit('connected', {
171
+ replicator: 'MongoDBReplicator',
172
+ host: this.host,
173
+ database: this.database
174
+ });
175
+ }
176
+
177
+ async _createLogCollection() {
178
+ const [ok] = await tryFn(async () => {
179
+ const collections = await this.db.listCollections({ name: this.logCollection }).toArray();
180
+
181
+ if (collections.length === 0) {
182
+ await this.db.createCollection(this.logCollection);
183
+
184
+ // Create indexes for better query performance
185
+ await this.db.collection(this.logCollection).createIndexes([
186
+ { key: { resource_name: 1 } },
187
+ { key: { timestamp: 1 } }
188
+ ]);
189
+ }
190
+ });
191
+
192
+ if (!ok && this.config.verbose) {
193
+ console.warn('[MongoDBReplicator] Failed to create log collection');
194
+ }
195
+ }
196
+
197
+ async replicate(resourceName, operation, data, id) {
198
+ if (!this.resources[resourceName]) {
199
+ throw new ReplicationError('Resource not configured for replication', {
200
+ operation: 'replicate',
201
+ replicatorClass: 'MongoDBReplicator',
202
+ resourceName,
203
+ configuredResources: Object.keys(this.resources),
204
+ suggestion: 'Add resource to replicator resources configuration'
205
+ });
206
+ }
207
+
208
+ const results = [];
209
+
210
+ for (const collectionConfig of this.resources[resourceName]) {
211
+ if (!collectionConfig.actions.includes(operation)) {
212
+ continue; // Skip if operation not allowed for this collection
213
+ }
214
+
215
+ const [ok, error, result] = await tryFn(async () => {
216
+ switch (operation) {
217
+ case 'insert':
218
+ return await this._insertDocument(collectionConfig.collection, data);
219
+ case 'update':
220
+ return await this._updateDocument(collectionConfig.collection, id, data);
221
+ case 'delete':
222
+ return await this._deleteDocument(collectionConfig.collection, id);
223
+ default:
224
+ throw new ReplicationError(`Unsupported operation: ${operation}`, {
225
+ operation: 'replicate',
226
+ replicatorClass: 'MongoDBReplicator',
227
+ invalidOperation: operation,
228
+ supportedOperations: ['insert', 'update', 'delete']
229
+ });
230
+ }
231
+ });
232
+
233
+ if (ok) {
234
+ results.push(result);
235
+
236
+ // Log to replication log collection if configured
237
+ if (this.logCollection) {
238
+ await this._logOperation(resourceName, operation, id, data);
239
+ }
240
+ } else {
241
+ this.emit('replication_error', {
242
+ resource: resourceName,
243
+ operation,
244
+ collection: collectionConfig.collection,
245
+ error: error.message
246
+ });
247
+
248
+ if (this.config.verbose) {
249
+ console.error(`[MongoDBReplicator] Failed to replicate ${operation} for ${resourceName}:`, error);
250
+ }
251
+ }
252
+ }
253
+
254
+ return results.length > 0 ? results[0] : null;
255
+ }
256
+
257
+ async _insertDocument(collectionName, data) {
258
+ const cleanData = this._cleanInternalFields(data);
259
+ const collection = this.db.collection(collectionName);
260
+
261
+ const result = await collection.insertOne(cleanData);
262
+ return result;
263
+ }
264
+
265
+ async _updateDocument(collectionName, id, data) {
266
+ const cleanData = this._cleanInternalFields(data);
267
+ const collection = this.db.collection(collectionName);
268
+
269
+ // Remove _id from update data if present
270
+ delete cleanData._id;
271
+
272
+ const result = await collection.updateOne(
273
+ { _id: id },
274
+ { $set: cleanData }
275
+ );
276
+
277
+ return result;
278
+ }
279
+
280
+ async _deleteDocument(collectionName, id) {
281
+ const collection = this.db.collection(collectionName);
282
+ const result = await collection.deleteOne({ _id: id });
283
+ return result;
284
+ }
285
+
286
+ async _logOperation(resourceName, operation, id, data) {
287
+ const [ok] = await tryFn(async () => {
288
+ const collection = this.db.collection(this.logCollection);
289
+ await collection.insertOne({
290
+ resource_name: resourceName,
291
+ operation,
292
+ record_id: id,
293
+ data,
294
+ timestamp: new Date()
295
+ });
296
+ });
297
+
298
+ if (!ok && this.config.verbose) {
299
+ console.warn('[MongoDBReplicator] Failed to log operation');
300
+ }
301
+ }
302
+
303
+ shouldReplicateResource(resourceName) {
304
+ return this.resources.hasOwnProperty(resourceName);
305
+ }
306
+
307
+ _cleanInternalFields(data) {
308
+ if (!data || typeof data !== 'object') return data;
309
+
310
+ const cleanData = { ...data };
311
+
312
+ // Remove internal s3db fields
313
+ // Preserve _id as it's the MongoDB primary key
314
+ Object.keys(cleanData).forEach(key => {
315
+ if (key === '_id') {
316
+ return; // Keep _id field for MongoDB
317
+ }
318
+ if (key.startsWith('$') || key.startsWith('_')) {
319
+ delete cleanData[key];
320
+ }
321
+ });
322
+
323
+ return cleanData;
324
+ }
325
+
326
+ async replicateBatch(resourceName, records) {
327
+ const results = [];
328
+ const errors = [];
329
+
330
+ // MongoDB supports bulk operations, but for consistency with other replicators
331
+ // and error handling, we process sequentially
332
+ for (const record of records) {
333
+ const [ok, err, result] = await tryFn(() =>
334
+ this.replicate(resourceName, record.operation, record.data, record.id)
335
+ );
336
+
337
+ if (ok) {
338
+ results.push(result);
339
+ } else {
340
+ errors.push({ id: record.id, error: err.message });
341
+ }
342
+ }
343
+
344
+ return {
345
+ success: errors.length === 0,
346
+ results,
347
+ errors,
348
+ total: records.length
349
+ };
350
+ }
351
+
352
+ async testConnection() {
353
+ const [ok, err] = await tryFn(async () => {
354
+ if (!this.client) {
355
+ throw new Error('Client not initialized');
356
+ }
357
+
358
+ await this.db.admin().ping();
359
+ return true;
360
+ });
361
+
362
+ if (!ok) {
363
+ this.emit('connection_error', { replicator: 'MongoDBReplicator', error: err.message });
364
+ return false;
365
+ }
366
+
367
+ return true;
368
+ }
369
+
370
+ async getStatus() {
371
+ const baseStatus = await super.getStatus();
372
+ return {
373
+ ...baseStatus,
374
+ connected: !!this.client && !!this.db,
375
+ host: this.host,
376
+ database: this.database,
377
+ resources: Object.keys(this.resources)
378
+ };
379
+ }
380
+
381
+ async cleanup() {
382
+ if (this.client) {
383
+ await this.client.close();
384
+ this.client = null;
385
+ this.db = null;
386
+ }
387
+ await super.cleanup();
388
+ }
389
+ }
390
+
391
+ export default MongoDBReplicator;