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.
- package/.github/workflows/npm-publish.yml +1 -1
- package/index.js +2 -0
- package/package.json +28 -7
- package/readme.md +342 -235
- package/src/core/Database.js +295 -0
- package/src/core/Events.js +286 -0
- package/src/core/IndexManager.js +814 -0
- package/src/core/Persistence.js +376 -0
- package/src/core/QueryEngine.js +448 -0
- package/src/core/Storage.js +322 -0
- package/src/core/Validator.js +325 -0
- package/src/index.js +90 -469
- package/src/performance/Cache.js +339 -0
- package/src/performance/LazyLoader.js +355 -0
- package/src/performance/MemoryManager.js +496 -0
- package/src/server/api.js +688 -0
- package/src/server/websocket.js +528 -0
- package/src/utils/benchmark.js +52 -0
- package/src/utils/dot-notation.js +248 -0
- package/src/utils/helpers.js +276 -0
- package/src/utils/profiler.js +71 -0
- package/src/version.js +38 -0
|
@@ -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;
|