sehawq.db 2.3.0 → 3.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/package.json +7 -2
- package/readme.md +223 -28
- package/src/index.js +588 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sehawq.db",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Lightweight JSON-based key-value database with namespaces, array & math helpers.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -11,5 +11,10 @@
|
|
|
11
11
|
"lightweight"
|
|
12
12
|
],
|
|
13
13
|
"author": "Omer (sehawq)",
|
|
14
|
-
"license": "MIT"
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"cors": "^2.8.5",
|
|
17
|
+
"express": "^5.1.0",
|
|
18
|
+
"socket.io": "^4.8.1"
|
|
19
|
+
}
|
|
15
20
|
}
|
package/readme.md
CHANGED
|
@@ -1,46 +1,241 @@
|
|
|
1
|
-
# sehawq.db
|
|
1
|
+
# sehawq.db 🚀
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/sehawq.db)
|
|
4
4
|
[](https://www.npmjs.com/package/sehawq.db)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
**
|
|
8
|
-
|
|
7
|
+
**The most powerful JSON-based database for Node.js**
|
|
8
|
+
Local database + REST API + Real-time Sync = **Firebase Alternative in One Package!**
|
|
9
|
+
|
|
10
|
+
Perfect for: APIs, Real-time apps, Chat apps, Collaborative tools, Prototypes, and Production!
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 🎯 Why SehawqDB?
|
|
15
|
+
|
|
16
|
+
❌ **Firebase**: Expensive, vendor lock-in, complex pricing
|
|
17
|
+
❌ **MongoDB**: Heavy, requires separate server setup
|
|
18
|
+
❌ **Redis**: In-memory only, no persistence by default
|
|
19
|
+
|
|
20
|
+
✅ **SehawqDB**: Lightweight, local-first, REST API built-in, real-time sync, **ZERO configuration!**
|
|
9
21
|
|
|
10
22
|
---
|
|
11
23
|
|
|
12
|
-
##
|
|
24
|
+
## 🔥 Features
|
|
25
|
+
|
|
26
|
+
### 💾 Core Database
|
|
27
|
+
- **JSON-based storage** — Simple, readable, git-friendly
|
|
28
|
+
- **Query System** — MongoDB-like queries with `find()`, `where()`, filtering
|
|
29
|
+
- **Aggregations** — `sum()`, `avg()`, `min()`, `max()`, `groupBy()`
|
|
30
|
+
- **Method Chaining** — Fluent API for complex queries
|
|
31
|
+
- **Dot notation** — Access nested data easily
|
|
32
|
+
|
|
33
|
+
### 🌐 Built-in REST API (NEW!)
|
|
34
|
+
- **Zero configuration** — Call `.startServer()` and you're live!
|
|
35
|
+
- **Full CRUD** — GET, POST, PUT, DELETE endpoints
|
|
36
|
+
- **Query API** — Filter, sort, paginate via HTTP
|
|
37
|
+
- **Authentication** — Optional API key protection
|
|
38
|
+
|
|
39
|
+
### ⚡ Real-time Sync (NEW!)
|
|
40
|
+
- **WebSocket integration** — Powered by Socket.io
|
|
41
|
+
- **Live updates** — All clients sync instantly
|
|
42
|
+
- **Event-driven** — Listen to data changes in real-time
|
|
43
|
+
- **Cross-platform** — Works with React, Vue, Angular, mobile apps
|
|
44
|
+
|
|
45
|
+
### 🔧 Developer Experience
|
|
46
|
+
- **TypeScript ready** — Full type definitions
|
|
47
|
+
- **Events** — Hook into all database operations
|
|
48
|
+
- **Backup & Restore** — Easy data management
|
|
49
|
+
- **Auto-save** — Configurable intervals
|
|
50
|
+
- **Array & Math helpers** — Built-in utilities
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 📦 Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm install sehawq.db express socket.io socket.io-client cors
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
13
61
|
|
|
14
|
-
|
|
15
|
-
- **Key-Value structure** — Simple `set`, `get`, `delete` logic.
|
|
16
|
-
- **Dot-notation namespace** — Access nested data with `user.123.balance`.
|
|
17
|
-
- **Sync & Async API** — Choose blocking or non-blocking file operations.
|
|
18
|
-
- **Auto-save** — Writes changes to disk at regular intervals.
|
|
62
|
+
## ⚡ Quick Start (Local Database)
|
|
19
63
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
64
|
+
```javascript
|
|
65
|
+
const SehawqDB = require('sehawq.db');
|
|
66
|
+
const db = new SehawqDB();
|
|
23
67
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
68
|
+
// Basic operations
|
|
69
|
+
db.set('user', { name: 'John', age: 25 });
|
|
70
|
+
console.log(db.get('user')); // { name: 'John', age: 25 }
|
|
27
71
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
72
|
+
// Query system
|
|
73
|
+
db.set('user1', { name: 'Alice', score: 95 });
|
|
74
|
+
db.set('user2', { name: 'Bob', score: 87 });
|
|
31
75
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
- `push` / `pull` — Triggered on array modification.
|
|
38
|
-
- `add` — Triggered on numeric increment.
|
|
39
|
-
- `backup` / `restore` — Triggered on backup or restore.
|
|
76
|
+
const topUsers = db.find()
|
|
77
|
+
.sort('score', 'desc')
|
|
78
|
+
.limit(2)
|
|
79
|
+
.values();
|
|
80
|
+
```
|
|
40
81
|
|
|
41
82
|
---
|
|
42
83
|
|
|
43
|
-
##
|
|
84
|
+
## 🌐 REST API Server
|
|
85
|
+
|
|
86
|
+
### Start Server
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
const SehawqDB = require('sehawq.db');
|
|
90
|
+
|
|
91
|
+
const db = new SehawqDB({
|
|
92
|
+
path: './database.json',
|
|
93
|
+
enableServer: true, // Enable REST API
|
|
94
|
+
serverPort: 3000,
|
|
95
|
+
enableRealtime: true, // Enable WebSocket
|
|
96
|
+
apiKey: 'your-secret-key' // Optional authentication
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Or start manually:
|
|
100
|
+
// await db.startServer(3000);
|
|
101
|
+
|
|
102
|
+
// 🚀 Server is now running on http://localhost:3000
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### API Endpoints
|
|
44
106
|
|
|
107
|
+
#### Health Check
|
|
45
108
|
```bash
|
|
46
|
-
|
|
109
|
+
GET /api/health
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
#### Get All Data
|
|
113
|
+
```bash
|
|
114
|
+
GET /api/data
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
#### Get by Key
|
|
118
|
+
```bash
|
|
119
|
+
GET /api/data/:key
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Set Data
|
|
123
|
+
```bash
|
|
124
|
+
POST /api/data/:key
|
|
125
|
+
Content-Type: application/json
|
|
126
|
+
|
|
127
|
+
{
|
|
128
|
+
"value": { "name": "John", "age": 25 }
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
#### Update Data
|
|
133
|
+
```bash
|
|
134
|
+
PUT /api/data/:key
|
|
135
|
+
Content-Type: application/json
|
|
136
|
+
|
|
137
|
+
{
|
|
138
|
+
"value": { "name": "John", "age": 26 }
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Delete Data
|
|
143
|
+
```bash
|
|
144
|
+
DELETE /api/data/:key
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### Query with Filters
|
|
148
|
+
```bash
|
|
149
|
+
POST /api/query
|
|
150
|
+
Content-Type: application/json
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
"filter": {},
|
|
154
|
+
"sort": { "field": "age", "direction": "desc" },
|
|
155
|
+
"limit": 10,
|
|
156
|
+
"skip": 0
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Aggregations
|
|
161
|
+
```bash
|
|
162
|
+
GET /api/aggregate/count
|
|
163
|
+
GET /api/aggregate/sum?field=score
|
|
164
|
+
GET /api/aggregate/avg?field=age
|
|
165
|
+
GET /api/aggregate/min?field=price
|
|
166
|
+
GET /api/aggregate/max?field=rating
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Array Operations
|
|
170
|
+
```bash
|
|
171
|
+
POST /api/array/:key/push
|
|
172
|
+
POST /api/array/:key/pull
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Math Operations
|
|
176
|
+
```bash
|
|
177
|
+
POST /api/math/:key/add
|
|
178
|
+
POST /api/math/:key/subtract
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### API Authentication
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
// Server side
|
|
185
|
+
const db = new SehawqDB({
|
|
186
|
+
apiKey: 'my-secret-key-123'
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Client side
|
|
190
|
+
fetch('http://localhost:3000/api/data', {
|
|
191
|
+
headers: {
|
|
192
|
+
'X-API-Key': 'my-secret-key-123'
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## ⚡ Real-time Sync
|
|
200
|
+
|
|
201
|
+
### Server Setup
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
const db = new SehawqDB({
|
|
205
|
+
enableServer: true,
|
|
206
|
+
enableRealtime: true,
|
|
207
|
+
serverPort: 3000
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Listen to client events
|
|
211
|
+
db.on('client:connected', ({ socketId }) => {
|
|
212
|
+
console.log('Client connected:', socketId);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
db.on('client:disconnected', ({ socketId }) => {
|
|
216
|
+
console.log('Client disconnected:', socketId);
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Frontend (React Example)
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
import { io } from 'socket.io-client';
|
|
224
|
+
import { useEffect, useState } from 'react';
|
|
225
|
+
|
|
226
|
+
function App() {
|
|
227
|
+
const [data, setData] = useState({});
|
|
228
|
+
const socket = io('http://localhost:3000');
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
// Receive initial data
|
|
232
|
+
socket.on('data:init', (initialData) => {
|
|
233
|
+
setData(initialData);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// Listen to real-time changes
|
|
237
|
+
socket.on('data:changed', ({ action, key, value }) => {
|
|
238
|
+
console.log(`Data ${action}:`, key, value);
|
|
239
|
+
|
|
240
|
+
if (action === 'set') {
|
|
241
|
+
setData(prev => ({ ...prev, [key]:
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const fs = require("fs").promises;
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const EventEmitter = require("events");
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const express = require("express");
|
|
6
|
+
const { Server } = require("socket.io");
|
|
7
|
+
const cors = require("cors");
|
|
4
8
|
|
|
5
9
|
class SehawqDB extends EventEmitter {
|
|
6
10
|
/**
|
|
@@ -8,18 +12,38 @@ class SehawqDB extends EventEmitter {
|
|
|
8
12
|
* @param {Object} options
|
|
9
13
|
* @param {string} [options.path="sehawq.json"] File path for storage.
|
|
10
14
|
* @param {number} [options.autoSaveInterval=0] Autosave interval in ms (0 disables autosave).
|
|
15
|
+
* @param {boolean} [options.enableServer=false] Enable REST API server
|
|
16
|
+
* @param {number} [options.serverPort=3000] Server port
|
|
17
|
+
* @param {boolean} [options.enableRealtime=true] Enable real-time sync via WebSocket
|
|
18
|
+
* @param {string} [options.apiKey] Optional API key for authentication
|
|
11
19
|
*/
|
|
12
20
|
constructor(options = {}) {
|
|
13
21
|
super();
|
|
14
22
|
this.filePath = path.resolve(options.path || "sehawq.json");
|
|
15
23
|
this.autoSaveInterval = options.autoSaveInterval || 0;
|
|
16
24
|
this.data = {};
|
|
25
|
+
|
|
26
|
+
// Server options
|
|
27
|
+
this.enableServer = options.enableServer || false;
|
|
28
|
+
this.serverPort = options.serverPort || 3000;
|
|
29
|
+
this.enableRealtime = options.enableRealtime !== false;
|
|
30
|
+
this.apiKey = options.apiKey || null;
|
|
31
|
+
|
|
32
|
+
// Server instances
|
|
33
|
+
this.app = null;
|
|
34
|
+
this.server = null;
|
|
35
|
+
this.io = null;
|
|
36
|
+
this.isServerRunning = false;
|
|
17
37
|
|
|
18
38
|
this._init();
|
|
19
39
|
|
|
20
40
|
if (this.autoSaveInterval > 0) {
|
|
21
41
|
this._interval = setInterval(() => this.save(), this.autoSaveInterval);
|
|
22
42
|
}
|
|
43
|
+
|
|
44
|
+
if (this.enableServer) {
|
|
45
|
+
this.startServer(this.serverPort);
|
|
46
|
+
}
|
|
23
47
|
}
|
|
24
48
|
|
|
25
49
|
async _init() {
|
|
@@ -41,6 +65,17 @@ class SehawqDB extends EventEmitter {
|
|
|
41
65
|
set(key, value) {
|
|
42
66
|
this._setByPath(key, value);
|
|
43
67
|
this.emit("set", { key, value });
|
|
68
|
+
|
|
69
|
+
// Real-time broadcast
|
|
70
|
+
if (this.io) {
|
|
71
|
+
this.io.emit("data:changed", {
|
|
72
|
+
action: "set",
|
|
73
|
+
key,
|
|
74
|
+
value,
|
|
75
|
+
timestamp: Date.now()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
44
79
|
this.save();
|
|
45
80
|
return value;
|
|
46
81
|
}
|
|
@@ -52,6 +87,16 @@ class SehawqDB extends EventEmitter {
|
|
|
52
87
|
delete(key) {
|
|
53
88
|
this._deleteByPath(key);
|
|
54
89
|
this.emit("delete", { key });
|
|
90
|
+
|
|
91
|
+
// Real-time broadcast
|
|
92
|
+
if (this.io) {
|
|
93
|
+
this.io.emit("data:changed", {
|
|
94
|
+
action: "delete",
|
|
95
|
+
key,
|
|
96
|
+
timestamp: Date.now()
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
this.save();
|
|
56
101
|
}
|
|
57
102
|
|
|
@@ -66,6 +111,15 @@ class SehawqDB extends EventEmitter {
|
|
|
66
111
|
clear() {
|
|
67
112
|
this.data = {};
|
|
68
113
|
this.emit("clear");
|
|
114
|
+
|
|
115
|
+
// Real-time broadcast
|
|
116
|
+
if (this.io) {
|
|
117
|
+
this.io.emit("data:changed", {
|
|
118
|
+
action: "clear",
|
|
119
|
+
timestamp: Date.now()
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
69
123
|
this.save();
|
|
70
124
|
}
|
|
71
125
|
|
|
@@ -84,6 +138,17 @@ class SehawqDB extends EventEmitter {
|
|
|
84
138
|
arr.push(value);
|
|
85
139
|
this._setByPath(key, arr);
|
|
86
140
|
this.emit("push", { key, value });
|
|
141
|
+
|
|
142
|
+
// Real-time broadcast
|
|
143
|
+
if (this.io) {
|
|
144
|
+
this.io.emit("data:changed", {
|
|
145
|
+
action: "push",
|
|
146
|
+
key,
|
|
147
|
+
value,
|
|
148
|
+
timestamp: Date.now()
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
87
152
|
this.save();
|
|
88
153
|
return arr;
|
|
89
154
|
}
|
|
@@ -94,6 +159,17 @@ class SehawqDB extends EventEmitter {
|
|
|
94
159
|
arr = arr.filter(v => v !== value);
|
|
95
160
|
this._setByPath(key, arr);
|
|
96
161
|
this.emit("pull", { key, value });
|
|
162
|
+
|
|
163
|
+
// Real-time broadcast
|
|
164
|
+
if (this.io) {
|
|
165
|
+
this.io.emit("data:changed", {
|
|
166
|
+
action: "pull",
|
|
167
|
+
key,
|
|
168
|
+
value,
|
|
169
|
+
timestamp: Date.now()
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
97
173
|
this.save();
|
|
98
174
|
return arr;
|
|
99
175
|
}
|
|
@@ -105,6 +181,18 @@ class SehawqDB extends EventEmitter {
|
|
|
105
181
|
val += number;
|
|
106
182
|
this._setByPath(key, val);
|
|
107
183
|
this.emit("add", { key, number });
|
|
184
|
+
|
|
185
|
+
// Real-time broadcast
|
|
186
|
+
if (this.io) {
|
|
187
|
+
this.io.emit("data:changed", {
|
|
188
|
+
action: "add",
|
|
189
|
+
key,
|
|
190
|
+
number,
|
|
191
|
+
newValue: val,
|
|
192
|
+
timestamp: Date.now()
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
108
196
|
this.save();
|
|
109
197
|
return val;
|
|
110
198
|
}
|
|
@@ -113,6 +201,415 @@ class SehawqDB extends EventEmitter {
|
|
|
113
201
|
return this.add(key, -number);
|
|
114
202
|
}
|
|
115
203
|
|
|
204
|
+
// ---------------- Query System ----------------
|
|
205
|
+
|
|
206
|
+
find(filter) {
|
|
207
|
+
const results = [];
|
|
208
|
+
|
|
209
|
+
if (typeof filter === 'function') {
|
|
210
|
+
for (const [key, value] of Object.entries(this.data)) {
|
|
211
|
+
if (filter(value, key)) {
|
|
212
|
+
results.push({ key, value });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
for (const [key, value] of Object.entries(this.data)) {
|
|
217
|
+
results.push({ key, value });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return new QueryResult(results);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
findOne(filter) {
|
|
225
|
+
if (typeof filter === 'function') {
|
|
226
|
+
for (const [key, value] of Object.entries(this.data)) {
|
|
227
|
+
if (filter(value, key)) {
|
|
228
|
+
return { key, value };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
where(field, operator, value) {
|
|
236
|
+
return this.find((item, key) => {
|
|
237
|
+
const fieldValue = this._getValueByPath(item, field);
|
|
238
|
+
|
|
239
|
+
switch (operator) {
|
|
240
|
+
case '=':
|
|
241
|
+
case '==':
|
|
242
|
+
return fieldValue === value;
|
|
243
|
+
case '!=':
|
|
244
|
+
return fieldValue !== value;
|
|
245
|
+
case '>':
|
|
246
|
+
return fieldValue > value;
|
|
247
|
+
case '<':
|
|
248
|
+
return fieldValue < value;
|
|
249
|
+
case '>=':
|
|
250
|
+
return fieldValue >= value;
|
|
251
|
+
case '<=':
|
|
252
|
+
return fieldValue <= value;
|
|
253
|
+
case 'in':
|
|
254
|
+
return Array.isArray(value) && value.includes(fieldValue);
|
|
255
|
+
case 'contains':
|
|
256
|
+
return typeof fieldValue === 'string' && fieldValue.includes(value);
|
|
257
|
+
case 'startsWith':
|
|
258
|
+
return typeof fieldValue === 'string' && fieldValue.startsWith(value);
|
|
259
|
+
case 'endsWith':
|
|
260
|
+
return typeof fieldValue === 'string' && fieldValue.endsWith(value);
|
|
261
|
+
default:
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ---------------- Aggregation System ----------------
|
|
268
|
+
|
|
269
|
+
count(filter) {
|
|
270
|
+
if (filter) {
|
|
271
|
+
return this.find(filter).count();
|
|
272
|
+
}
|
|
273
|
+
return Object.keys(this.data).length;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
sum(field, filter) {
|
|
277
|
+
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
278
|
+
return items.reduce((sum, item) => {
|
|
279
|
+
const val = this._getValueByPath(item.value, field);
|
|
280
|
+
return sum + (typeof val === 'number' ? val : 0);
|
|
281
|
+
}, 0);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
avg(field, filter) {
|
|
285
|
+
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
286
|
+
if (items.length === 0) return 0;
|
|
287
|
+
|
|
288
|
+
const sum = items.reduce((total, item) => {
|
|
289
|
+
const val = this._getValueByPath(item.value, field);
|
|
290
|
+
return total + (typeof val === 'number' ? val : 0);
|
|
291
|
+
}, 0);
|
|
292
|
+
|
|
293
|
+
return sum / items.length;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
min(field, filter) {
|
|
297
|
+
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
298
|
+
if (items.length === 0) return undefined;
|
|
299
|
+
|
|
300
|
+
return Math.min(...items.map(item => {
|
|
301
|
+
const val = this._getValueByPath(item.value, field);
|
|
302
|
+
return typeof val === 'number' ? val : Infinity;
|
|
303
|
+
}).filter(val => val !== Infinity));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
max(field, filter) {
|
|
307
|
+
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
308
|
+
if (items.length === 0) return undefined;
|
|
309
|
+
|
|
310
|
+
return Math.max(...items.map(item => {
|
|
311
|
+
const val = this._getValueByPath(item.value, field);
|
|
312
|
+
return typeof val === 'number' ? val : -Infinity;
|
|
313
|
+
}).filter(val => val !== -Infinity));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
groupBy(field, filter) {
|
|
317
|
+
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
318
|
+
const groups = {};
|
|
319
|
+
|
|
320
|
+
items.forEach(item => {
|
|
321
|
+
const groupKey = this._getValueByPath(item.value, field);
|
|
322
|
+
const key = groupKey !== undefined ? String(groupKey) : 'undefined';
|
|
323
|
+
|
|
324
|
+
if (!groups[key]) {
|
|
325
|
+
groups[key] = [];
|
|
326
|
+
}
|
|
327
|
+
groups[key].push(item);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return groups;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---------------- REST API Server ----------------
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Start REST API server with real-time sync
|
|
337
|
+
* @param {number} port - Server port
|
|
338
|
+
* @returns {Promise<void>}
|
|
339
|
+
*/
|
|
340
|
+
async startServer(port = 3000) {
|
|
341
|
+
if (this.isServerRunning) {
|
|
342
|
+
console.log(`⚠️ Server is already running on port ${this.serverPort}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.app = express();
|
|
347
|
+
this.server = http.createServer(this.app);
|
|
348
|
+
|
|
349
|
+
// Enable real-time if requested
|
|
350
|
+
if (this.enableRealtime) {
|
|
351
|
+
this.io = new Server(this.server, {
|
|
352
|
+
cors: {
|
|
353
|
+
origin: "*",
|
|
354
|
+
methods: ["GET", "POST", "PUT", "DELETE"]
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
this._setupWebSocket();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Middleware
|
|
362
|
+
this.app.use(cors());
|
|
363
|
+
this.app.use(express.json());
|
|
364
|
+
|
|
365
|
+
// API Key middleware
|
|
366
|
+
if (this.apiKey) {
|
|
367
|
+
this.app.use((req, res, next) => {
|
|
368
|
+
const key = req.headers['x-api-key'];
|
|
369
|
+
if (key !== this.apiKey) {
|
|
370
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
371
|
+
}
|
|
372
|
+
next();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this._setupRoutes();
|
|
377
|
+
|
|
378
|
+
return new Promise((resolve, reject) => {
|
|
379
|
+
this.server.listen(port, () => {
|
|
380
|
+
this.isServerRunning = true;
|
|
381
|
+
this.serverPort = port;
|
|
382
|
+
console.log(`🚀 SehawqDB Server running on http://localhost:${port}`);
|
|
383
|
+
console.log(`📡 Real-time sync: ${this.enableRealtime ? 'ENABLED' : 'DISABLED'}`);
|
|
384
|
+
console.log(`🔒 API Key: ${this.apiKey ? 'ENABLED' : 'DISABLED'}`);
|
|
385
|
+
this.emit('server:started', { port });
|
|
386
|
+
resolve();
|
|
387
|
+
}).on('error', reject);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Stop the REST API server
|
|
393
|
+
*/
|
|
394
|
+
stopServer() {
|
|
395
|
+
if (!this.isServerRunning) return;
|
|
396
|
+
|
|
397
|
+
if (this.io) this.io.close();
|
|
398
|
+
if (this.server) this.server.close();
|
|
399
|
+
|
|
400
|
+
this.isServerRunning = false;
|
|
401
|
+
console.log('🛑 SehawqDB Server stopped');
|
|
402
|
+
this.emit('server:stopped');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_setupRoutes() {
|
|
406
|
+
const router = express.Router();
|
|
407
|
+
|
|
408
|
+
// Health check
|
|
409
|
+
router.get('/health', (req, res) => {
|
|
410
|
+
res.json({
|
|
411
|
+
status: 'ok',
|
|
412
|
+
uptime: process.uptime(),
|
|
413
|
+
realtime: this.enableRealtime,
|
|
414
|
+
dataSize: Object.keys(this.data).length
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// Get all data
|
|
419
|
+
router.get('/data', (req, res) => {
|
|
420
|
+
res.json({ success: true, data: this.data });
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
// Get by key
|
|
424
|
+
router.get('/data/:key', (req, res) => {
|
|
425
|
+
const { key } = req.params;
|
|
426
|
+
const value = this.get(key);
|
|
427
|
+
|
|
428
|
+
if (value === undefined) {
|
|
429
|
+
return res.status(404).json({ success: false, error: 'Key not found' });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
res.json({ success: true, key, value });
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// Set data
|
|
436
|
+
router.post('/data/:key', (req, res) => {
|
|
437
|
+
const { key } = req.params;
|
|
438
|
+
const { value } = req.body;
|
|
439
|
+
|
|
440
|
+
if (value === undefined) {
|
|
441
|
+
return res.status(400).json({ success: false, error: 'Value is required' });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const result = this.set(key, value);
|
|
445
|
+
res.json({ success: true, key, value: result });
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Update data (alias for set)
|
|
449
|
+
router.put('/data/:key', (req, res) => {
|
|
450
|
+
const { key } = req.params;
|
|
451
|
+
const { value } = req.body;
|
|
452
|
+
|
|
453
|
+
if (value === undefined) {
|
|
454
|
+
return res.status(400).json({ success: false, error: 'Value is required' });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const result = this.set(key, value);
|
|
458
|
+
res.json({ success: true, key, value: result });
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Delete data
|
|
462
|
+
router.delete('/data/:key', (req, res) => {
|
|
463
|
+
const { key } = req.params;
|
|
464
|
+
|
|
465
|
+
if (!this.has(key)) {
|
|
466
|
+
return res.status(404).json({ success: false, error: 'Key not found' });
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
this.delete(key);
|
|
470
|
+
res.json({ success: true, key });
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Query with find
|
|
474
|
+
router.post('/query', (req, res) => {
|
|
475
|
+
try {
|
|
476
|
+
const { filter, sort, limit, skip } = req.body;
|
|
477
|
+
|
|
478
|
+
let query = this.find();
|
|
479
|
+
|
|
480
|
+
// Apply sorting
|
|
481
|
+
if (sort && sort.field) {
|
|
482
|
+
query = query.sort(sort.field, sort.direction || 'asc');
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Apply pagination
|
|
486
|
+
if (skip) query = query.skip(skip);
|
|
487
|
+
if (limit) query = query.limit(limit);
|
|
488
|
+
|
|
489
|
+
const results = query.toArray();
|
|
490
|
+
|
|
491
|
+
res.json({
|
|
492
|
+
success: true,
|
|
493
|
+
results,
|
|
494
|
+
count: results.length
|
|
495
|
+
});
|
|
496
|
+
} catch (error) {
|
|
497
|
+
res.status(400).json({ success: false, error: error.message });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
// Aggregation
|
|
502
|
+
router.get('/aggregate/:operation', (req, res) => {
|
|
503
|
+
try {
|
|
504
|
+
const { operation } = req.params;
|
|
505
|
+
const { field } = req.query;
|
|
506
|
+
|
|
507
|
+
let result;
|
|
508
|
+
|
|
509
|
+
switch (operation) {
|
|
510
|
+
case 'count':
|
|
511
|
+
result = this.count();
|
|
512
|
+
break;
|
|
513
|
+
case 'sum':
|
|
514
|
+
if (!field) return res.status(400).json({ error: 'Field is required' });
|
|
515
|
+
result = this.sum(field);
|
|
516
|
+
break;
|
|
517
|
+
case 'avg':
|
|
518
|
+
if (!field) return res.status(400).json({ error: 'Field is required' });
|
|
519
|
+
result = this.avg(field);
|
|
520
|
+
break;
|
|
521
|
+
case 'min':
|
|
522
|
+
if (!field) return res.status(400).json({ error: 'Field is required' });
|
|
523
|
+
result = this.min(field);
|
|
524
|
+
break;
|
|
525
|
+
case 'max':
|
|
526
|
+
if (!field) return res.status(400).json({ error: 'Field is required' });
|
|
527
|
+
result = this.max(field);
|
|
528
|
+
break;
|
|
529
|
+
default:
|
|
530
|
+
return res.status(400).json({ error: 'Invalid operation' });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
res.json({ success: true, operation, field, result });
|
|
534
|
+
} catch (error) {
|
|
535
|
+
res.status(400).json({ success: false, error: error.message });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// Array operations
|
|
540
|
+
router.post('/array/:key/push', (req, res) => {
|
|
541
|
+
const { key } = req.params;
|
|
542
|
+
const { value } = req.body;
|
|
543
|
+
|
|
544
|
+
const result = this.push(key, value);
|
|
545
|
+
res.json({ success: true, key, value: result });
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
router.post('/array/:key/pull', (req, res) => {
|
|
549
|
+
const { key } = req.params;
|
|
550
|
+
const { value } = req.body;
|
|
551
|
+
|
|
552
|
+
const result = this.pull(key, value);
|
|
553
|
+
res.json({ success: true, key, value: result });
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// Math operations
|
|
557
|
+
router.post('/math/:key/add', (req, res) => {
|
|
558
|
+
const { key } = req.params;
|
|
559
|
+
const { number } = req.body;
|
|
560
|
+
|
|
561
|
+
if (typeof number !== 'number') {
|
|
562
|
+
return res.status(400).json({ error: 'Number is required' });
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const result = this.add(key, number);
|
|
566
|
+
res.json({ success: true, key, value: result });
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
router.post('/math/:key/subtract', (req, res) => {
|
|
570
|
+
const { key } = req.params;
|
|
571
|
+
const { number } = req.body;
|
|
572
|
+
|
|
573
|
+
if (typeof number !== 'number') {
|
|
574
|
+
return res.status(400).json({ error: 'Number is required' });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const result = this.subtract(key, number);
|
|
578
|
+
res.json({ success: true, key, value: result });
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
this.app.use('/api', router);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
_setupWebSocket() {
|
|
585
|
+
this.io.on('connection', (socket) => {
|
|
586
|
+
console.log(`✅ Client connected: ${socket.id}`);
|
|
587
|
+
this.emit('client:connected', { socketId: socket.id });
|
|
588
|
+
|
|
589
|
+
// Send current data on connection
|
|
590
|
+
socket.emit('data:init', this.data);
|
|
591
|
+
|
|
592
|
+
// Handle client operations
|
|
593
|
+
socket.on('data:set', ({ key, value }) => {
|
|
594
|
+
this.set(key, value);
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
socket.on('data:delete', ({ key }) => {
|
|
598
|
+
this.delete(key);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
socket.on('data:get', ({ key }, callback) => {
|
|
602
|
+
const value = this.get(key);
|
|
603
|
+
callback({ success: true, value });
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
socket.on('disconnect', () => {
|
|
607
|
+
console.log(`❌ Client disconnected: ${socket.id}`);
|
|
608
|
+
this.emit('client:disconnected', { socketId: socket.id });
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
116
613
|
// ---------------- Backup & Restore ----------------
|
|
117
614
|
async backup(backupPath) {
|
|
118
615
|
await fs.writeFile(backupPath, JSON.stringify(this.data, null, 2), "utf8");
|
|
@@ -168,6 +665,96 @@ class SehawqDB extends EventEmitter {
|
|
|
168
665
|
}
|
|
169
666
|
delete obj[keys[0]];
|
|
170
667
|
}
|
|
668
|
+
|
|
669
|
+
_getValueByPath(obj, pathStr) {
|
|
670
|
+
const keys = pathStr.split(".");
|
|
671
|
+
let result = obj;
|
|
672
|
+
for (const key of keys) {
|
|
673
|
+
if (result && Object.prototype.hasOwnProperty.call(result, key)) {
|
|
674
|
+
result = result[key];
|
|
675
|
+
} else {
|
|
676
|
+
return undefined;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ---------------- QueryResult Class ----------------
|
|
684
|
+
class QueryResult {
|
|
685
|
+
constructor(results) {
|
|
686
|
+
this.results = results || [];
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
sort(field, direction = 'asc') {
|
|
690
|
+
this.results.sort((a, b) => {
|
|
691
|
+
const aVal = this._getValueByPath(a.value, field);
|
|
692
|
+
const bVal = this._getValueByPath(b.value, field);
|
|
693
|
+
|
|
694
|
+
if (aVal === bVal) return 0;
|
|
695
|
+
|
|
696
|
+
const comparison = aVal > bVal ? 1 : -1;
|
|
697
|
+
return direction === 'desc' ? -comparison : comparison;
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
return this;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
limit(count) {
|
|
704
|
+
this.results = this.results.slice(0, count);
|
|
705
|
+
return this;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
skip(count) {
|
|
709
|
+
this.results = this.results.slice(count);
|
|
710
|
+
return this;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
count() {
|
|
714
|
+
return this.results.length;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
first() {
|
|
718
|
+
return this.results[0];
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
last() {
|
|
722
|
+
return this.results[this.results.length - 1];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
toArray() {
|
|
726
|
+
return this.results;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
values() {
|
|
730
|
+
return this.results.map(item => item.value);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
keys() {
|
|
734
|
+
return this.results.map(item => item.key);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
filter(filter) {
|
|
738
|
+
this.results = this.results.filter(item => filter(item.value, item.key));
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
map(mapper) {
|
|
743
|
+
return this.results.map(item => mapper(item.value, item.key));
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
_getValueByPath(obj, pathStr) {
|
|
747
|
+
const keys = pathStr.split(".");
|
|
748
|
+
let result = obj;
|
|
749
|
+
for (const key of keys) {
|
|
750
|
+
if (result && Object.prototype.hasOwnProperty.call(result, key)) {
|
|
751
|
+
result = result[key];
|
|
752
|
+
} else {
|
|
753
|
+
return undefined;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return result;
|
|
757
|
+
}
|
|
171
758
|
}
|
|
172
759
|
|
|
173
|
-
module.exports = SehawqDB;
|
|
760
|
+
module.exports = SehawqDB;
|