@nekodb/client 1.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/LICENSE +21 -0
- package/README.md +557 -0
- package/auth/index.js +7 -0
- package/auth/key-derivation.js +7 -0
- package/auth/manager.js +45 -0
- package/connection/index.js +8 -0
- package/connection/manager.js +135 -0
- package/connection/status.js +17 -0
- package/errors/base.js +19 -0
- package/errors/classifier.js +15 -0
- package/errors/database.js +63 -0
- package/errors/index.js +16 -0
- package/errors/network.js +23 -0
- package/events/constants.js +10 -0
- package/events/emitter.js +106 -0
- package/events/index.js +9 -0
- package/events/subscriber.js +21 -0
- package/helpers/collection.js +151 -0
- package/helpers/document.js +49 -0
- package/helpers/index.js +9 -0
- package/helpers/pagination.js +59 -0
- package/index.js +392 -0
- package/middleware/batch.js +49 -0
- package/middleware/buffer.js +33 -0
- package/middleware/cache.js +55 -0
- package/middleware/index.js +15 -0
- package/middleware/logger.js +35 -0
- package/middleware/ping.js +29 -0
- package/middleware/reconnect.js +36 -0
- package/package.json +46 -0
- package/query/builder.js +181 -0
- package/query/field.js +21 -0
- package/query/index.js +9 -0
- package/query/operators.js +19 -0
- package/schema/index.js +7 -0
- package/schema/schema.js +70 -0
- package/schema/types.js +31 -0
- package/schema/validator.js +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Galang Purnama
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# NekoDB
|
|
2
|
+
|
|
3
|
+
> Secure, resilient document database with high availability and seamless real-time data access.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nekodb/client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const NekoDB = require('@nekodb/client');
|
|
15
|
+
|
|
16
|
+
const db = new NekoDB({
|
|
17
|
+
host: 'your-server.com',
|
|
18
|
+
username: 'Username',
|
|
19
|
+
password: 'Password',
|
|
20
|
+
// key: 'Key', // Optional: automatically derived as SHA-256 of username
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const id = await db.insert('users', { name: 'Neko', age: 5 });
|
|
24
|
+
const doc = await db.get('users', id);
|
|
25
|
+
const results = await db.search('users', { name: 'Neko' });
|
|
26
|
+
await db.update('users', id, { age: 6 });
|
|
27
|
+
await db.delete('users', id);
|
|
28
|
+
|
|
29
|
+
db.close();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Why NekoDB?
|
|
33
|
+
|
|
34
|
+
| Feature | NekoDB | MongoDB | Firebase | SQLite |
|
|
35
|
+
|---------|--------|---------|----------|--------|
|
|
36
|
+
| **AES-256 Encryption** | ✅ Built-in | ❌ | ❌ | ❌ |
|
|
37
|
+
| **Multi-Node Auto-Failover** | ✅ | Manual setup | N/A | ❌ |
|
|
38
|
+
| **Zero Disk Overflow** | ✅ 99% auto-migrate | ❌ | N/A | ❌ |
|
|
39
|
+
| **WebSocket Real-time** | ✅ | Change Streams | ✅ | ❌ |
|
|
40
|
+
| **Binary WebSocket Protocol** | ✅ | ❌ | ❌ | ❌ |
|
|
41
|
+
| **No Schema Required** | ✅ | ✅ | ✅ | ❌ |
|
|
42
|
+
| **Aggregation Pipeline** | ✅ | ✅ | Limited | ❌ |
|
|
43
|
+
| **Bulk Operations** | ✅ Parallel | ✅ | ✅ | ❌ |
|
|
44
|
+
| **Auto-Reconnect** | ✅ Built-in | Manual | Built-in | N/A |
|
|
45
|
+
| **Data Encrypted at Rest** | ✅ AES-256-GCM | Enterprise only | ✅ | ❌ |
|
|
46
|
+
|
|
47
|
+
## Connection
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
const NekoDB = require('@nekodb/client');
|
|
51
|
+
|
|
52
|
+
const db = new NekoDB({
|
|
53
|
+
host: 'your-server.com',
|
|
54
|
+
username: 'your_username',
|
|
55
|
+
password: 'your_password',
|
|
56
|
+
// key: 'your_key', // Optional: automatically derived as SHA-256 of username
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// From environment variables (NEKODB_HOST, NEKODB_USERNAME, NEKODB_PASSWORD, NEKODB_KEY)
|
|
60
|
+
const db = NekoDB.fromEnv();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Events
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
db.on('connected', () => console.log('Connected!'));
|
|
67
|
+
db.on('disconnected', (info) => console.log('Disconnected:', info));
|
|
68
|
+
db.on('reconnected', () => console.log('Reconnected!'));
|
|
69
|
+
db.on('reconnect_failed', () => console.log('Could not reconnect'));
|
|
70
|
+
db.on('error', (err) => console.error('Error:', err));
|
|
71
|
+
db.on('closed', () => console.log('Connection closed'));
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Collection API
|
|
75
|
+
|
|
76
|
+
Use `db.collection()` for a cleaner syntax:
|
|
77
|
+
|
|
78
|
+
```javascript
|
|
79
|
+
const users = db.collection('users');
|
|
80
|
+
|
|
81
|
+
await users.insert({ name: 'Neko', role: 'admin' });
|
|
82
|
+
const doc = await users.get('doc-id');
|
|
83
|
+
const all = await users.list();
|
|
84
|
+
const found = await users.search({ role: 'admin' });
|
|
85
|
+
await users.update('doc-id', { role: 'superadmin' });
|
|
86
|
+
await users.delete('doc-id');
|
|
87
|
+
const total = await users.count();
|
|
88
|
+
await users.drop();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## CRUD Operations
|
|
92
|
+
|
|
93
|
+
### Insert
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const docId = await db.insert('products', {
|
|
97
|
+
name: 'Laptop',
|
|
98
|
+
price: 999,
|
|
99
|
+
tags: ['electronics', 'computers'],
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Get
|
|
104
|
+
|
|
105
|
+
```javascript
|
|
106
|
+
const product = await db.get('products', docId);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### List
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
const allIds = await db.list('products');
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Search
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
// Exact match
|
|
119
|
+
await db.search('products', { name: 'Laptop' });
|
|
120
|
+
|
|
121
|
+
// Comparison operators: $gt, $gte, $lt, $lte, $eq, $ne, $in, $nin
|
|
122
|
+
await db.search('products', { price: { $gt: 500 } });
|
|
123
|
+
await db.search('products', { price: { $gte: 100, $lte: 1000 } });
|
|
124
|
+
await db.search('products', { category: { $in: ['electronics', 'software'] } });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Update
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
await db.update('products', docId, { price: 899 });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Delete
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
await db.delete('products', docId);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Count
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
const result = await db.count('products');
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### List Collections
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
const collections = await db.listCollections();
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Delete Collection
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
await db.deleteCollection('temp_data');
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Query Builder
|
|
158
|
+
|
|
159
|
+
Build queries with a chainable, fluent API:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const users = db.collection('users');
|
|
163
|
+
|
|
164
|
+
// Simple query
|
|
165
|
+
const results = await users.query()
|
|
166
|
+
.where('age').gt(18)
|
|
167
|
+
.where('role').eq('admin')
|
|
168
|
+
.exec();
|
|
169
|
+
|
|
170
|
+
// With sorting and pagination
|
|
171
|
+
const page = await users.query()
|
|
172
|
+
.where('status').eq('active')
|
|
173
|
+
.sort('name', 'asc')
|
|
174
|
+
.limit(20)
|
|
175
|
+
.page(1)
|
|
176
|
+
.exec();
|
|
177
|
+
|
|
178
|
+
// Range query
|
|
179
|
+
const products = await db.query('products')
|
|
180
|
+
.between('price', 100, 500)
|
|
181
|
+
.sortDesc('price')
|
|
182
|
+
.limit(10)
|
|
183
|
+
.exec();
|
|
184
|
+
|
|
185
|
+
// Select specific fields (projection)
|
|
186
|
+
const names = await users.query()
|
|
187
|
+
.where('role').eq('admin')
|
|
188
|
+
.select('name', 'email')
|
|
189
|
+
.exec();
|
|
190
|
+
|
|
191
|
+
// Exclude specific fields
|
|
192
|
+
const safe = await users.query()
|
|
193
|
+
.exclude('password', 'secret')
|
|
194
|
+
.exec();
|
|
195
|
+
|
|
196
|
+
// Get first match only
|
|
197
|
+
const first = await users.query()
|
|
198
|
+
.where('email').eq('admin@example.com')
|
|
199
|
+
.first();
|
|
200
|
+
|
|
201
|
+
// In / Not In
|
|
202
|
+
const results = await users.query()
|
|
203
|
+
.where('role').in(['admin', 'moderator'])
|
|
204
|
+
.where('status').ne('banned')
|
|
205
|
+
.exec();
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Collection Helpers
|
|
209
|
+
|
|
210
|
+
High-level helper methods for common operations:
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
const users = db.collection('users').helper();
|
|
214
|
+
|
|
215
|
+
// Find single document matching query
|
|
216
|
+
const admin = await users.findOne({ role: 'admin' });
|
|
217
|
+
|
|
218
|
+
// Find by ID (returns doc with _id)
|
|
219
|
+
const user = await users.findById('doc-id');
|
|
220
|
+
|
|
221
|
+
// Check if document exists
|
|
222
|
+
const exists = await users.exists('doc-id');
|
|
223
|
+
|
|
224
|
+
// Find all matching documents (returns full docs, not just IDs)
|
|
225
|
+
const admins = await users.findMany({ role: 'admin' });
|
|
226
|
+
|
|
227
|
+
// Get all documents in collection
|
|
228
|
+
const allUsers = await users.getAll();
|
|
229
|
+
|
|
230
|
+
// Upsert: update if exists, insert if not
|
|
231
|
+
const result = await users.upsert({ email: 'neko@example.com' }, {
|
|
232
|
+
email: 'neko@example.com',
|
|
233
|
+
name: 'Neko',
|
|
234
|
+
role: 'user',
|
|
235
|
+
});
|
|
236
|
+
// result: { action: 'inserted', id: '...' } or { action: 'updated', id: '...' }
|
|
237
|
+
|
|
238
|
+
// Find or insert
|
|
239
|
+
const { doc, created } = await users.findOrInsert(
|
|
240
|
+
{ email: 'neko@example.com' },
|
|
241
|
+
{ email: 'neko@example.com', name: 'Neko' }
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// Update all documents matching query
|
|
245
|
+
await users.updateWhere({ role: 'user' }, { verified: true });
|
|
246
|
+
|
|
247
|
+
// Delete all documents matching query
|
|
248
|
+
await users.deleteWhere({ status: 'inactive' });
|
|
249
|
+
|
|
250
|
+
// Async paginate iterator
|
|
251
|
+
for await (const page of users.paginate({ limit: 10 })) {
|
|
252
|
+
console.log(`Page ${page.page}:`, page.data);
|
|
253
|
+
// page.pageInfo.has_next tells if more pages exist
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Functional helpers
|
|
257
|
+
await users.forEach({ role: 'admin' }, (user, i) => {
|
|
258
|
+
console.log(`Admin ${i}:`, user.name);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const names = await users.map({ role: 'admin' }, u => u.name);
|
|
262
|
+
const seniors = await users.filter({}, u => u.age > 50);
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Schema Validation
|
|
266
|
+
|
|
267
|
+
Client-side validation before sending data to the server:
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
const { Schema } = require('@nekodb/client');
|
|
271
|
+
|
|
272
|
+
const userSchema = new Schema({
|
|
273
|
+
name: { type: 'string', required: true, minLength: 1, maxLength: 100 },
|
|
274
|
+
email: { type: 'string', required: true, match: /^[^@]+@[^@]+\.[^@]+$/ },
|
|
275
|
+
age: { type: 'number', min: 0, max: 200 },
|
|
276
|
+
role: { type: 'string', enum: ['admin', 'user', 'moderator'] },
|
|
277
|
+
tags: 'array',
|
|
278
|
+
}, { strict: true }); // strict: reject unknown fields
|
|
279
|
+
|
|
280
|
+
// Validate
|
|
281
|
+
const { valid, errors } = userSchema.validate({
|
|
282
|
+
name: 'Neko',
|
|
283
|
+
email: 'neko@example.com',
|
|
284
|
+
age: 5,
|
|
285
|
+
role: 'admin',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
if (!valid) {
|
|
289
|
+
console.error('Validation errors:', errors);
|
|
290
|
+
// [{ field: 'name', rule: 'required', message: '"name" is required' }]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Apply defaults
|
|
294
|
+
const withDefaults = userSchema.applyDefaults({ name: 'Neko' });
|
|
295
|
+
|
|
296
|
+
// Pick/Omit fields
|
|
297
|
+
const partial = userSchema.pick(doc, 'name', 'email');
|
|
298
|
+
const safe = userSchema.omit(doc, 'password', 'secret');
|
|
299
|
+
|
|
300
|
+
// Inspect schema
|
|
301
|
+
console.log(userSchema.getFieldNames()); // ['name', 'email', 'age', ...]
|
|
302
|
+
console.log(userSchema.getRequiredFields()); // ['name', 'email']
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Pagination & Sorting
|
|
306
|
+
|
|
307
|
+
```javascript
|
|
308
|
+
// Offset-based
|
|
309
|
+
const page1 = await db.listPaginated('products', { limit: 10, offset: 0 });
|
|
310
|
+
|
|
311
|
+
// Page-based
|
|
312
|
+
const page2 = await db.listPaginated('products', { limit: 10, page: 2 });
|
|
313
|
+
|
|
314
|
+
// With sorting
|
|
315
|
+
const sorted = await db.listPaginated('products', {
|
|
316
|
+
limit: 10,
|
|
317
|
+
sort: [{ field: 'price', order: 'desc' }],
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Cursor-based
|
|
321
|
+
const next = await db.listPaginated('products', {
|
|
322
|
+
limit: 10,
|
|
323
|
+
cursor: page1.page_info.next_cursor,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Search with pagination
|
|
327
|
+
const results = await db.searchPaginated('products', { price: { $gt: 100 } }, { limit: 5 });
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Aggregation Pipeline
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
const result = await db.aggregate('orders', [
|
|
334
|
+
{ type: '$match', params: { status: 'completed' } },
|
|
335
|
+
{ type: '$group', params: {
|
|
336
|
+
_id: '$category',
|
|
337
|
+
total: { $sum: '$amount' },
|
|
338
|
+
avg: { $avg: '$amount' },
|
|
339
|
+
count: { $sum: 1 },
|
|
340
|
+
}},
|
|
341
|
+
{ type: '$sort', params: { total: -1 } },
|
|
342
|
+
{ type: '$limit', params: { value: 5 } },
|
|
343
|
+
]);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Stages:** `$match`, `$group`, `$sort`, `$limit`, `$skip`, `$project`, `$count`, `$unwind`
|
|
347
|
+
|
|
348
|
+
**Accumulators:** `$sum`, `$avg`, `$min`, `$max`, `$count`, `$first`, `$last`, `$push`
|
|
349
|
+
|
|
350
|
+
## Bulk Operations
|
|
351
|
+
|
|
352
|
+
```javascript
|
|
353
|
+
// Bulk insert
|
|
354
|
+
await db.bulkInsert('products', [
|
|
355
|
+
{ name: 'Mouse', price: 25 },
|
|
356
|
+
{ name: 'Keyboard', price: 75 },
|
|
357
|
+
]);
|
|
358
|
+
|
|
359
|
+
// Bulk update
|
|
360
|
+
await db.bulkUpdate('products', {
|
|
361
|
+
'doc-id-1': { price: 30 },
|
|
362
|
+
'doc-id-2': { price: 80 },
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Bulk delete
|
|
366
|
+
await db.bulkDelete('products', ['doc-id-1', 'doc-id-2']);
|
|
367
|
+
|
|
368
|
+
// Mixed operations
|
|
369
|
+
await db.bulkExecute([
|
|
370
|
+
{ type: 'insert', collection: 'logs', document: { event: 'login' } },
|
|
371
|
+
{ type: 'update', collection: 'users', document_id: 'abc', document: { active: true } },
|
|
372
|
+
{ type: 'delete', collection: 'sessions', document_id: 'old-session' },
|
|
373
|
+
]);
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Projection
|
|
377
|
+
|
|
378
|
+
```javascript
|
|
379
|
+
// Include only specific fields
|
|
380
|
+
const doc = await db.getProjected('users', 'doc-id', { name: 1, email: 1 });
|
|
381
|
+
|
|
382
|
+
// Exclude specific fields
|
|
383
|
+
const doc2 = await db.getProjected('users', 'doc-id', { password: 0 });
|
|
384
|
+
|
|
385
|
+
// Search with projection
|
|
386
|
+
const results = await db.searchProjected('users', { role: 'admin' }, { name: 1, email: 1 });
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Indexing
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
await db.createIndex('users', 'email', 'hash');
|
|
393
|
+
await db.createIndex('products', 'price', 'sorted');
|
|
394
|
+
const indexes = await db.listIndexes('users');
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Error Handling
|
|
398
|
+
|
|
399
|
+
Typed error classes for structured error handling:
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
const { errors } = require('@nekodb/client');
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const doc = await db.get('users', 'non-existent');
|
|
406
|
+
} catch (err) {
|
|
407
|
+
if (err instanceof errors.NotFoundError) {
|
|
408
|
+
console.log('Document not found:', err.collection, err.documentId);
|
|
409
|
+
} else if (err instanceof errors.AuthError) {
|
|
410
|
+
console.log('Authentication failed');
|
|
411
|
+
} else if (err instanceof errors.TimeoutError) {
|
|
412
|
+
console.log('Request timed out after', err.durationMs, 'ms');
|
|
413
|
+
} else if (err instanceof errors.ConnectionError) {
|
|
414
|
+
console.log('Connection error to', err.host);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Classify raw server response into typed error
|
|
419
|
+
const error = errors.classify('invalid-credentials'); // returns AuthError
|
|
420
|
+
const error2 = errors.classify('not-found'); // returns NotFoundError
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
**Available error classes:** `NekoError`, `ConnectionError`, `TimeoutError`, `AuthError`, `NotFoundError`, `ValidationError`, `BulkOperationError`, `CollectionError`
|
|
424
|
+
|
|
425
|
+
## Event Bus
|
|
426
|
+
|
|
427
|
+
Enhanced event emitter with wildcards and promise support:
|
|
428
|
+
|
|
429
|
+
```javascript
|
|
430
|
+
const { EventBus } = require('@nekodb/client');
|
|
431
|
+
|
|
432
|
+
const bus = new EventBus();
|
|
433
|
+
|
|
434
|
+
// Standard events
|
|
435
|
+
bus.on('user:created', (user) => console.log('Created:', user));
|
|
436
|
+
bus.on('user:deleted', (user) => console.log('Deleted:', user));
|
|
437
|
+
|
|
438
|
+
// Wildcard — listen to ALL events
|
|
439
|
+
bus.on('*', (eventName, data) => console.log('Event:', eventName, data));
|
|
440
|
+
|
|
441
|
+
// Once — fire only once
|
|
442
|
+
bus.once('db:ready', () => console.log('Database ready'));
|
|
443
|
+
|
|
444
|
+
// Remove listener
|
|
445
|
+
const handler = (data) => console.log(data);
|
|
446
|
+
bus.on('test', handler);
|
|
447
|
+
bus.off('test', handler);
|
|
448
|
+
|
|
449
|
+
// Wait for event (promise-based)
|
|
450
|
+
const data = await bus.waitFor('user:created', 5000); // 5s timeout
|
|
451
|
+
|
|
452
|
+
// Emit
|
|
453
|
+
bus.emit('user:created', { name: 'Neko' });
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
## Connection Manager
|
|
457
|
+
|
|
458
|
+
Manage multiple NekoDB connections:
|
|
459
|
+
|
|
460
|
+
```javascript
|
|
461
|
+
const { ConnectionManager } = require('@nekodb/client');
|
|
462
|
+
|
|
463
|
+
const manager = new ConnectionManager();
|
|
464
|
+
|
|
465
|
+
// Add connections
|
|
466
|
+
manager.add('primary', new NekoDB({ host: 'server1.com', ... }));
|
|
467
|
+
manager.add('secondary', new NekoDB({ host: 'server2.com', ... }));
|
|
468
|
+
|
|
469
|
+
// Use active connection
|
|
470
|
+
const db = manager.active();
|
|
471
|
+
await db.insert('logs', { event: 'test' });
|
|
472
|
+
|
|
473
|
+
// Switch active connection
|
|
474
|
+
manager.setActive('secondary');
|
|
475
|
+
|
|
476
|
+
// Health management
|
|
477
|
+
const healthy = manager.getHealthy(); // ['primary']
|
|
478
|
+
const unhealthy = manager.getUnhealthy(); // ['secondary']
|
|
479
|
+
manager.switchToHealthy(); // auto-switch to healthy node
|
|
480
|
+
|
|
481
|
+
// Stats & info
|
|
482
|
+
console.log(manager.list());
|
|
483
|
+
// [{ id: 'primary', connected: true, isActive: true, ... }, ...]
|
|
484
|
+
|
|
485
|
+
console.log(manager.getStats());
|
|
486
|
+
// { total: 2, connected: 1, disconnected: 1, activeId: 'primary' }
|
|
487
|
+
|
|
488
|
+
// Cleanup
|
|
489
|
+
await manager.closeAll();
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## Full Example
|
|
493
|
+
|
|
494
|
+
```javascript
|
|
495
|
+
const NekoDB = require('@nekodb/client');
|
|
496
|
+
const { Schema } = require('@nekodb/client');
|
|
497
|
+
|
|
498
|
+
async function main() {
|
|
499
|
+
const db = new NekoDB({
|
|
500
|
+
host: 'your-server.com',
|
|
501
|
+
username: 'Username',
|
|
502
|
+
password: 'Password',
|
|
503
|
+
// key: 'Key', // Optional: automatically derived as SHA-256 of username
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
db.on('connected', () => console.log('✅ Connected'));
|
|
507
|
+
db.on('error', (err) => console.error('❌', err));
|
|
508
|
+
|
|
509
|
+
// Define schema
|
|
510
|
+
const userSchema = new Schema({
|
|
511
|
+
name: { type: 'string', required: true },
|
|
512
|
+
age: { type: 'number', min: 0 },
|
|
513
|
+
role: { type: 'string', enum: ['admin', 'user'] },
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Validate before insert
|
|
517
|
+
const data = { name: 'Alice', age: 30, role: 'admin' };
|
|
518
|
+
const { valid, errors } = userSchema.validate(data);
|
|
519
|
+
if (!valid) return console.error('Invalid:', errors);
|
|
520
|
+
|
|
521
|
+
const users = db.collection('users');
|
|
522
|
+
const id = await users.insert(data);
|
|
523
|
+
|
|
524
|
+
// Query builder
|
|
525
|
+
const admins = await users.query()
|
|
526
|
+
.where('role').eq('admin')
|
|
527
|
+
.where('age').gte(18)
|
|
528
|
+
.sortDesc('age')
|
|
529
|
+
.limit(10)
|
|
530
|
+
.exec();
|
|
531
|
+
console.log('Admins:', admins);
|
|
532
|
+
|
|
533
|
+
// Collection helpers
|
|
534
|
+
const helper = users.helper();
|
|
535
|
+
const alice = await helper.findOne({ name: 'Alice' });
|
|
536
|
+
console.log('Found:', alice);
|
|
537
|
+
|
|
538
|
+
// Aggregation
|
|
539
|
+
const stats = await users.aggregate([
|
|
540
|
+
{ type: '$group', params: { _id: '$role', count: { $sum: 1 } } },
|
|
541
|
+
]);
|
|
542
|
+
console.log('Stats:', stats);
|
|
543
|
+
|
|
544
|
+
// Paginate
|
|
545
|
+
for await (const page of helper.paginate({ limit: 5 })) {
|
|
546
|
+
console.log(`Page ${page.page}:`, page.data.length, 'docs');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
db.close();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
main().catch(console.error);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
## License
|
|
556
|
+
|
|
557
|
+
MIT
|
package/auth/index.js
ADDED
package/auth/manager.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const { sha256 } = require('./key-derivation');
|
|
2
|
+
|
|
3
|
+
class AuthManager {
|
|
4
|
+
#username;
|
|
5
|
+
#password;
|
|
6
|
+
#key;
|
|
7
|
+
|
|
8
|
+
constructor(username, password, key) {
|
|
9
|
+
this.#username = username || '';
|
|
10
|
+
this.#password = password || '';
|
|
11
|
+
if (key) {
|
|
12
|
+
this.#key = key;
|
|
13
|
+
} else if (this.#username) {
|
|
14
|
+
this.#key = sha256(this.#username);
|
|
15
|
+
} else {
|
|
16
|
+
this.#key = '';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getCredentials() {
|
|
21
|
+
return {
|
|
22
|
+
username: this.#username,
|
|
23
|
+
password: this.#password,
|
|
24
|
+
key: this.#key,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getUsername() {
|
|
29
|
+
return this.#username;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static fromEnv() {
|
|
33
|
+
return new AuthManager(
|
|
34
|
+
process.env.NEKODB_USERNAME,
|
|
35
|
+
process.env.NEKODB_PASSWORD,
|
|
36
|
+
process.env.NEKODB_KEY
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static fromConfig(config) {
|
|
41
|
+
return new AuthManager(config.username, config.password, config.key);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = { AuthManager };
|