s3db.js 8.1.1 → 8.2.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/bin/cli.js ADDED
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from 'commander';
4
+ import { config } from 'dotenv';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+ import { readFileSync, existsSync } from 'fs';
8
+ import { homedir } from 'os';
9
+
10
+ // Load environment variables
11
+ config();
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ const packageJsonPath = join(__dirname, '..', 'package.json');
16
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
17
+
18
+ // Colors for console output
19
+ const colors = {
20
+ red: '\x1b[31m',
21
+ green: '\x1b[32m',
22
+ yellow: '\x1b[33m',
23
+ blue: '\x1b[34m',
24
+ magenta: '\x1b[35m',
25
+ cyan: '\x1b[36m',
26
+ white: '\x1b[37m',
27
+ reset: '\x1b[0m',
28
+ bright: '\x1b[1m'
29
+ };
30
+
31
+ // Helper functions
32
+ function log(message, color = colors.white) {
33
+ console.log(`${color}${message}${colors.reset}`);
34
+ }
35
+
36
+ function error(message) {
37
+ log(`❌ ${message}`, colors.red);
38
+ }
39
+
40
+ function success(message) {
41
+ log(`✅ ${message}`, colors.green);
42
+ }
43
+
44
+ function info(message) {
45
+ log(`ℹ️ ${message}`, colors.blue);
46
+ }
47
+
48
+ function warn(message) {
49
+ log(`⚠️ ${message}`, colors.yellow);
50
+ }
51
+
52
+ // Auto-detect connection string from various sources
53
+ function detectConnectionString() {
54
+ // Priority order for connection string detection
55
+ const sources = [
56
+ // 1. Environment variable
57
+ () => process.env.S3DB_CONNECTION_STRING,
58
+ () => process.env.S3_CONNECTION_STRING,
59
+ () => process.env.DATABASE_URL,
60
+
61
+ // 2. AWS credentials from environment
62
+ () => {
63
+ const key = process.env.AWS_ACCESS_KEY_ID;
64
+ const secret = process.env.AWS_SECRET_ACCESS_KEY;
65
+ const bucket = process.env.AWS_S3_BUCKET || process.env.S3_BUCKET;
66
+ const region = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1';
67
+
68
+ if (key && secret && bucket) {
69
+ return `s3://${key}:${secret}@${bucket}?region=${region}`;
70
+ }
71
+ return null;
72
+ },
73
+
74
+ // 3. MCP config file
75
+ () => {
76
+ const mcpConfigPath = join(homedir(), '.config', 'mcp', 'config.json');
77
+ if (existsSync(mcpConfigPath)) {
78
+ try {
79
+ const mcpConfig = JSON.parse(readFileSync(mcpConfigPath, 'utf-8'));
80
+ const s3dbConfig = mcpConfig.servers?.s3db;
81
+ if (s3dbConfig?.env?.S3DB_CONNECTION_STRING) {
82
+ return s3dbConfig.env.S3DB_CONNECTION_STRING;
83
+ }
84
+ } catch (e) {
85
+ // Ignore config parsing errors
86
+ }
87
+ }
88
+ return null;
89
+ },
90
+
91
+ // 4. Local .env file
92
+ () => {
93
+ const envPath = join(process.cwd(), '.env');
94
+ if (existsSync(envPath)) {
95
+ const envContent = readFileSync(envPath, 'utf-8');
96
+ const match = envContent.match(/^S3DB_CONNECTION_STRING=(.*)$/m);
97
+ if (match && match[1]) {
98
+ return match[1].trim().replace(/^["']|["']$/g, ''); // Remove quotes
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ ];
104
+
105
+ for (const source of sources) {
106
+ const connectionString = source();
107
+ if (connectionString) {
108
+ return connectionString;
109
+ }
110
+ }
111
+
112
+ return null;
113
+ }
114
+
115
+ // Validate connection string format
116
+ function validateConnectionString(connectionString) {
117
+ if (!connectionString) return false;
118
+
119
+ const patterns = [
120
+ /^s3:\/\/[^:]+:[^@]+@[^?]+(\?.*)?$/, // s3://key:secret@bucket?region=...
121
+ /^https?:\/\/[^:]+:[^@]+@[^\/]+\/[^?]+(\?.*)?$/ // http(s)://key:secret@host/bucket?...
122
+ ];
123
+
124
+ return patterns.some(pattern => pattern.test(connectionString));
125
+ }
126
+
127
+ // Start MCP server function
128
+ async function startMcpServer(options) {
129
+ try {
130
+ // Import the MCP server
131
+ const { S3dbMCPServer } = await import('../mcp/server.js');
132
+
133
+ // Set environment variables from options
134
+ if (options.transport) process.env.MCP_TRANSPORT = options.transport;
135
+ if (options.host) process.env.MCP_SERVER_HOST = options.host;
136
+ if (options.port) process.env.MCP_SERVER_PORT = options.port.toString();
137
+ if (options.connectionString) process.env.S3DB_CONNECTION_STRING = options.connectionString;
138
+
139
+ // Create and start server
140
+ const server = new S3dbMCPServer();
141
+
142
+ info(`Starting S3DB MCP Server v${packageJson.version}`);
143
+ info(`Transport: ${options.transport}`);
144
+ info(`Host: ${options.host}`);
145
+ info(`Port: ${options.port}`);
146
+
147
+ if (options.connectionString) {
148
+ info(`Connection: ${options.connectionString.replace(/:[^@]+@/, ':***@')}`); // Hide secrets
149
+ } else {
150
+ warn('No connection string provided - server will require manual connection via MCP tools');
151
+ }
152
+
153
+ // Handle graceful shutdown
154
+ process.on('SIGINT', () => {
155
+ log('\n🛑 Shutting down S3DB MCP Server...', colors.yellow);
156
+ process.exit(0);
157
+ });
158
+
159
+ process.on('SIGTERM', () => {
160
+ log('\n🛑 Shutting down S3DB MCP Server...', colors.yellow);
161
+ process.exit(0);
162
+ });
163
+
164
+ success('S3DB MCP Server started successfully!');
165
+
166
+ if (options.transport === 'sse') {
167
+ success(`Server available at: http://${options.host}:${options.port}/sse`);
168
+ success(`Health check: http://${options.host}:${parseInt(options.port) + 1}/health`);
169
+ } else {
170
+ info('Server running in stdio mode for MCP client communication');
171
+ }
172
+
173
+ } catch (err) {
174
+ error(`Failed to start MCP server: ${err.message}`);
175
+ if (options.verbose) {
176
+ console.error(err.stack);
177
+ }
178
+ process.exit(1);
179
+ }
180
+ }
181
+
182
+ // Setup CLI program
183
+ program
184
+ .name('s3db.js')
185
+ .description('S3DB - Use AWS S3 as a database with ORM capabilities and MCP server')
186
+ .version(packageJson.version);
187
+
188
+ // MCP Server command
189
+ program
190
+ .command('mcp')
191
+ .alias('server')
192
+ .description('Start the S3DB MCP (Model Context Protocol) server')
193
+ .option('-p, --port <port>', 'Port for SSE transport (default: 8000)', '8000')
194
+ .option('-h, --host <host>', 'Host address to bind to (default: 0.0.0.0)', '0.0.0.0')
195
+ .option('-t, --transport <type>', 'Transport type: stdio or sse (default: stdio)', 'stdio')
196
+ .option('-c, --connection-string <string>', 'S3DB connection string (auto-detected if not provided)')
197
+ .option('-v, --verbose', 'Enable verbose logging', false)
198
+ .action(async (options) => {
199
+ // Auto-detect connection string if not provided
200
+ let connectionString = options.connectionString;
201
+
202
+ if (!connectionString) {
203
+ info('Auto-detecting connection string...');
204
+ connectionString = detectConnectionString();
205
+ }
206
+
207
+ if (connectionString) {
208
+ if (!validateConnectionString(connectionString)) {
209
+ error('Invalid connection string format');
210
+ error('Expected formats:');
211
+ error(' s3://key:secret@bucket?region=us-east-1');
212
+ error(' http://key:secret@localhost:9000/bucket (MinIO)');
213
+ error(' https://key:secret@host/bucket (other S3-compatible)');
214
+ process.exit(1);
215
+ }
216
+ success('Connection string detected and validated');
217
+ } else {
218
+ warn('No connection string found. Server will start without auto-connection.');
219
+ warn('You can connect manually using MCP tools or set one of these:');
220
+ warn(' - S3DB_CONNECTION_STRING environment variable');
221
+ warn(' - AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_S3_BUCKET env vars');
222
+ warn(' - ~/.config/mcp/config.json MCP configuration');
223
+ warn(' - .env file in current directory');
224
+ }
225
+
226
+ const serverOptions = {
227
+ ...options,
228
+ port: parseInt(options.port),
229
+ connectionString
230
+ };
231
+
232
+ await startMcpServer(serverOptions);
233
+ });
234
+
235
+ // Connection test command
236
+ program
237
+ .command('test')
238
+ .description('Test S3DB connection and basic operations')
239
+ .option('-c, --connection-string <string>', 'S3DB connection string (auto-detected if not provided)')
240
+ .option('-v, --verbose', 'Enable verbose output', false)
241
+ .action(async (options) => {
242
+ try {
243
+ // Auto-detect connection string if not provided
244
+ let connectionString = options.connectionString;
245
+
246
+ if (!connectionString) {
247
+ info('Auto-detecting connection string...');
248
+ connectionString = detectConnectionString();
249
+ }
250
+
251
+ if (!connectionString) {
252
+ error('No connection string found. Please provide one using:');
253
+ error(' s3db.js test -c "s3://key:secret@bucket?region=us-east-1"');
254
+ process.exit(1);
255
+ }
256
+
257
+ if (!validateConnectionString(connectionString)) {
258
+ error('Invalid connection string format');
259
+ process.exit(1);
260
+ }
261
+
262
+ info('Testing S3DB connection...');
263
+
264
+ // Import and test S3DB
265
+ const { S3db } = await import('../dist/s3db.es.js');
266
+
267
+ const database = new S3db({
268
+ connectionString,
269
+ verbose: options.verbose
270
+ });
271
+
272
+ info('Connecting to database...');
273
+ await database.connect();
274
+ success('Connected successfully!');
275
+
276
+ info('Testing basic operations...');
277
+
278
+ // Test resource listing
279
+ const resources = await database.listResources();
280
+ success(`Found ${resources.length} resources`);
281
+
282
+ if (options.verbose && resources.length > 0) {
283
+ console.log('Resources:', resources);
284
+ }
285
+
286
+ await database.disconnect();
287
+ success('All tests passed!');
288
+
289
+ } catch (err) {
290
+ error(`Connection test failed: ${err.message}`);
291
+ if (options.verbose) {
292
+ console.error(err.stack);
293
+ }
294
+ process.exit(1);
295
+ }
296
+ });
297
+
298
+ // Config command
299
+ program
300
+ .command('config')
301
+ .description('Display current configuration and auto-detected settings')
302
+ .action(() => {
303
+ info('S3DB Configuration:');
304
+ console.log('');
305
+
306
+ log('📦 Package Information:', colors.cyan);
307
+ console.log(` Name: ${packageJson.name}`);
308
+ console.log(` Version: ${packageJson.version}`);
309
+ console.log(` Description: ${packageJson.description}`);
310
+ console.log('');
311
+
312
+ log('🔗 Connection String Detection:', colors.cyan);
313
+ const connectionString = detectConnectionString();
314
+ if (connectionString) {
315
+ success(` Detected: ${connectionString.replace(/:[^@]+@/, ':***@')}`);
316
+ } else {
317
+ warn(' No connection string detected');
318
+ }
319
+ console.log('');
320
+
321
+ log('🌍 Environment Variables:', colors.cyan);
322
+ const envVars = [
323
+ 'S3DB_CONNECTION_STRING',
324
+ 'AWS_ACCESS_KEY_ID',
325
+ 'AWS_SECRET_ACCESS_KEY',
326
+ 'AWS_S3_BUCKET',
327
+ 'AWS_REGION',
328
+ 'MCP_TRANSPORT',
329
+ 'MCP_SERVER_HOST',
330
+ 'MCP_SERVER_PORT'
331
+ ];
332
+
333
+ envVars.forEach(envVar => {
334
+ const value = process.env[envVar];
335
+ if (value) {
336
+ if (envVar.includes('SECRET') || envVar.includes('KEY')) {
337
+ console.log(` ${envVar}: ${'*'.repeat(Math.min(value.length, 8))}`);
338
+ } else {
339
+ console.log(` ${envVar}: ${value}`);
340
+ }
341
+ } else {
342
+ console.log(` ${envVar}: ${colors.yellow}not set${colors.reset}`);
343
+ }
344
+ });
345
+ console.log('');
346
+
347
+ log('📁 Configuration Files:', colors.cyan);
348
+ const configFiles = [
349
+ join(homedir(), '.config', 'mcp', 'config.json'),
350
+ join(process.cwd(), '.env')
351
+ ];
352
+
353
+ configFiles.forEach(configFile => {
354
+ if (existsSync(configFile)) {
355
+ success(` ${configFile}: found`);
356
+ } else {
357
+ console.log(` ${configFile}: ${colors.yellow}not found${colors.reset}`);
358
+ }
359
+ });
360
+ });
361
+
362
+ // Examples command
363
+ program
364
+ .command('examples')
365
+ .description('Show usage examples and common patterns')
366
+ .action(() => {
367
+ log('🚀 S3DB CLI Examples:', colors.bright + colors.cyan);
368
+ console.log('');
369
+
370
+ log('1. Start MCP Server (stdio mode for MCP clients):', colors.green);
371
+ console.log(' s3db.js mcp');
372
+ console.log(' s3db.js server # alias');
373
+ console.log('');
374
+
375
+ log('2. Start MCP Server with SSE transport:', colors.green);
376
+ console.log(' s3db.js mcp --transport sse --port 8888');
377
+ console.log(' s3db.js mcp -t sse -p 8888 # short form');
378
+ console.log('');
379
+
380
+ log('3. Start with explicit connection string:', colors.green);
381
+ console.log(' s3db.js mcp -c "s3://key:secret@bucket?region=us-east-1"');
382
+ console.log('');
383
+
384
+ log('4. Test connection:', colors.green);
385
+ console.log(' s3db.js test');
386
+ console.log(' s3db.js test --verbose');
387
+ console.log(' s3db.js test -c "s3://key:secret@bucket"');
388
+ console.log('');
389
+
390
+ log('5. View configuration:', colors.green);
391
+ console.log(' s3db.js config');
392
+ console.log('');
393
+
394
+ log('💡 Connection String Formats:', colors.yellow);
395
+ console.log(' AWS S3:');
396
+ console.log(' s3://accessKey:secretKey@bucketName?region=us-east-1');
397
+ console.log(' MinIO:');
398
+ console.log(' http://accessKey:secretKey@localhost:9000/bucketName');
399
+ console.log(' DigitalOcean Spaces:');
400
+ console.log(' https://accessKey:secretKey@nyc3.digitaloceanspaces.com/bucketName');
401
+ console.log('');
402
+
403
+ log('🔧 Environment Variables (auto-detected):', colors.yellow);
404
+ console.log(' S3DB_CONNECTION_STRING="s3://key:secret@bucket"');
405
+ console.log(' AWS_ACCESS_KEY_ID=your_access_key');
406
+ console.log(' AWS_SECRET_ACCESS_KEY=your_secret_key');
407
+ console.log(' AWS_S3_BUCKET=your_bucket');
408
+ console.log(' AWS_REGION=us-east-1');
409
+ console.log('');
410
+
411
+ log('📱 Usage with npx:', colors.yellow);
412
+ console.log(' npx s3db.js mcp --port 8888');
413
+ console.log(' npx s3db.js test');
414
+ console.log(' npx s3db.js config');
415
+ });
416
+
417
+ // Handle unknown commands
418
+ program.on('command:*', () => {
419
+ error(`Unknown command: ${program.args.join(' ')}`);
420
+ error('Use --help to see available commands');
421
+ process.exit(1);
422
+ });
423
+
424
+ // Show help if no arguments provided
425
+ if (process.argv.length <= 2) {
426
+ program.help();
427
+ }
428
+
429
+ // Parse command line arguments
430
+ program.parse();
package/dist/s3db.cjs.js CHANGED
@@ -1367,17 +1367,13 @@ class AuditPlugin extends plugin_class_default {
1367
1367
  const plugin = this;
1368
1368
  resource.deleteMany = async function(ids) {
1369
1369
  const objectsToDelete = [];
1370
- if (plugin.config.includeData) {
1371
- for (const id of ids) {
1372
- const [ok, err, fetched] = await try_fn_default(() => resource.get(id));
1373
- if (ok) {
1374
- objectsToDelete.push(fetched);
1375
- } else {
1376
- objectsToDelete.push({ id });
1377
- }
1370
+ for (const id of ids) {
1371
+ const [ok, err, fetched] = await try_fn_default(() => resource.get(id));
1372
+ if (ok) {
1373
+ objectsToDelete.push(fetched);
1374
+ } else {
1375
+ objectsToDelete.push({ id });
1378
1376
  }
1379
- } else {
1380
- objectsToDelete.push(...ids.map((id) => ({ id })));
1381
1377
  }
1382
1378
  const result = await originalDeleteMany(ids);
1383
1379
  for (const oldData of objectsToDelete) {
@@ -1482,18 +1478,37 @@ class AuditPlugin extends plugin_class_default {
1482
1478
  async getAuditLogs(options = {}) {
1483
1479
  if (!this.auditResource) return [];
1484
1480
  const { resourceName, operation, recordId, partition, startDate, endDate, limit = 100, offset = 0 } = options;
1485
- let query = {};
1486
- if (resourceName) query.resourceName = resourceName;
1487
- if (operation) query.operation = operation;
1488
- if (recordId) query.recordId = recordId;
1489
- if (partition) query.partition = partition;
1490
- if (startDate || endDate) {
1491
- query.timestamp = {};
1492
- if (startDate) query.timestamp.$gte = startDate;
1493
- if (endDate) query.timestamp.$lte = endDate;
1494
- }
1495
- const result = await this.auditResource.page({ query, limit, offset });
1496
- return result.items || [];
1481
+ const hasFilters = resourceName || operation || recordId || partition || startDate || endDate;
1482
+ let items = [];
1483
+ if (hasFilters) {
1484
+ const fetchSize = Math.min(1e4, Math.max(1e3, (limit + offset) * 20));
1485
+ const result = await this.auditResource.list({ limit: fetchSize });
1486
+ items = result || [];
1487
+ if (resourceName) {
1488
+ items = items.filter((log) => log.resourceName === resourceName);
1489
+ }
1490
+ if (operation) {
1491
+ items = items.filter((log) => log.operation === operation);
1492
+ }
1493
+ if (recordId) {
1494
+ items = items.filter((log) => log.recordId === recordId);
1495
+ }
1496
+ if (partition) {
1497
+ items = items.filter((log) => log.partition === partition);
1498
+ }
1499
+ if (startDate || endDate) {
1500
+ items = items.filter((log) => {
1501
+ const timestamp = new Date(log.timestamp);
1502
+ if (startDate && timestamp < new Date(startDate)) return false;
1503
+ if (endDate && timestamp > new Date(endDate)) return false;
1504
+ return true;
1505
+ });
1506
+ }
1507
+ return items.slice(offset, offset + limit);
1508
+ } else {
1509
+ const result = await this.auditResource.page({ size: limit, offset });
1510
+ return result.items || [];
1511
+ }
1497
1512
  }
1498
1513
  async getRecordHistory(resourceName, recordId) {
1499
1514
  return await this.getAuditLogs({ resourceName, recordId });
@@ -13507,7 +13522,7 @@ class Database extends EventEmitter {
13507
13522
  this.id = idGenerator(7);
13508
13523
  this.version = "1";
13509
13524
  this.s3dbVersion = (() => {
13510
- const [ok, err, version] = try_fn_default(() => true ? "8.1.1" : "latest");
13525
+ const [ok, err, version] = try_fn_default(() => true ? "8.2.0" : "latest");
13511
13526
  return ok ? version : "latest";
13512
13527
  })();
13513
13528
  this.resources = {};
@@ -13516,6 +13531,7 @@ class Database extends EventEmitter {
13516
13531
  this.verbose = options.verbose || false;
13517
13532
  this.parallelism = parseInt(options.parallelism + "") || 10;
13518
13533
  this.plugins = options.plugins || [];
13534
+ this.pluginRegistry = {};
13519
13535
  this.pluginList = options.plugins || [];
13520
13536
  this.cache = options.cache;
13521
13537
  this.passphrase = options.passphrase || "secret";
@@ -13808,6 +13824,8 @@ class Database extends EventEmitter {
13808
13824
  if (plugin.beforeSetup) await plugin.beforeSetup();
13809
13825
  await plugin.setup(db);
13810
13826
  if (plugin.afterSetup) await plugin.afterSetup();
13827
+ const pluginName = this._getPluginName(plugin);
13828
+ this.pluginRegistry[pluginName] = plugin;
13811
13829
  });
13812
13830
  await Promise.all(setupProms);
13813
13831
  const startProms = plugins.map(async (plugin) => {
@@ -13823,8 +13841,15 @@ class Database extends EventEmitter {
13823
13841
  * @param {Plugin} plugin - Plugin instance to register
13824
13842
  * @param {string} [name] - Optional name for the plugin (defaults to plugin.constructor.name)
13825
13843
  */
13844
+ /**
13845
+ * Get the normalized plugin name
13846
+ * @private
13847
+ */
13848
+ _getPluginName(plugin, customName = null) {
13849
+ return customName || plugin.constructor.name.replace("Plugin", "").toLowerCase();
13850
+ }
13826
13851
  async usePlugin(plugin, name = null) {
13827
- const pluginName = name || plugin.constructor.name.replace("Plugin", "").toLowerCase();
13852
+ const pluginName = this._getPluginName(plugin, name);
13828
13853
  this.plugins[pluginName] = plugin;
13829
13854
  if (this.isConnected()) {
13830
13855
  await plugin.setup(this);