@nekodb/client
v1.1.7
Published
NekoDB is a lightweight encrypted document database for Node.js, built with real-time WebSocket communication, simple JSON-based storage, and a developer-friendly API. Designed for speed, security, and ease of integration without external database depende
Maintainers
Readme
NekoDB
Secure, resilient document database with high availability and seamless real-time data access.
Install
npm install @nekodb/clientQuick Start
const NekoDB = require('@nekodb/client');
const db = new NekoDB({
host: 'your-server.com',
username: 'Username',
password: 'Password',
});
const id = await db.insert('users', { name: 'Neko', age: 5 });
const doc = await db.get('users', id);
const results = await db.search('users', { name: 'Neko' });
await db.update('users', id, { age: 6 });
await db.delete('users', id);
db.close();Why NekoDB?
| Feature | NekoDB | MongoDB | Firebase | SQLite | |---------|--------|---------|----------|--------| | AES-256 Encryption | ✅ Built-in | ❌ | ❌ | ❌ | | Multi-Node Auto-Failover | ✅ | Manual setup | N/A | ❌ | | Zero Disk Overflow | ✅ 99% auto-migrate | ❌ | N/A | ❌ | | WebSocket Real-time | ✅ | Change Streams | ✅ | ❌ | | Binary WebSocket Protocol | ✅ | ❌ | ❌ | ❌ | | No Schema Required | ✅ | ✅ | ✅ | ❌ | | Aggregation Pipeline | ✅ | ✅ | Limited | ❌ | | Bulk Operations | ✅ Parallel | ✅ | ✅ | ❌ | | Auto-Reconnect | ✅ Built-in | Manual | Built-in | N/A | | Data Encrypted at Rest | ✅ AES-256-GCM | Enterprise only | ✅ | ❌ |
Connection
const NekoDB = require('@nekodb/client');
const db = new NekoDB({
host: 'your-server.com',
username: 'your_username',
password: 'your_password',
// Options: 'none', 'debug', 'info', 'warn', 'error'
// Or pass a custom logger object like winston/pino (must implement debug, info, warn, error)
logging: 'info',
});
// From environment variables (NEKODB_HOST, NEKODB_USERNAME, NEKODB_PASSWORD)
const db = NekoDB.fromEnv();Events
db.on('connected', () => console.log('Connected!'));
db.on('disconnected', (info) => console.log('Disconnected:', info));
db.on('reconnected', () => console.log('Reconnected!'));
db.on('reconnect_failed', () => console.log('Could not reconnect'));
db.on('error', (err) => console.error('Error:', err));
db.on('closed', () => console.log('Connection closed'));L1 Cache & Real-Time Invalidation
NekoDB Client has a built-in L1 Cache that speeds up read queries (get, list, count) by storing responses in local memory.
By default, L1 cache has a TTL of 10 seconds. You can configure it when initializing the client:
const db = new NekoDB({
host: 'your-server.com',
username: 'your_username',
password: 'your_password',
cache: {
ttl: 30000, // TTL in milliseconds (default: 10000)
maxSize: 500 // Maximum cache size (default: 200)
}
});Real-Time Invalidation via Pub/Sub
When you subscribe to real-time events on a collection, NekoDB client automatically keeps your L1 cache updated. Any incoming insert, update, or delete event from the server will automatically invalidate the corresponding L1 cache entries.
// Local L1 cache is automatically invalidated when real-time updates occur
await db.subscribe('users', (event) => {
console.log('Real-time database event:', event);
});This guarantees 0 ms latency for read operations on cache hit while keeping data perfectly fresh!
TypeScript Support
NekoDB Client has built-in TypeScript definitions. Autocomplete and type checks work out-of-the-box in VS Code or any TypeScript project.
Collection API
Use db.collection() for a cleaner syntax:
const users = db.collection('users');
await users.insert({ name: 'Neko', role: 'admin' });
const doc = await users.get('doc-id');
const all = await users.list();
const found = await users.search({ role: 'admin' });
await users.update('doc-id', { role: 'superadmin' });
await users.delete('doc-id');
const total = await users.count();
await users.drop();CRUD Operations
Insert
const docId = await db.insert('products', {
name: 'Laptop',
price: 999,
tags: ['electronics', 'computers'],
});
// Insert document with TTL (Time-To-Live in seconds)
const sessionDocId = await db.insertTTL('sessions', { token: 'secret123' }, 3600);
// or using options object:
await db.insert('sessions', { token: 'secret123' }, { ttl: 3600 });Get
const product = await db.get('products', docId);List
const allIds = await db.list('products');Search
// Exact match
await db.search('products', { name: 'Laptop' });
// Comparison operators: $gt, $gte, $lt, $lte, $eq, $ne, $in, $nin, $regex
await db.search('products', { price: { $gt: 500 } });
await db.search('products', { price: { $gte: 100, $lte: 1000 } });
await db.search('products', { category: { $in: ['electronics', 'software'] } });
await db.search('products', { name: { $regex: '^Laptop' } });Vector Search
Search documents by vector/embedding similarity using Cosine Similarity or Euclidean (L2) Distance:
// From NekoDB instance
const results = await db.vectorSearch('products', {
field: 'embedding',
vector: [0.12, 0.43, -0.91],
metric: 'cosine', // 'cosine' or 'l2' (euclidean)
topK: 5
});
// From Collection instance
const products = db.collection('products');
const results = await products.vectorSearch({
field: 'embedding',
vector: [0.12, 0.43, -0.91],
metric: 'l2',
topK: 3
});Full-Text Search (FTS)
Search documents by textual relevance using the BM25 scoring algorithm:
// From NekoDB instance
const results = await db.ftsSearch('articles', {
field: 'content',
query: 'belajar go database',
limit: 10
});
// From Collection instance
const articles = db.collection('articles');
const results = await articles.ftsSearch({
field: 'content',
query: 'belajar go database',
limit: 5
});Update
await db.update('products', docId, { price: 899 });Delete
await db.delete('products', docId);Count
const result = await db.count('products');List Collections
const collections = await db.listCollections();Delete Collection
await db.deleteCollection('temp_data');Query Builder
Build queries with a chainable, fluent API:
const users = db.collection('users');
// Simple query
const results = await users.query()
.where('age').gt(18)
.where('role').eq('admin')
.exec();
// With sorting and pagination
const page = await users.query()
.where('status').eq('active')
.sort('name', 'asc')
.limit(20)
.page(1)
.exec();
// Range query
const products = await db.query('products')
.between('price', 100, 500)
.sortDesc('price')
.limit(10)
.exec();
// Select specific fields (projection)
const names = await users.query()
.where('role').eq('admin')
.select('name', 'email')
.exec();
// Exclude specific fields
const safe = await users.query()
.exclude('password', 'secret')
.exec();
// Get first match only
const first = await users.query()
.where('email').eq('[email protected]')
.first();
// In / Not In
const results = await users.query()
.where('role').in(['admin', 'moderator'])
.where('status').ne('banned')
.exec();
// Regex / Wildcard matching
const matches = await users.query()
.where('email').regex('@gmail\\.com$')
.exec();Collection Helpers
High-level helper methods for common operations:
const users = db.collection('users').helper();
// Find single document matching query
const admin = await users.findOne({ role: 'admin' });
// Find by ID (returns doc with _id)
const user = await users.findById('doc-id');
// Check if document exists
const exists = await users.exists('doc-id');
// Find all matching documents (returns full docs, not just IDs)
// ⚡ Optimized: Fetches all matching documents in exactly 1 WebSocket roundtrip!
const admins = await users.findMany({ role: 'admin' });
// Get all documents in collection
// ⚡ Optimized: Fetches all documents in exactly 1 WebSocket roundtrip!
const allUsers = await users.getAll();
// Upsert: update if exists, insert if not
const result = await users.upsert({ email: '[email protected]' }, {
email: '[email protected]',
name: 'Neko',
role: 'user',
});
// result: { action: 'inserted', id: '...' } or { action: 'updated', id: '...' }
// Find or insert
const { doc, created } = await users.findOrInsert(
{ email: '[email protected]' },
{ email: '[email protected]', name: 'Neko' }
);
// Update all documents matching query
await users.updateWhere({ role: 'user' }, { verified: true });
// Delete all documents matching query
await users.deleteWhere({ status: 'inactive' });
// Async paginate iterator
for await (const page of users.paginate({ limit: 10 })) {
console.log(`Page ${page.page}:`, page.data);
// page.pageInfo.has_next tells if more pages exist
}
// Functional helpers
await users.forEach({ role: 'admin' }, (user, i) => {
console.log(`Admin ${i}:`, user.name);
});
const names = await users.map({ role: 'admin' }, u => u.name);
const seniors = await users.filter({}, u => u.age > 50);
// Update nested properties (dot-notation)
await users.updatePath('doc-id', 'profile.avatar', 'new-avatar.png');
// Push item to nested array
await users.pushToArray('doc-id', 'profile.tags', 'developer');
// Pull item from nested array
await users.pullFromArray('doc-id', 'profile.tags', 'developer');Schema Validation
Client-side validation before sending data to the server:
const { Schema } = require('@nekodb/client');
const userSchema = new Schema({
name: { type: 'string', required: true, minLength: 1, maxLength: 100 },
email: { type: 'string', required: true, match: /^[^@]+@[^@]+\.[^@]+$/ },
age: { type: 'number', min: 0, max: 200 },
role: { type: 'string', enum: ['admin', 'user', 'moderator'] },
tags: 'array',
}, { strict: true }); // strict: reject unknown fields
// Validate
const { valid, errors } = userSchema.validate({
name: 'Neko',
email: '[email protected]',
age: 5,
role: 'admin',
});
if (!valid) {
console.error('Validation errors:', errors);
// [{ field: 'name', rule: 'required', message: '"name" is required' }]
}
// Apply defaults
const withDefaults = userSchema.applyDefaults({ name: 'Neko' });
// Pick/Omit fields
const partial = userSchema.pick(doc, 'name', 'email');
const safe = userSchema.omit(doc, 'password', 'secret');
// Inspect schema
console.log(userSchema.getFieldNames()); // ['name', 'email', 'age', ...]
console.log(userSchema.getRequiredFields()); // ['name', 'email']Pagination & Sorting
// Offset-based
const page1 = await db.listPaginated('products', { limit: 10, offset: 0 });
// Page-based
const page2 = await db.listPaginated('products', { limit: 10, page: 2 });
// With sorting
const sorted = await db.listPaginated('products', {
limit: 10,
sort: [{ field: 'price', order: 'desc' }],
});
// Cursor-based
const next = await db.listPaginated('products', {
limit: 10,
cursor: page1.page_info.next_cursor,
});
// Search with pagination
const results = await db.searchPaginated('products', { price: { $gt: 100 } }, { limit: 5 });Aggregation Pipeline
const result = await db.aggregate('orders', [
{ type: '$match', params: { status: 'completed' } },
{ type: '$group', params: {
_id: '$category',
total: { $sum: '$amount' },
avg: { $avg: '$amount' },
count: { $sum: 1 },
}},
{ type: '$sort', params: { total: -1 } },
{ type: '$limit', params: { value: 5 } },
]);Stages: $match, $group, $sort, $limit, $skip, $project, $count, $unwind
Accumulators: $sum, $avg, $min, $max, $count, $first, $last, $push
Bulk Operations
// Bulk insert
await db.bulkInsert('products', [
{ name: 'Mouse', price: 25 },
{ name: 'Keyboard', price: 75 },
]);
// Bulk update
await db.bulkUpdate('products', {
'doc-id-1': { price: 30 },
'doc-id-2': { price: 80 },
});
// Bulk delete
await db.bulkDelete('products', ['doc-id-1', 'doc-id-2']);
// Mixed operations
await db.bulkExecute([
{ type: 'insert', collection: 'logs', document: { event: 'login' } },
{ type: 'update', collection: 'users', document_id: 'abc', document: { active: true } },
{ type: 'delete', collection: 'sessions', document_id: 'old-session' },
]);Projection
// Include only specific fields
const doc = await db.getProjected('users', 'doc-id', { name: 1, email: 1 });
// Exclude specific fields
const doc2 = await db.getProjected('users', 'doc-id', { password: 0 });
// Search with projection
const results = await db.searchProjected('users', { role: 'admin' }, { name: 1, email: 1 });Indexing
await db.createIndex('users', 'email', 'hash');
await db.createIndex('products', 'price', 'sorted');
const indexes = await db.listIndexes('users');Server-Side Schema & Snapshot Management
Expose server-side validation schemas and compressed backup snapshots:
// Register schema to be validated by the server before insert/update
await db.registerSchema('users', {
fields: {
name: 'string',
age: 'number',
verified: 'boolean',
},
required: ['name', 'verified'],
});
// Trigger a compressed .nkdb snapshot backup to local disk
const filename = await db.createSnapshot();
// returns: "backup_Username_1779752707.nkdb"
// Restore the database state from a specified .nkdb archive
await db.restoreSnapshot(filename);Data Export and File Download
Export documents into formatted CSV or JSON files, and download the resulting archives. Both collection and doc_id are required parameters. Exported files expire after 5 minutes.
// Export a document to CSV (both collection and doc_id are required)
const result = await db.exportCSV('users', 'c4752fbd433b9daae49ea09a4b490f7f');
// returns: { format: 'csv', filename: 'users_c4752fbd..._1779752707.csv',
// document_count: 1, file_size: 572, exported_at: '...', expires_at: '2026-06-25T15:05:00Z' }
// Export a document to JSON
const resultJson = await db.exportJSON('users', 'c4752fbd433b9daae49ea09a4b490f7f');
// Download the exported file using the filename (must download within 5 minutes)
const filename = result.filename;
await db.downloadExport(filename, './downloaded_users.csv');
console.log('File successfully downloaded!');You can also export directly from a Collection instance:
const users = db.collection('users');
// Export document to CSV (doc_id is required)
const csvRes = await users.exportCSV('c4752fbd433b9daae49ea09a4b490f7f');
// Export document to JSON (doc_id is required)
const jsonRes = await users.exportJSON('c4752fbd433b9daae49ea09a4b490f7f');Pub/Sub Reactive Channels
Subscribe to real-time change events on collections:
// Subscribe to real-time events on 'messages' collection
await db.subscribe('messages', (event) => {
console.log(`Real-time change: ${event.event} on document ${event.document_id}`);
if (event.document) {
console.log('Document content:', event.document);
}
});
// Broadcast events: Any other client inserting/updating/deleting in 'messages'
// will trigger the above callback in sub-millisecond real-time!Multi-Document ACID Transactions
Perform atomic multi-document writes with staging and rollbacks:
// Start a new ACID transaction session
const txId = await db.beginTransaction();
try {
// Stage insert operations inside the transaction
const id1 = await db.insert('users', { name: 'Alice' }, txId);
const id2 = await db.insert('users', { name: 'Bob' }, txId);
// Stage updates inside the transaction
await db.update('users', id1, { verified: true }, txId);
// Atomically commit all staged changes to the database
await db.commitTransaction(txId);
console.log('Transaction committed successfully!');
} catch (err) {
// Roll back the entire transaction if any error occurs
await db.rollbackTransaction(txId);
console.error('Transaction failed and rolled back:', err.message);
}Error Handling
Typed error classes for structured error handling:
const { errors } = require('@nekodb/client');
try {
const doc = await db.get('users', 'non-existent');
} catch (err) {
if (err instanceof errors.NotFoundError) {
console.log('Document not found:', err.collection, err.documentId);
} else if (err instanceof errors.AuthError) {
console.log('Authentication failed');
} else if (err instanceof errors.TimeoutError) {
console.log('Request timed out after', err.durationMs, 'ms');
} else if (err instanceof errors.ConnectionError) {
console.log('Connection error to', err.host);
}
}
// Classify raw server response into typed error
const error = errors.classify('invalid-credentials'); // returns AuthError
const error2 = errors.classify('not-found'); // returns NotFoundErrorAvailable error classes: NekoError, ConnectionError, TimeoutError, AuthError, NotFoundError, ValidationError, BulkOperationError, CollectionError
Event Bus
Enhanced event emitter with wildcards and promise support:
const { EventBus } = require('@nekodb/client');
const bus = new EventBus();
// Standard events
bus.on('user:created', (user) => console.log('Created:', user));
bus.on('user:deleted', (user) => console.log('Deleted:', user));
// Wildcard — listen to ALL events
bus.on('*', (eventName, data) => console.log('Event:', eventName, data));
// Once — fire only once
bus.once('db:ready', () => console.log('Database ready'));
// Remove listener
const handler = (data) => console.log(data);
bus.on('test', handler);
bus.off('test', handler);Connection Manager
Manage multiple NekoDB connections with built-in auto-failover query routing (Proxy):
const { ConnectionManager } = require('@nekodb/client');
const manager = new ConnectionManager();
// Add connections
manager.add('primary', new NekoDB({ host: 'server1.com', ... }));
manager.add('secondary', new NekoDB({ host: 'server2.com', ... }));
// 🌟 Smart Auto-Failover Proxy Routing:
// You can invoke database queries and helper methods directly on the manager.
// If the active node fails or goes offline, the manager automatically performs
// a failover (switches active node to a healthy one) and retries the operation transparently!
const id = await manager.insert('users', { name: 'Neko' });
// Works seamlessly with collection helper API too:
const users = manager.collection('users');
await users.insert({ name: 'Alice' });
// Switch active connection manually
manager.setActive('secondary');
// Health management
const healthy = manager.getHealthy(); // ['primary']
const unhealthy = manager.getUnhealthy(); // ['secondary']
manager.switchToHealthy(); // auto-switch to healthy node
// Stats & info
console.log(manager.list());
// [{ id: 'primary', connected: true, isActive: true, ... }, ...]
console.log(manager.getStats());
// { total: 2, connected: 1, disconnected: 1, activeId: 'primary' }
// Cleanup
await manager.closeAll();Full Example
const NekoDB = require('@nekodb/client');
const { Schema } = require('@nekodb/client');
async function main() {
const db = new NekoDB({
host: 'your-server.com',
username: 'Username',
password: 'Password',
});
db.on('connected', () => console.log('✅ Connected'));
db.on('error', (err) => console.error('❌', err));
// Define schema
const userSchema = new Schema({
name: { type: 'string', required: true },
age: { type: 'number', min: 0 },
role: { type: 'string', enum: ['admin', 'user'] },
});
// Validate before insert
const data = { name: 'Alice', age: 30, role: 'admin' };
const { valid, errors } = userSchema.validate(data);
if (!valid) return console.error('Invalid:', errors);
const users = db.collection('users');
const id = await users.insert(data);
// Query builder
const admins = await users.query()
.where('role').eq('admin')
.where('age').gte(18)
.sortDesc('age')
.limit(10)
.exec();
console.log('Admins:', admins);
// Collection helpers
const helper = users.helper();
const alice = await helper.findOne({ name: 'Alice' });
console.log('Found:', alice);
// Aggregation
const stats = await users.aggregate([
{ type: '$group', params: { _id: '$role', count: { $sum: 1 } } },
]);
console.log('Stats:', stats);
// Paginate
for await (const page of helper.paginate({ limit: 5 })) {
console.log(`Page ${page.page}:`, page.data.length, 'docs');
}
db.close();
}
main().catch(console.error);License
MIT
