evelodb
Version:
A high-performance native B-tree database for Node.js. Made by Evelocore.
494 lines (385 loc) • 18.1 kB
Markdown
# EveloDB — Agent Reference Guide
> This document is written for AI agents and automated tools. It provides everything needed to correctly read, write, and maintain code that uses **EveloDB** (`evelodb` npm package).
---
## 1. What is EveloDB?
EveloDB is a **local, file-backed, BSON B-Tree database for Node.js**. It stores data in binary `.db` files on disk using B-Tree indexes (`.bidx` files) for O(log n) lookups. It requires **no external database server** and is designed for high-throughput local storage.
**Key facts:**
- Data is stored on the local filesystem in a configurable directory (default: `./evelodbprime`).
- All records are serialized as BSON documents.
- Primary key is always `_id` internally (a 24-character hex ObjectID). A custom `objectIdKey` can alias it in the API.
- System-managed fields: `_id` (or `objectIdKey` alias), `_createdAt`, `_modifiedAt`. **Never set these manually.**
- Supports schema validation, B-Tree secondary indexes, unique constraints, atomic operators, and transactions.
---
## 2. Installation & Import
```bash
npm i evelodb
```
```typescript
// TypeScript / ES Modules
import eveloDB from 'evelodb';
// CommonJS
const eveloDB = require('evelodb');
```
---
## 3. CRITICAL RULES — Read Before Writing Any Code
> **Violating these rules will cause bugs that are hard to trace.**
### Rule 1: Single Instance Only
Create **one** `eveloDB` instance per application and export/share it. **Never** call `new eveloDB()` in multiple files or modules. Doing so creates desynchronized caches and causes `EPERM` lock errors and data loss.
```typescript
// db.ts — do this ONCE
import eveloDB from 'evelodb';
export const db = new eveloDB({ directory: './data' });
// other files — import the shared instance
import { db } from './db';
```
### Rule 2: Never Manually Set System Fields
The fields `_id`, `_createdAt`, `_modifiedAt`, and any custom `objectIdKey` are auto-managed. Passing them to `create()` or `edit()` will throw an error. The system strips them before writing.
### Rule 3: Empty Conditions `{}` Targets ALL Records
Calling `db.edit('users', {}, ...)`, `db.delete('users', {})`, or `db.find('users', {})` matches every record in the collection.
### Rule 4: `readImage()` is async, everything else is sync
All methods are **synchronous** except `readImage()`, `atomic()`, and `transaction()`. Do not `await` synchronous methods.
### Rule 5: Schema Strict Mode
If a `schema` is defined in the config with at least one collection, **only collections listed in the schema are accessible**. Calling any method on an unlisted collection returns `{ err: '...not defined in schema', code: 'COLLECTION_NOT_DEFINED' }`.
---
## 4. Configuration
```typescript
const db = new eveloDB({
directory: './evelodbprime', // Storage path (default: './evelodbprime')
maxHandles: 64, // Max open file handles in LRU cache (default: 64)
compactThreshold: 0.3, // Auto-compact when tombstones/total >= this ratio (default: 0.3)
schema: { // Optional schema, indexes, and constraints per collection
users: {
fields: {
username: { type: String, required: true, min: 5, max: 30 },
email: { type: String, required: true },
age: { type: Number, required: false, max: 120 },
address: {
type: {
city: { type: String, required: true },
country: { type: String, required: true },
},
required: false,
},
},
indexes: ['email', 'username'], // B-Tree secondary indexes (O(log n) lookups)
uniqueKeys: ['email', 'username'], // Fields that must be globally unique
objectIdKey: 'userId', // Alias for _id exposed in the API
noRepeat: true, // Reject exact duplicate records (default: true)
},
},
});
```
### Schema Field Definition
| Property | Type | Description |
|------------|-------------------|-----------------------------------------------------------------|
| `type` | `String` \| `Number` \| `Boolean` \| `Array` \| `Object` \| nested obj | The data type. Use constructors, not strings. |
| `required` | `boolean` | If `true`, field must be present in `create()` / `edit()`. |
| `min` | `number` | Min value (Number) or min length (String/Array). |
| `max` | `number` | Max value (Number) or max length (String/Array). |
### Nested Object Types
Nest a plain object (not `Object` constructor) as the `type` to define nested schema validation:
```typescript
vehicle: {
type: {
color: { type: String, required: true },
model: { type: String, required: true },
},
required: false,
}
```
---
## 5. Core CRUD Operations
### `db.create(collection, data)` → `WriteResult`
Inserts a new record. Returns the generated ID under the `objectIdKey` (or `_id` if not configured).
```typescript
const result = db.create('users', { username: 'alice', email: 'alice@example.com' });
// result: { success: true, userId: '662e5a4e3d5a4e3d5a4e3d5a' }
// On error: { err: '...', code: 'SCHEMA_VALIDATION_FAILED' | 'DUPLICATE_KEY' | 'DUPLICATE_DATA' | ... }
```
### `db.edit(collection, conditions, newData)` → `EditResult`
Updates all records matching `conditions`. Also aliased as `db.update()`.
```typescript
db.edit('users', { username: 'alice' }, { email: 'new@example.com' });
// result: { success: true, modifiedCount: 1, skippedDuplicates: 0 }
// On no match: { err: 'No match', code: 'NO_MATCH' }
```
Supports **atomic update operators** in `newData`:
```typescript
db.edit('products', { productId: 'p1' }, { $inc: { stock: -1 } });
db.edit('users', { userId: 'u1' }, { $set: { status: 'active' } });
db.edit('posts', { postId: 'p2' }, { $push: { tags: 'nodejs' } });
db.edit('posts', { postId: 'p2' }, { $pull: { tags: 'experimental' } });
db.edit('records', { userId: 'u2' }, { $unset: { tempFlag: true } });
```
| Operator | Effect |
|----------|--------|
| `$inc` | Increment/decrement a numeric field |
| `$set` | Set a field to a value |
| `$pull` | Remove a value or matching items from an array |
| `$unset` | Remove a field from the document |
| `$push` | Append a value to an array field |
> If both operators and plain fields are mixed, operators take full precedence. Use only operators OR only plain merge, not both.
### `db.delete(collection, conditions)` → `DeleteResult`
Deletes all matching records.
```typescript
db.delete('users', { username: 'alice' });
// result: { success: true, deletedCount: 1 }
```
### `db.find(collection, conditions)` → `QueryResult<T>`
Returns a `QueryResult` for all matching records. Conditions support comparison operators.
```typescript
const result = db.find('users', { age: { $gte: 18 } });
const page = result.getList(0, 10); // first 10
const total = result.count();
const all = result.all();
```
### `db.findOne(collection, conditions)` → `T | null`
Returns the first matching record, or `null`.
```typescript
const user = db.findOne('users', { userId: 'abc123' });
```
### `db.get(collection)` → `QueryResult<T>`
Returns all records in a collection.
```typescript
const allUsers = db.get('users').all();
```
### `db.search(collection, conditions)` → `QueryResult<T>`
Case-insensitive substring search across specified fields.
```typescript
db.search('users', { username: 'ali' }); // matches "alice", "Ali", etc.
```
---
## 6. Comparison Operators (for `find()` and `delete()`)
```typescript
{ age: { $eq: 25 } } // Equal
{ age: { $ne: 25 } } // Not equal
{ age: { $gt: 25 } } // Greater than
{ age: { $gte: 25 } } // Greater than or equal
{ age: { $lt: 25 } } // Less than
{ age: { $lte: 25 } } // Less than or equal
{ status: { $in: ['active', 'pending'] } } // In array
{ status: { $nin: ['inactive'] } } // Not in array
{ name: { $regex: '^Al', $options: 'i' } } // Regex match
```
---
## 7. QueryResult Methods
`find()`, `search()`, and `get()` all return a `QueryResult<T>` object with:
| Method | Returns | Description |
|--------------------------------|------------------------------|--------------------------------------------|
| `.all()` | `T[] \| { err: string }` | All matched records |
| `.getList(offset, limit)` | `T[] \| { err: string }` | Paginated slice |
| `.count()` | `number \| { err: string }` | Total matched count |
| `.sort(compareFn)` | `QueryResult<T>` | Returns a new sorted QueryResult (chainable) |
```typescript
const result = db.find('users', { role: 'admin' })
.sort((a, b) => a.username.localeCompare(b.username))
.getList(0, 20);
```
---
## 8. Utility Operations
### `db.count(collection)` → `CountResult`
```typescript
const { count } = db.count('users');
```
### `db.check(collection, conditions)` → `boolean`
```typescript
const exists = db.check('users', { email: 'alice@example.com' });
```
### `db.inject(collection, data[], options?)` → `{ success, count }`
High-performance bulk import. Each record **must** include system fields: `_createdAt`, `_modifiedAt`, and the ID field.
```typescript
db.inject('users', records, { method: 'overwrite' }); // clears then fills
db.inject('users', records, { method: 'merge' }); // appends to existing
```
### `db.compact(collection)` → `{ success }`
Reclaims space used by tombstoned (deleted/updated) records. Normally triggered automatically.
### `db.rebuildIndexes(collection)` → `void`
Rebuilds all B-Tree index files from the raw data. Use for recovery from corruption.
### `db.drop(collection)` / `db.reset(collection)` → `DropResult`
Permanently deletes all data and index files for a collection.
### `db.closeAll()` → `void`
Flushes all indexes and closes all file handles. Call on graceful shutdown.
---
## 9. Object Store
For storing small key-value application state (not collections). Stored as `.objdb` BSON files.
```typescript
db.object('appConfig').write({ theme: 'dark', version: '1.0' });
db.object('appConfig').update({ theme: 'light' }); // deep merge
const cfg = db.object('appConfig').read(); // returns object or null
db.object('appConfig').rename('userSettings');
db.object('userSettings').delete();
const list = db.object().list(); // ['appConfig', ...]
```
---
## 10. File Store
Stores raw binary files in `{directory}/files/`.
```typescript
db.writeFile('photo.jpg', buffer); // save
const { data } = db.readFile('photo.jpg'); // read as Buffer
db.deleteFile('photo.jpg'); // delete
const files = db.allFiles(); // ['photo.jpg', ...]
```
---
## 11. Image Processing (`readImage`)
> **This method is `async`.** Always `await` it.
```typescript
const result = await db.readImage('photo.jpg', {
returnBase64: true, // default true — returns data URL; false returns Buffer
quality: 0.8, // 0.1–1.0, compression quality
pixels: 500000, // max total pixel count (0 = no limit)
maxWidth: 1280, // max width in pixels
maxHeight: 720, // max height in pixels
blackAndWhite: false, // greyscale
mirror: false, // flip horizontal
upToDown: false, // flip vertical
invert: false, // invert colors
brightness: 1, // 0.1–5, 1 = original
contrast: 1, // 0.1–5, 1 = original
});
// result: { success: true, data: 'data:image/jpeg;base64,...', metadata: { ... } }
```
Supported formats: `.jpg`, `.jpeg`, `.jfif`, `.png`, `.webp`, `.avif`, `.tiff`, `.gif`, `.bmp`, `.svg`, `.ico`, `.heic`.
**Performance notes:**
- Results are cached in a 200MB LRU cache (keyed by image hash + config). Repeated calls with the same input are near-instant.
- AVIF encodes are limited to 2 parallel workers by default. Set `AVIF_CONCURRENCY` env var to tune.
---
## 12. Backup & Restore
```typescript
// Create backup
db.createBackup('users', {
type: 'binary', // 'binary' (default, XOR-encoded) or 'json'
path: './backups',
password: 'secret', // optional, for binary type
title: 'April 2026', // optional metadata
});
// Read backup info without restoring
const info = db.readBackupFile('./backups/users_backup.backup', 'secret');
// Restore (OVERWRITES current data)
db.restoreBackup('users', {
type: 'binary',
file: './backups/users_backup.backup',
password: 'secret',
});
```
> Backups always preserve the raw `_id` field, regardless of `objectIdKey` settings.
---
## 13. Transactions & Atomic Operations
Use these when there is an `await` between a read and a subsequent write, to prevent race conditions.
### `db.transaction(collection, callback)` — Collection-level lock
```typescript
await db.transaction('products', async () => {
const item = db.findOne('products', { productId: 'p1' });
await someAsyncLogic();
db.edit('products', { productId: 'p1' }, { stock: item.stock - 1 });
});
```
### `db.atomic(collection, callback)` — Collection-level lock with `tx` object
```typescript
await db.atomic('products', async (tx) => {
const item = tx.findOne('products', { productId: 'p1' });
await someAsyncLogic();
tx.edit('products', { productId: 'p1' }, { $inc: { stock: -1 } });
});
```
### `db.atomic(callback)` — Global lock (all collections)
```typescript
await db.atomic(async (tx) => {
const user = tx.findOne('users', { userId: 'u1' });
tx.create('logs', { message: `${user.username} updated` });
});
```
> For simple counter updates without any `await` gaps, prefer `$inc` directly — it's faster and doesn't need a lock.
---
## 14. `objectIdKey` Mapping — How the ID Alias Works
When you set `objectIdKey: 'userId'` in the schema:
- **Reading**: The internal `_id` field is renamed to `userId` in all returned documents.
- **Writing/Querying**: Use `userId` in conditions and returned results; `_id` is internal only.
- **Inject**: The injection payload must include the ID as `userId` (or `_id` — both are accepted during inject).
```typescript
// Creating
db.create('users', { username: 'bob', email: 'bob@b.com' });
// → { success: true, userId: 'abc...' } ← NOT _id
// Finding
db.findOne('users', { userId: 'abc...' });
// → { userId: 'abc...', username: 'bob', ... } ← _id is hidden
```
---
## 15. Error Codes Reference
| Code | Meaning |
|----------------------------|--------------------------------------------------|
| `COLLECTION_NOT_DEFINED` | Collection not in schema (when schema is set) |
| `SCHEMA_VALIDATION_FAILED` | Field type/required/min/max validation failed |
| `DUPLICATE_KEY` | `_id` already exists |
| `DUPLICATE_UNIQUE_KEY` | A `uniqueKeys` field value is already taken |
| `DUPLICATE_DATA` | Exact record already exists (`noRepeat: true`) |
| `NO_MATCH` | `edit()` found no matching records |
| `MISSING_ID` | `inject()` record missing ID field |
| `MISSING_CREATED_AT` | `inject()` record missing `_createdAt` |
| `MISSING_MODIFIED_AT` | `inject()` record missing `_modifiedAt` |
| `ID_CONFLICT` | `inject({ method: 'merge' })` — ID already exists |
| `404` | File not found (file store operations) |
---
## 16. TypeScript Types Quick Reference
```typescript
import eveloDB, {
EveloDBConfig,
WriteResult,
EditResult,
DeleteResult,
CountResult,
DropResult,
FileResult,
ReadImageResult,
ReadImageConfig,
BackupResult,
BackupFileInfo,
Conditions,
Condition,
QueryResult,
ObjectResult,
ObjectStore,
} from 'evelodb';
```
---
## 17. Common Patterns for Agents
### Pattern: Paginated list endpoint
```typescript
const page = 0, limit = 20;
const result = db.find('products', { category: 'electronics' });
const items = result.getList(page * limit, limit);
const total = result.count();
```
### Pattern: Upsert (check-then-create-or-edit)
```typescript
const existing = db.findOne('users', { email: 'x@x.com' });
if (existing) {
db.edit('users', { email: 'x@x.com' }, { lastSeen: new Date().toISOString() });
} else {
db.create('users', { email: 'x@x.com', username: 'newuser' });
}
```
### Pattern: Atomic stock decrement
```typescript
// Simple (no async gap) — no lock needed
db.edit('products', { productId: 'p1' }, { $inc: { stock: -1 } });
// With async gap — use atomic
await db.atomic('products', async (tx) => {
const p = tx.findOne('products', { productId: 'p1' });
if (!p || p.stock <= 0) throw new Error('Out of stock');
await chargePayment();
tx.edit('products', { productId: 'p1' }, { $inc: { stock: -1 } });
});
```
### Pattern: Sorted search with pagination
```typescript
const users = db.search('users', { username: 'ali' })
.sort((a, b) => a.username.localeCompare(b.username))
.getList(0, 10);
```
### Pattern: Safe shutdown
```typescript
process.on('SIGINT', () => {
db.closeAll();
process.exit(0);
});
```