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.
- package/README.md +102 -8
- package/dist/s3db.cjs.js +36664 -15480
- package/dist/s3db.cjs.js.map +1 -1
- package/dist/s3db.d.ts +57 -0
- package/dist/s3db.es.js +36661 -15531
- package/dist/s3db.es.js.map +1 -1
- package/mcp/entrypoint.js +58 -0
- package/mcp/tools/documentation.js +434 -0
- package/mcp/tools/index.js +4 -0
- package/package.json +27 -6
- package/src/behaviors/user-managed.js +13 -6
- package/src/client.class.js +41 -46
- package/src/concerns/base62.js +85 -0
- package/src/concerns/dictionary-encoding.js +294 -0
- package/src/concerns/geo-encoding.js +256 -0
- package/src/concerns/high-performance-inserter.js +34 -30
- package/src/concerns/ip.js +325 -0
- package/src/concerns/metadata-encoding.js +345 -66
- package/src/concerns/money.js +193 -0
- package/src/concerns/partition-queue.js +7 -4
- package/src/concerns/plugin-storage.js +39 -19
- package/src/database.class.js +76 -74
- package/src/errors.js +0 -4
- package/src/plugins/api/auth/api-key-auth.js +88 -0
- package/src/plugins/api/auth/basic-auth.js +154 -0
- package/src/plugins/api/auth/index.js +112 -0
- package/src/plugins/api/auth/jwt-auth.js +169 -0
- package/src/plugins/api/index.js +539 -0
- package/src/plugins/api/middlewares/index.js +15 -0
- package/src/plugins/api/middlewares/validator.js +185 -0
- package/src/plugins/api/routes/auth-routes.js +241 -0
- package/src/plugins/api/routes/resource-routes.js +304 -0
- package/src/plugins/api/server.js +350 -0
- package/src/plugins/api/utils/error-handler.js +147 -0
- package/src/plugins/api/utils/openapi-generator.js +1240 -0
- package/src/plugins/api/utils/response-formatter.js +218 -0
- package/src/plugins/backup/streaming-exporter.js +132 -0
- package/src/plugins/backup.plugin.js +103 -50
- package/src/plugins/cache/s3-cache.class.js +95 -47
- package/src/plugins/cache.plugin.js +107 -9
- package/src/plugins/concerns/plugin-dependencies.js +313 -0
- package/src/plugins/concerns/prometheus-formatter.js +255 -0
- package/src/plugins/consumers/rabbitmq-consumer.js +4 -0
- package/src/plugins/consumers/sqs-consumer.js +4 -0
- package/src/plugins/costs.plugin.js +255 -39
- package/src/plugins/eventual-consistency/helpers.js +15 -1
- package/src/plugins/geo.plugin.js +873 -0
- package/src/plugins/importer/index.js +1020 -0
- package/src/plugins/index.js +11 -0
- package/src/plugins/metrics.plugin.js +163 -4
- package/src/plugins/queue-consumer.plugin.js +6 -27
- package/src/plugins/relation.errors.js +139 -0
- package/src/plugins/relation.plugin.js +1242 -0
- package/src/plugins/replicators/bigquery-replicator.class.js +180 -8
- package/src/plugins/replicators/dynamodb-replicator.class.js +383 -0
- package/src/plugins/replicators/index.js +28 -3
- package/src/plugins/replicators/mongodb-replicator.class.js +391 -0
- package/src/plugins/replicators/mysql-replicator.class.js +558 -0
- package/src/plugins/replicators/planetscale-replicator.class.js +409 -0
- package/src/plugins/replicators/postgres-replicator.class.js +182 -7
- package/src/plugins/replicators/s3db-replicator.class.js +1 -12
- package/src/plugins/replicators/schema-sync.helper.js +601 -0
- package/src/plugins/replicators/sqs-replicator.class.js +11 -9
- package/src/plugins/replicators/turso-replicator.class.js +416 -0
- package/src/plugins/replicators/webhook-replicator.class.js +612 -0
- package/src/plugins/state-machine.plugin.js +122 -68
- package/src/plugins/tfstate/README.md +745 -0
- package/src/plugins/tfstate/base-driver.js +80 -0
- package/src/plugins/tfstate/errors.js +112 -0
- package/src/plugins/tfstate/filesystem-driver.js +129 -0
- package/src/plugins/tfstate/index.js +2660 -0
- package/src/plugins/tfstate/s3-driver.js +192 -0
- package/src/plugins/ttl.plugin.js +536 -0
- package/src/resource.class.js +14 -10
- package/src/s3db.d.ts +57 -0
- package/src/schema.class.js +366 -32
- package/SECURITY.md +0 -76
- package/src/partition-drivers/base-partition-driver.js +0 -106
- package/src/partition-drivers/index.js +0 -66
- package/src/partition-drivers/memory-partition-driver.js +0 -289
- package/src/partition-drivers/sqs-partition-driver.js +0 -337
- 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;
|