sehawq.db 2.4.2 → 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 +172 -237
- package/src/index.js +371 -106
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,306 +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!
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- **🔥 NEW: Advanced Query System** — Filter, sort, and paginate your data.
|
|
20
|
-
- **🔥 NEW: Aggregation Functions** — Calculate sum, average, min, max, and more.
|
|
21
|
-
- **🔥 NEW: Method Chaining** — Chain operations for complex queries.
|
|
22
|
-
|
|
23
|
-
### 🔍 Query System
|
|
24
|
-
- `find(filter)` — Find all entries matching a filter function.
|
|
25
|
-
- `findOne(filter)` — Find the first entry matching a filter.
|
|
26
|
-
- `where(field, operator, value)` — Filter by field with operators (`>`, `<`, `>=`, `<=`, `=`, `!=`, `in`, `contains`, `startsWith`, `endsWith`).
|
|
27
|
-
|
|
28
|
-
### 📊 Aggregation Functions
|
|
29
|
-
- `count(filter)` — Count entries (with optional filter).
|
|
30
|
-
- `sum(field)` — Sum numeric values by field.
|
|
31
|
-
- `avg(field)` — Calculate average of numeric values.
|
|
32
|
-
- `min(field)` / `max(field)` — Find minimum/maximum values.
|
|
33
|
-
- `groupBy(field)` — Group entries by field value.
|
|
34
|
-
|
|
35
|
-
### ⛓️ Method Chaining & Pagination
|
|
36
|
-
- `sort(field, direction)` — Sort results by field (`'asc'` or `'desc'`).
|
|
37
|
-
- `limit(count)` — Limit number of results.
|
|
38
|
-
- `skip(count)` — Skip number of results for pagination.
|
|
39
|
-
- `first()` / `last()` — Get first or last result.
|
|
40
|
-
- `values()` / `keys()` — Extract values or keys only.
|
|
41
|
-
|
|
42
|
-
### 🔧 Array Helpers
|
|
43
|
-
- `push(key, value)` — Add an element to an array.
|
|
44
|
-
- `pull(key, value)` — Remove an element from an array.
|
|
45
|
-
|
|
46
|
-
### ➗ Math Helpers
|
|
47
|
-
- `add(key, number)` — Increment a numeric value.
|
|
48
|
-
- `subtract(key, number)` — Decrement a numeric value.
|
|
49
|
-
|
|
50
|
-
### 💾 Backup & Restore
|
|
51
|
-
- `backup(filePath)` — Save a backup of the database.
|
|
52
|
-
- `restore(filePath)` — Restore database from a backup.
|
|
53
|
-
|
|
54
|
-
### 📡 Event Emitter
|
|
55
|
-
Hooks into database operations:
|
|
56
|
-
- `set` — Triggered when data is added or updated.
|
|
57
|
-
- `delete` — Triggered when a key is removed.
|
|
58
|
-
- `clear` — Triggered when all data is cleared.
|
|
59
|
-
- `push` / `pull` — Triggered on array modification.
|
|
60
|
-
- `add` — Triggered on numeric increment.
|
|
61
|
-
- `backup` / `restore` — Triggered on backup or restore.
|
|
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!**
|
|
62
21
|
|
|
63
22
|
---
|
|
64
23
|
|
|
65
|
-
##
|
|
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
|
|
66
55
|
|
|
67
56
|
```bash
|
|
68
|
-
npm install sehawq.db
|
|
57
|
+
npm install sehawq.db express socket.io socket.io-client cors
|
|
69
58
|
```
|
|
70
59
|
|
|
71
60
|
---
|
|
72
61
|
|
|
73
|
-
## ⚡ Quick Start (
|
|
62
|
+
## ⚡ Quick Start (Local Database)
|
|
74
63
|
|
|
75
64
|
```javascript
|
|
76
|
-
const
|
|
65
|
+
const SehawqDB = require('sehawq.db');
|
|
66
|
+
const db = new SehawqDB();
|
|
77
67
|
|
|
78
|
-
//
|
|
79
|
-
db.set('user', 'John
|
|
80
|
-
db.
|
|
68
|
+
// Basic operations
|
|
69
|
+
db.set('user', { name: 'John', age: 25 });
|
|
70
|
+
console.log(db.get('user')); // { name: 'John', age: 25 }
|
|
81
71
|
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
72
|
+
// Query system
|
|
73
|
+
db.set('user1', { name: 'Alice', score: 95 });
|
|
74
|
+
db.set('user2', { name: 'Bob', score: 87 });
|
|
85
75
|
|
|
86
|
-
|
|
76
|
+
const topUsers = db.find()
|
|
77
|
+
.sort('score', 'desc')
|
|
78
|
+
.limit(2)
|
|
79
|
+
.values();
|
|
87
80
|
```
|
|
88
81
|
|
|
89
82
|
---
|
|
90
83
|
|
|
91
|
-
##
|
|
84
|
+
## 🌐 REST API Server
|
|
92
85
|
|
|
93
|
-
###
|
|
86
|
+
### Start Server
|
|
94
87
|
|
|
95
88
|
```javascript
|
|
96
89
|
const SehawqDB = require('sehawq.db');
|
|
90
|
+
|
|
97
91
|
const db = new SehawqDB({
|
|
98
|
-
path: './
|
|
99
|
-
|
|
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
|
|
100
97
|
});
|
|
101
98
|
|
|
102
|
-
//
|
|
103
|
-
db.
|
|
104
|
-
db.set('user.123.balance', 1000);
|
|
105
|
-
console.log(db.get('user.123')); // { name: 'John Doe', balance: 1000 }
|
|
106
|
-
|
|
107
|
-
// Check if key exists
|
|
108
|
-
if (db.has('user.123')) {
|
|
109
|
-
console.log('User exists!');
|
|
110
|
-
}
|
|
99
|
+
// Or start manually:
|
|
100
|
+
// await db.startServer(3000);
|
|
111
101
|
|
|
112
|
-
//
|
|
113
|
-
db.delete('user.123.balance');
|
|
102
|
+
// 🚀 Server is now running on http://localhost:3000
|
|
114
103
|
```
|
|
115
104
|
|
|
116
|
-
###
|
|
117
|
-
|
|
118
|
-
```javascript
|
|
119
|
-
// Initialize an array
|
|
120
|
-
db.set('users', []);
|
|
121
|
-
|
|
122
|
-
// Add items
|
|
123
|
-
db.push('users', { id: 1, name: 'Alice' });
|
|
124
|
-
db.push('users', { id: 2, name: 'Bob' });
|
|
125
|
-
|
|
126
|
-
// Remove items
|
|
127
|
-
db.pull('users', { id: 1, name: 'Alice' });
|
|
105
|
+
### API Endpoints
|
|
128
106
|
|
|
129
|
-
|
|
107
|
+
#### Health Check
|
|
108
|
+
```bash
|
|
109
|
+
GET /api/health
|
|
130
110
|
```
|
|
131
111
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
db.set('score', 100);
|
|
136
|
-
db.add('score', 50); // score = 150
|
|
137
|
-
db.subtract('score', 20); // score = 130
|
|
138
|
-
console.log(db.get('score')); // 130
|
|
112
|
+
#### Get All Data
|
|
113
|
+
```bash
|
|
114
|
+
GET /api/data
|
|
139
115
|
```
|
|
140
116
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Sample data
|
|
145
|
-
db.set('user1', { name: 'Alice', age: 25, active: true, score: 95 });
|
|
146
|
-
db.set('user2', { name: 'Bob', age: 30, active: false, score: 87 });
|
|
147
|
-
db.set('user3', { name: 'Charlie', age: 22, active: true, score: 92 });
|
|
148
|
-
|
|
149
|
-
// Find all active users
|
|
150
|
-
const activeUsers = db.find(user => user.active).values();
|
|
151
|
-
console.log(activeUsers);
|
|
152
|
-
|
|
153
|
-
// Find users older than 24
|
|
154
|
-
const olderUsers = db.where('age', '>', 24).values();
|
|
155
|
-
|
|
156
|
-
// Complex query with chaining
|
|
157
|
-
const topActiveUsers = db
|
|
158
|
-
.find(user => user.active)
|
|
159
|
-
.sort('score', 'desc')
|
|
160
|
-
.limit(2)
|
|
161
|
-
.values();
|
|
162
|
-
|
|
163
|
-
console.log(topActiveUsers); // Top 2 active users by score
|
|
117
|
+
#### Get by Key
|
|
118
|
+
```bash
|
|
119
|
+
GET /api/data/:key
|
|
164
120
|
```
|
|
165
121
|
|
|
166
|
-
|
|
122
|
+
#### Set Data
|
|
123
|
+
```bash
|
|
124
|
+
POST /api/data/:key
|
|
125
|
+
Content-Type: application/json
|
|
167
126
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
127
|
+
{
|
|
128
|
+
"value": { "name": "John", "age": 25 }
|
|
129
|
+
}
|
|
130
|
+
```
|
|
171
131
|
|
|
172
|
-
|
|
173
|
-
|
|
132
|
+
#### Update Data
|
|
133
|
+
```bash
|
|
134
|
+
PUT /api/data/:key
|
|
135
|
+
Content-Type: application/json
|
|
174
136
|
|
|
175
|
-
|
|
176
|
-
|
|
137
|
+
{
|
|
138
|
+
"value": { "name": "John", "age": 26 }
|
|
139
|
+
}
|
|
140
|
+
```
|
|
177
141
|
|
|
178
|
-
|
|
179
|
-
|
|
142
|
+
#### Delete Data
|
|
143
|
+
```bash
|
|
144
|
+
DELETE /api/data/:key
|
|
145
|
+
```
|
|
180
146
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
}
|
|
188
158
|
```
|
|
189
159
|
|
|
190
|
-
|
|
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
|
+
```
|
|
191
168
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
.limit(10)
|
|
198
|
-
.values();
|
|
169
|
+
#### Array Operations
|
|
170
|
+
```bash
|
|
171
|
+
POST /api/array/:key/push
|
|
172
|
+
POST /api/array/:key/pull
|
|
173
|
+
```
|
|
199
174
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.skip(20)
|
|
205
|
-
.limit(5)
|
|
206
|
-
.values();
|
|
175
|
+
#### Math Operations
|
|
176
|
+
```bash
|
|
177
|
+
POST /api/math/:key/add
|
|
178
|
+
POST /api/math/:key/subtract
|
|
207
179
|
```
|
|
208
180
|
|
|
209
|
-
###
|
|
181
|
+
### API Authentication
|
|
210
182
|
|
|
211
183
|
```javascript
|
|
212
|
-
//
|
|
213
|
-
db
|
|
214
|
-
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
db.on('delete', (data) => {
|
|
218
|
-
console.log(`Deleted: ${data.key}`);
|
|
184
|
+
// Server side
|
|
185
|
+
const db = new SehawqDB({
|
|
186
|
+
apiKey: 'my-secret-key-123'
|
|
219
187
|
});
|
|
220
188
|
|
|
221
|
-
|
|
222
|
-
|
|
189
|
+
// Client side
|
|
190
|
+
fetch('http://localhost:3000/api/data', {
|
|
191
|
+
headers: {
|
|
192
|
+
'X-API-Key': 'my-secret-key-123'
|
|
193
|
+
}
|
|
223
194
|
});
|
|
224
195
|
```
|
|
225
196
|
|
|
226
|
-
### Backup & Restore
|
|
227
|
-
|
|
228
|
-
```javascript
|
|
229
|
-
// Create backup
|
|
230
|
-
await db.backup('./backup.json');
|
|
231
|
-
|
|
232
|
-
// Restore from backup
|
|
233
|
-
await db.restore('./backup.json');
|
|
234
|
-
```
|
|
235
|
-
|
|
236
197
|
---
|
|
237
198
|
|
|
238
|
-
##
|
|
239
|
-
|
|
240
|
-
### Changes in 2.4.2 🔥
|
|
241
|
-
|
|
242
|
-
- ✨ **Added Query System**
|
|
243
|
-
- `find(filter)` — Filter entries with custom functions
|
|
244
|
-
- `findOne(filter)` — Find first matching entry
|
|
245
|
-
- `where(field, operator, value)` — Field-based filtering with operators
|
|
246
|
-
- **Operators**: `>`, `<`, `>=`, `<=`, `=`, `!=`, `in`, `contains`, `startsWith`, `endsWith`
|
|
247
|
-
|
|
248
|
-
- ✨ **Added Aggregation Functions**
|
|
249
|
-
- `count(filter)` — Count entries with optional filtering
|
|
250
|
-
- `sum(field)` — Sum numeric values by field
|
|
251
|
-
- `avg(field)` — Calculate average of numeric values
|
|
252
|
-
- `min(field)` / `max(field)` — Find minimum/maximum values
|
|
253
|
-
- `groupBy(field)` — Group entries by field value
|
|
254
|
-
|
|
255
|
-
- ✨ **Added Method Chaining Support**
|
|
256
|
-
- New `QueryResult` class enables chaining operations
|
|
257
|
-
- `sort(field, direction)` — Sort results ascending or descending
|
|
258
|
-
- `limit(count)` / `skip(count)` — Pagination support
|
|
259
|
-
- `first()` / `last()` — Get first or last result
|
|
260
|
-
- `values()` / `keys()` — Extract values or keys only
|
|
261
|
-
- `filter()` — Apply additional filtering
|
|
262
|
-
- `map()` — Transform results
|
|
263
|
-
|
|
264
|
-
- 🔧 **Enhanced Dot Notation**
|
|
265
|
-
- Full support for nested queries and filtering
|
|
266
|
-
- Deep object traversal for all query operations
|
|
267
|
-
|
|
268
|
-
- 📊 **Advanced Query Examples**
|
|
269
|
-
```javascript
|
|
270
|
-
// Complex chained queries
|
|
271
|
-
db.find(user => user.active)
|
|
272
|
-
.sort('score', 'desc')
|
|
273
|
-
.limit(10)
|
|
274
|
-
.values();
|
|
275
|
-
|
|
276
|
-
// Field-based filtering
|
|
277
|
-
db.where('age', '>=', 18)
|
|
278
|
-
.where('status', 'in', ['premium', 'gold'])
|
|
279
|
-
.count();
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
### Changes in 2.4.2x
|
|
283
|
-
|
|
284
|
-
- ✨ Initial release with core features
|
|
285
|
-
- 🔧 Basic CRUD operations (`set`, `get`, `delete`, `has`)
|
|
286
|
-
- 🔧 Dot notation support for nested data
|
|
287
|
-
- 🔧 Array helpers (`push`, `pull`)
|
|
288
|
-
- 🔧 Math helpers (`add`, `subtract`)
|
|
289
|
-
- 🔧 Auto-save functionality
|
|
290
|
-
- 🔧 Event emitter system
|
|
291
|
-
- 🔧 Backup & restore functionality
|
|
292
|
-
- 🔧 Atomic file operations with temporary files
|
|
293
|
-
|
|
294
|
-
---
|
|
199
|
+
## ⚡ Real-time Sync
|
|
295
200
|
|
|
296
|
-
|
|
201
|
+
### Server Setup
|
|
297
202
|
|
|
298
|
-
|
|
203
|
+
```javascript
|
|
204
|
+
const db = new SehawqDB({
|
|
205
|
+
enableServer: true,
|
|
206
|
+
enableRealtime: true,
|
|
207
|
+
serverPort: 3000
|
|
208
|
+
});
|
|
299
209
|
|
|
300
|
-
|
|
210
|
+
// Listen to client events
|
|
211
|
+
db.on('client:connected', ({ socketId }) => {
|
|
212
|
+
console.log('Client connected:', socketId);
|
|
213
|
+
});
|
|
301
214
|
|
|
302
|
-
|
|
215
|
+
db.on('client:disconnected', ({ socketId }) => {
|
|
216
|
+
console.log('Client disconnected:', socketId);
|
|
217
|
+
});
|
|
218
|
+
```
|
|
303
219
|
|
|
304
|
-
|
|
220
|
+
### Frontend (React Example)
|
|
305
221
|
|
|
306
|
-
|
|
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,13 +201,8 @@ class SehawqDB extends EventEmitter {
|
|
|
113
201
|
return this.add(key, -number);
|
|
114
202
|
}
|
|
115
203
|
|
|
116
|
-
// ----------------
|
|
204
|
+
// ---------------- Query System ----------------
|
|
117
205
|
|
|
118
|
-
/**
|
|
119
|
-
* Find all entries that match the filter function
|
|
120
|
-
* @param {Function} filter - Filter function (item, key) => boolean
|
|
121
|
-
* @returns {QueryResult} Chainable query result
|
|
122
|
-
*/
|
|
123
206
|
find(filter) {
|
|
124
207
|
const results = [];
|
|
125
208
|
|
|
@@ -130,7 +213,6 @@ class SehawqDB extends EventEmitter {
|
|
|
130
213
|
}
|
|
131
214
|
}
|
|
132
215
|
} else {
|
|
133
|
-
// Eğer filter verilmezse tüm dataları döndür
|
|
134
216
|
for (const [key, value] of Object.entries(this.data)) {
|
|
135
217
|
results.push({ key, value });
|
|
136
218
|
}
|
|
@@ -139,11 +221,6 @@ class SehawqDB extends EventEmitter {
|
|
|
139
221
|
return new QueryResult(results);
|
|
140
222
|
}
|
|
141
223
|
|
|
142
|
-
/**
|
|
143
|
-
* Find first entry that matches the filter
|
|
144
|
-
* @param {Function} filter - Filter function
|
|
145
|
-
* @returns {Object|undefined} First matching item
|
|
146
|
-
*/
|
|
147
224
|
findOne(filter) {
|
|
148
225
|
if (typeof filter === 'function') {
|
|
149
226
|
for (const [key, value] of Object.entries(this.data)) {
|
|
@@ -155,13 +232,6 @@ class SehawqDB extends EventEmitter {
|
|
|
155
232
|
return undefined;
|
|
156
233
|
}
|
|
157
234
|
|
|
158
|
-
/**
|
|
159
|
-
* Filter by field value with operators
|
|
160
|
-
* @param {string} field - Field name (supports dot notation)
|
|
161
|
-
* @param {string} operator - Comparison operator (=, !=, >, <, >=, <=, in, contains)
|
|
162
|
-
* @param {*} value - Value to compare
|
|
163
|
-
* @returns {QueryResult} Chainable query result
|
|
164
|
-
*/
|
|
165
235
|
where(field, operator, value) {
|
|
166
236
|
return this.find((item, key) => {
|
|
167
237
|
const fieldValue = this._getValueByPath(item, field);
|
|
@@ -194,13 +264,8 @@ class SehawqDB extends EventEmitter {
|
|
|
194
264
|
});
|
|
195
265
|
}
|
|
196
266
|
|
|
197
|
-
// ----------------
|
|
267
|
+
// ---------------- Aggregation System ----------------
|
|
198
268
|
|
|
199
|
-
/**
|
|
200
|
-
* Count total entries
|
|
201
|
-
* @param {Function} [filter] - Optional filter function
|
|
202
|
-
* @returns {number} Count of entries
|
|
203
|
-
*/
|
|
204
269
|
count(filter) {
|
|
205
270
|
if (filter) {
|
|
206
271
|
return this.find(filter).count();
|
|
@@ -208,12 +273,6 @@ class SehawqDB extends EventEmitter {
|
|
|
208
273
|
return Object.keys(this.data).length;
|
|
209
274
|
}
|
|
210
275
|
|
|
211
|
-
/**
|
|
212
|
-
* Sum numeric values by field
|
|
213
|
-
* @param {string} field - Field name to sum
|
|
214
|
-
* @param {Function} [filter] - Optional filter function
|
|
215
|
-
* @returns {number} Sum of values
|
|
216
|
-
*/
|
|
217
276
|
sum(field, filter) {
|
|
218
277
|
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
219
278
|
return items.reduce((sum, item) => {
|
|
@@ -222,12 +281,6 @@ class SehawqDB extends EventEmitter {
|
|
|
222
281
|
}, 0);
|
|
223
282
|
}
|
|
224
283
|
|
|
225
|
-
/**
|
|
226
|
-
* Average of numeric values by field
|
|
227
|
-
* @param {string} field - Field name to average
|
|
228
|
-
* @param {Function} [filter] - Optional filter function
|
|
229
|
-
* @returns {number} Average of values
|
|
230
|
-
*/
|
|
231
284
|
avg(field, filter) {
|
|
232
285
|
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
233
286
|
if (items.length === 0) return 0;
|
|
@@ -240,12 +293,6 @@ class SehawqDB extends EventEmitter {
|
|
|
240
293
|
return sum / items.length;
|
|
241
294
|
}
|
|
242
295
|
|
|
243
|
-
/**
|
|
244
|
-
* Minimum value by field
|
|
245
|
-
* @param {string} field - Field name
|
|
246
|
-
* @param {Function} [filter] - Optional filter function
|
|
247
|
-
* @returns {*} Minimum value
|
|
248
|
-
*/
|
|
249
296
|
min(field, filter) {
|
|
250
297
|
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
251
298
|
if (items.length === 0) return undefined;
|
|
@@ -256,12 +303,6 @@ class SehawqDB extends EventEmitter {
|
|
|
256
303
|
}).filter(val => val !== Infinity));
|
|
257
304
|
}
|
|
258
305
|
|
|
259
|
-
/**
|
|
260
|
-
* Maximum value by field
|
|
261
|
-
* @param {string} field - Field name
|
|
262
|
-
* @param {Function} [filter] - Optional filter function
|
|
263
|
-
* @returns {*} Maximum value
|
|
264
|
-
*/
|
|
265
306
|
max(field, filter) {
|
|
266
307
|
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
267
308
|
if (items.length === 0) return undefined;
|
|
@@ -272,12 +313,6 @@ class SehawqDB extends EventEmitter {
|
|
|
272
313
|
}).filter(val => val !== -Infinity));
|
|
273
314
|
}
|
|
274
315
|
|
|
275
|
-
/**
|
|
276
|
-
* Group entries by field value
|
|
277
|
-
* @param {string} field - Field name to group by
|
|
278
|
-
* @param {Function} [filter] - Optional filter function
|
|
279
|
-
* @returns {Object} Grouped results
|
|
280
|
-
*/
|
|
281
316
|
groupBy(field, filter) {
|
|
282
317
|
const items = filter ? this.find(filter).toArray() : this.find().toArray();
|
|
283
318
|
const groups = {};
|
|
@@ -295,6 +330,286 @@ class SehawqDB extends EventEmitter {
|
|
|
295
330
|
return groups;
|
|
296
331
|
}
|
|
297
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
|
+
|
|
298
613
|
// ---------------- Backup & Restore ----------------
|
|
299
614
|
async backup(backupPath) {
|
|
300
615
|
await fs.writeFile(backupPath, JSON.stringify(this.data, null, 2), "utf8");
|
|
@@ -365,18 +680,12 @@ class SehawqDB extends EventEmitter {
|
|
|
365
680
|
}
|
|
366
681
|
}
|
|
367
682
|
|
|
368
|
-
// ---------------- QueryResult Class
|
|
683
|
+
// ---------------- QueryResult Class ----------------
|
|
369
684
|
class QueryResult {
|
|
370
685
|
constructor(results) {
|
|
371
686
|
this.results = results || [];
|
|
372
687
|
}
|
|
373
688
|
|
|
374
|
-
/**
|
|
375
|
-
* Sort results by field
|
|
376
|
-
* @param {string} field - Field name to sort by
|
|
377
|
-
* @param {string} direction - 'asc' or 'desc'
|
|
378
|
-
* @returns {QueryResult} Chainable
|
|
379
|
-
*/
|
|
380
689
|
sort(field, direction = 'asc') {
|
|
381
690
|
this.results.sort((a, b) => {
|
|
382
691
|
const aVal = this._getValueByPath(a.value, field);
|
|
@@ -391,89 +700,45 @@ class QueryResult {
|
|
|
391
700
|
return this;
|
|
392
701
|
}
|
|
393
702
|
|
|
394
|
-
/**
|
|
395
|
-
* Limit number of results
|
|
396
|
-
* @param {number} count - Maximum number of results
|
|
397
|
-
* @returns {QueryResult} Chainable
|
|
398
|
-
*/
|
|
399
703
|
limit(count) {
|
|
400
704
|
this.results = this.results.slice(0, count);
|
|
401
705
|
return this;
|
|
402
706
|
}
|
|
403
707
|
|
|
404
|
-
/**
|
|
405
|
-
* Skip number of results
|
|
406
|
-
* @param {number} count - Number of results to skip
|
|
407
|
-
* @returns {QueryResult} Chainable
|
|
408
|
-
*/
|
|
409
708
|
skip(count) {
|
|
410
709
|
this.results = this.results.slice(count);
|
|
411
710
|
return this;
|
|
412
711
|
}
|
|
413
712
|
|
|
414
|
-
/**
|
|
415
|
-
* Get count of results
|
|
416
|
-
* @returns {number} Count
|
|
417
|
-
*/
|
|
418
713
|
count() {
|
|
419
714
|
return this.results.length;
|
|
420
715
|
}
|
|
421
716
|
|
|
422
|
-
/**
|
|
423
|
-
* Get first result
|
|
424
|
-
* @returns {Object|undefined} First result
|
|
425
|
-
*/
|
|
426
717
|
first() {
|
|
427
718
|
return this.results[0];
|
|
428
719
|
}
|
|
429
720
|
|
|
430
|
-
/**
|
|
431
|
-
* Get last result
|
|
432
|
-
* @returns {Object|undefined} Last result
|
|
433
|
-
*/
|
|
434
721
|
last() {
|
|
435
722
|
return this.results[this.results.length - 1];
|
|
436
723
|
}
|
|
437
724
|
|
|
438
|
-
/**
|
|
439
|
-
* Convert to array
|
|
440
|
-
* @returns {Array} Results array
|
|
441
|
-
*/
|
|
442
725
|
toArray() {
|
|
443
726
|
return this.results;
|
|
444
727
|
}
|
|
445
728
|
|
|
446
|
-
/**
|
|
447
|
-
* Get only values (without keys)
|
|
448
|
-
* @returns {Array} Values array
|
|
449
|
-
*/
|
|
450
729
|
values() {
|
|
451
730
|
return this.results.map(item => item.value);
|
|
452
731
|
}
|
|
453
732
|
|
|
454
|
-
/**
|
|
455
|
-
* Get only keys
|
|
456
|
-
* @returns {Array} Keys array
|
|
457
|
-
*/
|
|
458
733
|
keys() {
|
|
459
734
|
return this.results.map(item => item.key);
|
|
460
735
|
}
|
|
461
736
|
|
|
462
|
-
/**
|
|
463
|
-
* Apply additional filter
|
|
464
|
-
* @param {Function} filter - Filter function
|
|
465
|
-
* @returns {QueryResult} Chainable
|
|
466
|
-
*/
|
|
467
737
|
filter(filter) {
|
|
468
738
|
this.results = this.results.filter(item => filter(item.value, item.key));
|
|
469
739
|
return this;
|
|
470
740
|
}
|
|
471
741
|
|
|
472
|
-
/**
|
|
473
|
-
* Map over results
|
|
474
|
-
* @param {Function} mapper - Map function
|
|
475
|
-
* @returns {Array} Mapped results
|
|
476
|
-
*/
|
|
477
742
|
map(mapper) {
|
|
478
743
|
return this.results.map(item => mapper(item.value, item.key));
|
|
479
744
|
}
|