@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 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
@@ -0,0 +1,7 @@
1
+ const { AuthManager } = require('./manager');
2
+ const { sha256 } = require('./key-derivation');
3
+
4
+ module.exports = {
5
+ AuthManager,
6
+ sha256,
7
+ };
@@ -0,0 +1,7 @@
1
+ const crypto = require('crypto');
2
+
3
+ function sha256(data) {
4
+ return crypto.createHash('sha256').update(data).digest('hex');
5
+ }
6
+
7
+ module.exports = { sha256 };
@@ -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 };
@@ -0,0 +1,8 @@
1
+ const { ConnectionManager } = require('./manager');
2
+ const { ConnectionStatus, ConnectionHealth } = require('./status');
3
+
4
+ module.exports = {
5
+ ConnectionManager,
6
+ ConnectionStatus,
7
+ ConnectionHealth,
8
+ };