sehawq.db 2.4.2 → 4.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.
@@ -0,0 +1,688 @@
1
+ /**
2
+ * API Server - Turns your database into a full REST API 🚀
3
+ *
4
+ * From zero to API in 5 seconds flat
5
+ * Because setting up Express routes should be easy, not exhausting 😴
6
+ */
7
+
8
+ const express = require('express');
9
+ const cors = require('cors');
10
+ const { performance } = require('perf_hooks');
11
+
12
+ class APIServer {
13
+ constructor(database, options = {}) {
14
+ this.db = database;
15
+ this.options = {
16
+ port: 3000,
17
+ enableCors: true,
18
+ apiKey: null, // Optional API key protection
19
+ rateLimit: 1000, // Requests per minute per IP
20
+ enableLogging: true,
21
+ ...options
22
+ };
23
+
24
+ this.app = express();
25
+ this.server = null;
26
+ this.clients = new Map(); // For connection tracking
27
+
28
+ // Middleware and routes
29
+ this._setupMiddleware();
30
+ this._setupRoutes();
31
+ this._setupErrorHandling();
32
+
33
+ this.stats = {
34
+ requests: 0,
35
+ errors: 0,
36
+ activeConnections: 0,
37
+ totalConnections: 0,
38
+ routes: {}
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Setup middleware - the boring but important stuff
44
+ */
45
+ _setupMiddleware() {
46
+ // CORS for cross-origin requests
47
+ if (this.options.enableCors) {
48
+ this.app.use(cors());
49
+ }
50
+
51
+ // JSON body parsing
52
+ this.app.use(express.json({ limit: '10mb' }));
53
+
54
+ // Request logging
55
+ if (this.options.enableLogging) {
56
+ this.app.use(this._requestLogger.bind(this));
57
+ }
58
+
59
+ // API key authentication (optional)
60
+ if (this.options.apiKey) {
61
+ this.app.use(this._apiKeyAuth.bind(this));
62
+ }
63
+
64
+ // Rate limiting
65
+ this.app.use(this._rateLimiter.bind(this));
66
+ }
67
+
68
+ /**
69
+ * Request logger middleware
70
+ */
71
+ _requestLogger(req, res, next) {
72
+ const startTime = performance.now();
73
+ const clientIp = req.ip || req.connection.remoteAddress;
74
+
75
+ // Track connection
76
+ this.stats.activeConnections++;
77
+ this.stats.totalConnections++;
78
+
79
+ // Log when response finishes
80
+ res.on('finish', () => {
81
+ const duration = performance.now() - startTime;
82
+ const logMessage = `${new Date().toISOString()} - ${clientIp} - ${req.method} ${req.path} - ${res.statusCode} - ${duration.toFixed(2)}ms`;
83
+
84
+ console.log(logMessage);
85
+
86
+ // Update stats
87
+ this.stats.activeConnections--;
88
+ this.stats.requests++;
89
+
90
+ // Track route statistics
91
+ const routeKey = `${req.method} ${req.path}`;
92
+ this.stats.routes[routeKey] = this.stats.routes[routeKey] || { count: 0, totalTime: 0 };
93
+ this.stats.routes[routeKey].count++;
94
+ this.stats.routes[routeKey].totalTime += duration;
95
+ });
96
+
97
+ next();
98
+ }
99
+
100
+ /**
101
+ * API key authentication middleware
102
+ */
103
+ _apiKeyAuth(req, res, next) {
104
+ // Skip auth for health check
105
+ if (req.path === '/api/health') return next();
106
+
107
+ const apiKey = req.headers['x-api-key'] || req.query.apiKey;
108
+
109
+ if (!apiKey) {
110
+ return res.status(401).json({
111
+ error: 'API key required',
112
+ message: 'Provide API key via X-API-Key header or apiKey query parameter'
113
+ });
114
+ }
115
+
116
+ if (apiKey !== this.options.apiKey) {
117
+ return res.status(403).json({
118
+ error: 'Invalid API key',
119
+ message: 'The provided API key is not valid'
120
+ });
121
+ }
122
+
123
+ next();
124
+ }
125
+
126
+ /**
127
+ * Simple rate limiter middleware
128
+ */
129
+ _rateLimiter(req, res, next) {
130
+ const clientIp = req.ip || req.connection.remoteAddress;
131
+ const now = Date.now();
132
+ const windowMs = 60000; // 1 minute
133
+
134
+ // Initialize client data if not exists
135
+ if (!this.clients.has(clientIp)) {
136
+ this.clients.set(clientIp, {
137
+ requests: 0,
138
+ firstRequest: now,
139
+ lastRequest: now
140
+ });
141
+ }
142
+
143
+ const clientData = this.clients.get(clientIp);
144
+
145
+ // Reset counter if window has passed
146
+ if (now - clientData.firstRequest > windowMs) {
147
+ clientData.requests = 0;
148
+ clientData.firstRequest = now;
149
+ }
150
+
151
+ // Check rate limit
152
+ if (clientData.requests >= this.options.rateLimit) {
153
+ return res.status(429).json({
154
+ error: 'Rate limit exceeded',
155
+ message: `Maximum ${this.options.rateLimit} requests per minute allowed`,
156
+ retryAfter: Math.ceil((clientData.firstRequest + windowMs - now) / 1000)
157
+ });
158
+ }
159
+
160
+ // Increment counter
161
+ clientData.requests++;
162
+ clientData.lastRequest = now;
163
+
164
+ // Clean up old clients (prevent memory leaks)
165
+ this._cleanupOldClients();
166
+
167
+ next();
168
+ }
169
+
170
+ /**
171
+ * Clean up old client data
172
+ */
173
+ _cleanupOldClients() {
174
+ const now = Date.now();
175
+ const maxAge = 300000; // 5 minutes
176
+
177
+ for (const [ip, data] of this.clients.entries()) {
178
+ if (now - data.lastRequest > maxAge) {
179
+ this.clients.delete(ip);
180
+ }
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Setup all API routes
186
+ */
187
+ _setupRoutes() {
188
+ // Health check
189
+ this.app.get('/api/health', (req, res) => {
190
+ res.json({
191
+ status: 'healthy',
192
+ timestamp: new Date().toISOString(),
193
+ uptime: process.uptime(),
194
+ memory: process.memoryUsage(),
195
+ database: {
196
+ records: this.db.data.size,
197
+ connected: true
198
+ }
199
+ });
200
+ });
201
+
202
+ // Get all data
203
+ this.app.get('/api/data', (req, res) => {
204
+ try {
205
+ const data = this.db.all();
206
+ res.json({
207
+ success: true,
208
+ data: data,
209
+ count: Object.keys(data).length
210
+ });
211
+ } catch (error) {
212
+ this._handleError(res, error, 'Failed to retrieve all data');
213
+ }
214
+ });
215
+
216
+ // Get specific key
217
+ this.app.get('/api/data/:key', (req, res) => {
218
+ try {
219
+ const value = this.db.get(req.params.key);
220
+
221
+ if (value === undefined) {
222
+ return res.status(404).json({
223
+ error: 'Key not found',
224
+ message: `Key '${req.params.key}' does not exist`
225
+ });
226
+ }
227
+
228
+ res.json({
229
+ success: true,
230
+ data: value
231
+ });
232
+ } catch (error) {
233
+ this._handleError(res, error, 'Failed to retrieve data');
234
+ }
235
+ });
236
+
237
+ // Set/update key
238
+ this.app.post('/api/data/:key', (req, res) => {
239
+ try {
240
+ const { value } = req.body;
241
+
242
+ if (value === undefined) {
243
+ return res.status(400).json({
244
+ error: 'Missing value',
245
+ message: 'Request body must contain a "value" field'
246
+ });
247
+ }
248
+
249
+ this.db.set(req.params.key, value);
250
+
251
+ res.json({
252
+ success: true,
253
+ message: 'Data set successfully',
254
+ key: req.params.key
255
+ });
256
+ } catch (error) {
257
+ this._handleError(res, error, 'Failed to set data');
258
+ }
259
+ });
260
+
261
+ // Update key (alias for POST)
262
+ this.app.put('/api/data/:key', (req, res) => {
263
+ try {
264
+ const { value } = req.body;
265
+
266
+ if (value === undefined) {
267
+ return res.status(400).json({
268
+ error: 'Missing value',
269
+ message: 'Request body must contain a "value" field'
270
+ });
271
+ }
272
+
273
+ this.db.set(req.params.key, value);
274
+
275
+ res.json({
276
+ success: true,
277
+ message: 'Data updated successfully',
278
+ key: req.params.key
279
+ });
280
+ } catch (error) {
281
+ this._handleError(res, error, 'Failed to update data');
282
+ }
283
+ });
284
+
285
+ // Delete key
286
+ this.app.delete('/api/data/:key', (req, res) => {
287
+ try {
288
+ const deleted = this.db.delete(req.params.key);
289
+
290
+ if (!deleted) {
291
+ return res.status(404).json({
292
+ error: 'Key not found',
293
+ message: `Key '${req.params.key}' does not exist`
294
+ });
295
+ }
296
+
297
+ res.json({
298
+ success: true,
299
+ message: 'Data deleted successfully',
300
+ key: req.params.key
301
+ });
302
+ } catch (error) {
303
+ this._handleError(res, error, 'Failed to delete data');
304
+ }
305
+ });
306
+
307
+ // Query data
308
+ this.app.post('/api/query', (req, res) => {
309
+ try {
310
+ const { filter, options } = req.body;
311
+
312
+ if (!filter) {
313
+ return res.status(400).json({
314
+ error: 'Missing filter',
315
+ message: 'Request body must contain a "filter" field'
316
+ });
317
+ }
318
+
319
+ // Convert string filter to function
320
+ let filterFn;
321
+ if (typeof filter === 'string') {
322
+ // Simple field-based filtering
323
+ const [field, operator, value] = filter.split(' ');
324
+ filterFn = this.db.queryEngine._compileWhereClause(field, operator, JSON.parse(value));
325
+ } else if (typeof filter === 'object') {
326
+ // MongoDB-style query
327
+ filterFn = (item) => {
328
+ for (const [field, condition] of Object.entries(filter)) {
329
+ if (item[field] !== condition) return false;
330
+ }
331
+ return true;
332
+ };
333
+ } else {
334
+ return res.status(400).json({
335
+ error: 'Invalid filter',
336
+ message: 'Filter must be a string or object'
337
+ });
338
+ }
339
+
340
+ const results = this.db.queryEngine.find(filterFn, options).toArray();
341
+
342
+ res.json({
343
+ success: true,
344
+ data: results,
345
+ count: results.length
346
+ });
347
+ } catch (error) {
348
+ this._handleError(res, error, 'Query failed');
349
+ }
350
+ });
351
+
352
+ // Aggregation endpoints
353
+ this.app.get('/api/aggregate/:operation', (req, res) => {
354
+ try {
355
+ const { operation } = req.params;
356
+ const { field, filter } = req.query;
357
+
358
+ if (!field) {
359
+ return res.status(400).json({
360
+ error: 'Missing field',
361
+ message: 'Query parameter "field" is required'
362
+ });
363
+ }
364
+
365
+ let result;
366
+ const filterFn = filter ? this._parseFilter(filter) : null;
367
+
368
+ switch (operation) {
369
+ case 'count':
370
+ result = this.db.queryEngine.count(filterFn);
371
+ break;
372
+ case 'sum':
373
+ result = this.db.queryEngine.sum(field, filterFn);
374
+ break;
375
+ case 'avg':
376
+ result = this.db.queryEngine.avg(field, filterFn);
377
+ break;
378
+ case 'min':
379
+ result = this.db.queryEngine.min(field, filterFn);
380
+ break;
381
+ case 'max':
382
+ result = this.db.queryEngine.max(field, filterFn);
383
+ break;
384
+ default:
385
+ return res.status(400).json({
386
+ error: 'Invalid operation',
387
+ message: `Operation '${operation}' not supported. Use: count, sum, avg, min, max`
388
+ });
389
+ }
390
+
391
+ res.json({
392
+ success: true,
393
+ operation,
394
+ field,
395
+ result
396
+ });
397
+ } catch (error) {
398
+ this._handleError(res, error, 'Aggregation failed');
399
+ }
400
+ });
401
+
402
+ // Array operations
403
+ this.app.post('/api/array/:key/push', (req, res) => {
404
+ try {
405
+ const { value } = req.body;
406
+
407
+ if (value === undefined) {
408
+ return res.status(400).json({
409
+ error: 'Missing value',
410
+ message: 'Request body must contain a "value" field'
411
+ });
412
+ }
413
+
414
+ const current = this.db.get(req.params.key) || [];
415
+
416
+ if (!Array.isArray(current)) {
417
+ return res.status(400).json({
418
+ error: 'Not an array',
419
+ message: `Key '${req.params.key}' does not contain an array`
420
+ });
421
+ }
422
+
423
+ current.push(value);
424
+ this.db.set(req.params.key, current);
425
+
426
+ res.json({
427
+ success: true,
428
+ message: 'Item pushed to array',
429
+ key: req.params.key,
430
+ newLength: current.length
431
+ });
432
+ } catch (error) {
433
+ this._handleError(res, error, 'Push operation failed');
434
+ }
435
+ });
436
+
437
+ this.app.post('/api/array/:key/pull', (req, res) => {
438
+ try {
439
+ const { value } = req.body;
440
+
441
+ if (value === undefined) {
442
+ return res.status(400).json({
443
+ error: 'Missing value',
444
+ message: 'Request body must contain a "value" field'
445
+ });
446
+ }
447
+
448
+ const current = this.db.get(req.params.key) || [];
449
+
450
+ if (!Array.isArray(current)) {
451
+ return res.status(400).json({
452
+ error: 'Not an array',
453
+ message: `Key '${req.params.key}' does not contain an array`
454
+ });
455
+ }
456
+
457
+ const index = current.indexOf(value);
458
+ if (index > -1) {
459
+ current.splice(index, 1);
460
+ this.db.set(req.params.key, current);
461
+ }
462
+
463
+ res.json({
464
+ success: true,
465
+ message: 'Item pulled from array',
466
+ key: req.params.key,
467
+ removed: index > -1,
468
+ newLength: current.length
469
+ });
470
+ } catch (error) {
471
+ this._handleError(res, error, 'Pull operation failed');
472
+ }
473
+ });
474
+
475
+ // Math operations
476
+ this.app.post('/api/math/:key/add', (req, res) => {
477
+ try {
478
+ const { value } = req.body;
479
+
480
+ if (value === undefined || typeof value !== 'number') {
481
+ return res.status(400).json({
482
+ error: 'Invalid value',
483
+ message: 'Request body must contain a numeric "value" field'
484
+ });
485
+ }
486
+
487
+ const current = this.db.get(req.params.key) || 0;
488
+
489
+ if (typeof current !== 'number') {
490
+ return res.status(400).json({
491
+ error: 'Not a number',
492
+ message: `Key '${req.params.key}' does not contain a number`
493
+ });
494
+ }
495
+
496
+ const newValue = current + value;
497
+ this.db.set(req.params.key, newValue);
498
+
499
+ res.json({
500
+ success: true,
501
+ message: 'Value added',
502
+ key: req.params.key,
503
+ oldValue: current,
504
+ newValue: newValue
505
+ });
506
+ } catch (error) {
507
+ this._handleError(res, error, 'Add operation failed');
508
+ }
509
+ });
510
+
511
+ this.app.post('/api/math/:key/subtract', (req, res) => {
512
+ try {
513
+ const { value } = req.body;
514
+
515
+ if (value === undefined || typeof value !== 'number') {
516
+ return res.status(400).json({
517
+ error: 'Invalid value',
518
+ message: 'Request body must contain a numeric "value" field'
519
+ });
520
+ }
521
+
522
+ const current = this.db.get(req.params.key) || 0;
523
+
524
+ if (typeof current !== 'number') {
525
+ return res.status(400).json({
526
+ error: 'Not a number',
527
+ message: `Key '${req.params.key}' does not contain a number`
528
+ });
529
+ }
530
+
531
+ const newValue = current - value;
532
+ this.db.set(req.params.key, newValue);
533
+
534
+ res.json({
535
+ success: true,
536
+ message: 'Value subtracted',
537
+ key: req.params.key,
538
+ oldValue: current,
539
+ newValue: newValue
540
+ });
541
+ } catch (error) {
542
+ this._handleError(res, error, 'Subtract operation failed');
543
+ }
544
+ });
545
+
546
+ // Get server statistics
547
+ this.app.get('/api/stats', (req, res) => {
548
+ try {
549
+ const dbStats = this.db.getStats ? this.db.getStats() : {};
550
+
551
+ res.json({
552
+ success: true,
553
+ server: this.stats,
554
+ database: dbStats,
555
+ memory: process.memoryUsage()
556
+ });
557
+ } catch (error) {
558
+ this._handleError(res, error, 'Failed to get statistics');
559
+ }
560
+ });
561
+ }
562
+
563
+ /**
564
+ * Parse filter string into function
565
+ */
566
+ _parseFilter(filterStr) {
567
+ // Simple parser for query string filters
568
+ // Format: "field operator value"
569
+ const [field, operator, value] = filterStr.split(' ');
570
+ return this.db.queryEngine._compileWhereClause(field, operator, JSON.parse(value));
571
+ }
572
+
573
+ /**
574
+ * Setup error handling middleware
575
+ */
576
+ _setupErrorHandling() {
577
+ // 404 handler
578
+ this.app.use('*', (req, res) => {
579
+ res.status(404).json({
580
+ error: 'Endpoint not found',
581
+ message: `Route ${req.method} ${req.originalUrl} does not exist`,
582
+ availableEndpoints: this._getAvailableEndpoints()
583
+ });
584
+ });
585
+
586
+ // Global error handler
587
+ this.app.use((error, req, res, next) => {
588
+ this.stats.errors++;
589
+
590
+ console.error('🚨 API Error:', error);
591
+
592
+ res.status(500).json({
593
+ error: 'Internal server error',
594
+ message: process.env.NODE_ENV === 'development' ? error.message : 'Something went wrong'
595
+ });
596
+ });
597
+ }
598
+
599
+ /**
600
+ * Handle API errors consistently
601
+ */
602
+ _handleError(res, error, defaultMessage) {
603
+ this.stats.errors++;
604
+
605
+ console.error('🚨 API Operation Failed:', error);
606
+
607
+ res.status(500).json({
608
+ error: 'Operation failed',
609
+ message: defaultMessage,
610
+ details: process.env.NODE_ENV === 'development' ? error.message : undefined
611
+ });
612
+ }
613
+
614
+ /**
615
+ * Get list of available endpoints
616
+ */
617
+ _getAvailableEndpoints() {
618
+ return [
619
+ 'GET /api/health',
620
+ 'GET /api/data',
621
+ 'GET /api/data/:key',
622
+ 'POST /api/data/:key',
623
+ 'PUT /api/data/:key',
624
+ 'DELETE /api/data/:key',
625
+ 'POST /api/query',
626
+ 'GET /api/aggregate/:operation',
627
+ 'POST /api/array/:key/push',
628
+ 'POST /api/array/:key/pull',
629
+ 'POST /api/math/:key/add',
630
+ 'POST /api/math/:key/subtract',
631
+ 'GET /api/stats'
632
+ ];
633
+ }
634
+
635
+ /**
636
+ * Start the API server
637
+ */
638
+ async start() {
639
+ return new Promise((resolve, reject) => {
640
+ this.server = this.app.listen(this.options.port, (error) => {
641
+ if (error) {
642
+ reject(error);
643
+ return;
644
+ }
645
+
646
+ console.log(`🚀 SehawqDB API server running on port ${this.options.port}`);
647
+ console.log(`📚 API Documentation: http://localhost:${this.options.port}/api/health`);
648
+
649
+ if (this.options.apiKey) {
650
+ console.log(`🔐 API Key protection: ENABLED`);
651
+ }
652
+
653
+ resolve();
654
+ });
655
+ });
656
+ }
657
+
658
+ /**
659
+ * Stop the API server
660
+ */
661
+ async stop() {
662
+ return new Promise((resolve) => {
663
+ if (this.server) {
664
+ this.server.close(() => {
665
+ console.log('🛑 API server stopped');
666
+ resolve();
667
+ });
668
+ } else {
669
+ resolve();
670
+ }
671
+ });
672
+ }
673
+
674
+ /**
675
+ * Get server statistics
676
+ */
677
+ getStats() {
678
+ return {
679
+ ...this.stats,
680
+ uptime: process.uptime(),
681
+ port: this.options.port,
682
+ apiKeyEnabled: !!this.options.apiKey,
683
+ activeClients: this.clients.size
684
+ };
685
+ }
686
+ }
687
+
688
+ module.exports = APIServer;