indinis
Version:
A storage library using LSM trees for storage and B-trees for indices with MVCC support
428 lines (305 loc) • 13.9 kB
Markdown
# Indinis DB
[](https://www.npmjs.com/package/indinis)
[](https://github.com/your-repo/indinis/actions)
[](https://opensource.org/licenses/MIT)
**Indinis** is a high-performance, embedded NoSQL database for Node.js and TypeScript, featuring a fluent, Firestore-like API. It's designed to be fast, easy to use, and powerful enough for complex applications.
## Install
Install Indinis via npm:
```bash
npm i indinis
```
### Key Features
* **Fluent, Type-Safe API**: A modern, chainable API that feels familiar and boosts productivity.
* **High-Performance Caching**: Optional in-memory cache to dramatically accelerate read-heavy workloads.
* **Powerful Indexing**: Automatic indexing for common fields and robust support for secondary and multikey indexes.
* **Advanced Queries**: Perform rich queries with filtering, sorting, pagination, and array operators (`arrayContains`, `arrayContainsAny`).
* **Atomic Operations**: Safe, concurrent-friendly atomic increments for counters.
* **Composable Queries**: Build clean, reusable, and maintainable query logic.
* **Zero Dependencies**: A lightweight, self-contained package.
## Table of Contents
1. [Installation](#installation)
2. [Quick Start](#quick-start)
3. [Core Concepts: Stores and Items](#core-concepts-stores-and-items)
4. [Core API Guide](#core-api-guide)
* [Creating Data](#creating-data)
* [Reading Data](#reading-data)
* [Updating Data](#updating-data)
* [Deleting Data](#deleting-data)
5. [Querying Data](#querying-data)
* [Filtering with `.filter()`](#filtering-with-filter)
* [Querying Arrays](#querying-arrays)
* [Sorting with `.sortBy()`](#sorting-with-sortby)
* [Pagination](#pagination)
6. [Advanced Topics](#advanced-topics)
* [In-Memory Data Caching](#in-memory-data-caching)
* [Indexing](#indexing)
* [Composable Queries with `.use()`](#composable-queries-with-use)
7. [Configuration](#configuration)
8. [License](#license)
## Installation
```bash
npm install indinis
```
## Quick Start
Here's a simple example to get you up and running in minutes.
```typescript
import { Indinis } from 'indinis';
// Define a type for your data for type-safety
interface User {
id?: string;
name: string;
email: string;
createdAt: number;
}
async function main() {
// 1. Initialize the database
const db = new Indinis('./my-first-database');
const usersStore = db.store<User>('users');
// 2. Create a new user with an auto-generated ID
console.log('Creating a new user...');
const newUser = await usersStore.make({
name: 'Alice',
email: 'alice@example.com',
createdAt: Date.now()
}).withSnapshot(); // .withSnapshot() returns the full document
console.log('User created:', newUser);
// 3. Read the user back by their ID
console.log('\nFetching user by ID...');
const fetchedUser = await usersStore.item(newUser.id!).one();
console.log('Found:', fetchedUser);
// 4. Query for the user by their email
console.log('\nQuerying for user by email...');
const userFromQuery = await usersStore
.filter('email').equals('alice@example.com')
.one();
console.log('Found via query:', userFromQuery);
}
main().catch(console.error);
```
## Core Concepts: Stores and Items
Indinis organizes data in a hierarchical structure familiar to users of document databases.
- **Store**: A container for documents, analogous to a *collection* in Firestore or a table. Store paths must have an **odd number of segments** (e.g., `'users'`, `'users/user123/posts'`).
- **Item**: An individual document within a store. Item paths must have an **even number of segments** (e.g., `'users/user123'`).
```typescript
import { Indinis } from 'indinis';
const db = new Indinis('./my-database');
// A reference to the top-level 'users' store
const usersStore = db.store('users');
// A reference to a specific user item
const userItem = usersStore.item('user123');
// A reference to a nested 'posts' store for a user
const userPostsStore = db.store('users/user123/posts');
```
## Core API Guide
### Creating Data
Use `.make()` to create or overwrite documents.
#### Auto-Generated IDs
Call `.make()` on a `StoreRef` to create a document with a unique, auto-generated ID.
```typescript
const users = db.store('users');
// By default, make() resolves with the new document's ID
const newUserId = await users.make({
name: 'John Doe',
email: 'john.doe@example.com'
});
console.log('Created user with ID:', newUserId);
// Chain .withSnapshot() to get the full document back immediately
const newUserDoc = await users.make({
name: 'Jane Smith',
email: 'jane.smith@example.com'
}).withSnapshot();
console.log('Created new user:', newUserDoc);
// { name: 'Jane Smith', ..., id: 'bH5iLr9tMq0xZaZ4wH3v' }
```
#### Specified IDs
Call `.make()` on an `ItemRef` to create a document with an ID you provide. By default, this fails if the document already exists to prevent accidental overwrites.
```typescript
const userItem = db.store('users').item('john-doe-123');
await userItem.make({ name: 'John Doe', email: 'john@example.com' });
// To overwrite an existing document, pass `true` as the second argument.
await userItem.make({ name: 'John Doe V2', status: 'active' }, true);
```
### Reading Data
#### Fetch a Single Item by ID
The most direct way to get a document is by its ID using `item(...).one()`.
```typescript
const userData = await db.store('users').item('john-doe-123').one();
if (userData) {
console.log(userData.name); // 'John Doe V2'
}
```
You can also efficiently check for existence with `.exists()`.
```typescript
const userExists = await db.store('users').item('john-doe-123').exists(); // true or false
```
### Updating Data
#### Partial Updates with `modify()`
Use `.modify()` on an `ItemRef` to update specific fields of a document without replacing the entire object. This method will fail if the document does not exist.
```typescript
const userItem = db.store('users').item('john-doe-123');
// Update the user's status and add a new 'lastLogin' field
await userItem.modify({
status: 'active',
lastLogin: Date.now()
});
```
#### Atomic Increments
For counters (likes, views, etc.), use the atomic `increment()` operator to prevent race conditions. Import the `increment` function from the `indinis` library.
**Rules:** The target field must exist and its value must be a number.
```typescript
import { Indinis, increment } from 'indinis';
const postRef = db.store('posts').item('postXYZ');
// Atomically increment 'views' and update 'lastViewedBy' in one operation
await postRef.modify({
views: increment(1),
lastViewedBy: 'user-abc'
});
// You can also
await db.store('users').item('user123').remove();
```
## Querying Data
Build queries by starting with a `store()` or `query()` and chaining methods.
### Filtering with `.filter()`
Use `.filter()` with a chainable operator to define your query conditions.
| Operator | Description |
| -------------------- | ----------------------------------- |
| `.equals(val)` | Field is equal to `val` |
| `.greaterThan(val)` | Field is greater than `val` |
| `.greaterThanOrEqual(val)` | Field is >= `val` |
| `.lessThan(val)` | Field is less than `val` |
| `.lessThanOrEqual(val)` | Field is <= `val` |
You can chain multiple `.filter()` calls. For a query to succeed, **at least one of the filtered fields must be indexed**.
```typescript
const activeEngineers = await db.store<Employee>('employees')
.filter('department').equals('eng') // Uses an index
.filter('status').equals('active') // Post-filtered in memory
.filter('level').greaterThanOrEqual(5) // Post-filtered in memory
.take(10); // Retrieves up to 10 matching documents
```
### Querying Arrays
Indinis supports powerful queries on array fields, provided a **multikey index** exists on the field.
#### Creating a Multikey Index
```typescript
// Create an index on the 'tags' array field for the 'products' store
await db.createIndex('products', 'idx_prod_tags', {
field: 'tags',
multikey: true
});
```
#### `arrayContains`
Find documents where the array field contains a specific element.
```typescript
// Find all products tagged as 'electronics'
const electronics = await db.store('products')
.filter('tags').arrayContains('electronics')
.take();
```
#### `arrayContainsAny`
Find documents where the array field contains at least one of the elements from the provided list.
```typescript
// Find products that are on 'sale' OR are a 'bestseller'
const featured = await db.store('products')
.filter('tags').arrayContainsAny(['sale', 'bestseller'])
.take();
```
### Sorting with `.sortBy()`
Use `.sortBy()` to order your query results. Sorting requires an index on the field you are sorting by.
```typescript
const recentUsers = await db.store('users')
.sortBy('createdAt', 'desc') // Sort by creation date, newest first
.take(20);
```
### Pagination
Indinis provides efficient, cursor-based pagination. Pagination queries **must** include at least one `.sortBy()` clause.
The `.get()` method executes the query and returns a `PaginatedQueryResult` object containing the documents, cursors, and page information.
#### Example: Paginating through Users
```typescript
const usersStore = db.store('users');
const PAGE_SIZE = 10;
// --- Fetch the First Page ---
const firstPage = await usersStore.query()
.sortBy('name', 'asc')
.limit(PAGE_SIZE)
.get();
console.log('First page users:', firstPage.docs.map(u => u.name));
console.log('Has next page?', firstPage.hasNextPage);
// --- Fetch the Next Page ---
if (firstPage.hasNextPage) {
const nextPage = await usersStore.query()
.sortBy('name', 'asc') // The query must be identical
.limit(PAGE_SIZE)
.startAfter(...firstPage.endCursor!) // Use the cursor from the previous page
.get();
console.log('Next page users:', nextPage.docs.map(u => u.name));
}
```
## Advanced Topics
### In-Memory Data Caching
Indinis includes an optional in-memory cache to dramatically accelerate read-heavy workloads. The cache operates transparently using a cache-aside pattern: items are cached on first read, and automatically invalidated on any write (`set`, `modify`, `remove`) to guarantee consistency.
#### Enabling and Configuring the Cache
Enable the cache via `IndinisOptions`.
```typescript
import { Indinis, IndinisOptions } from 'indinis';
const dbOptions: IndinisOptions = {
enableCache: true, // Simple enablement with defaults
// Advanced configuration
cacheOptions: {
maxSize: 5000, // Max items in cache (default: 1000)
policy: 'LRU', // Eviction policy: 'LRU' or 'LFU'
defaultTTLMilliseconds: 60000, // Expire entries after 1 minute (default: 0, no TTL)
enableStats: true // Set true to monitor performance
}
};
const db = new Indinis('./my-cached-db', dbOptions);
```
#### Monitoring Cache Performance
Use `db.getCacheStats()` to check the cache's effectiveness. The most important metric is the `hitRate`.
```typescript
const stats = await db.getCacheStats();
if (stats) {
console.log(`Cache Hit Rate: ${(stats.hitRate * 100).toFixed(2)}%`);
}
```
### Indexing
Indinis is designed for performance and will not perform slow, un-indexed collection scans.
**Rule: No Index, No Query.** A query with one or more `.filter()` conditions **must** be serviceable by at least one existing index. If no suitable index is found, the operation will throw an error.
#### Automatic `name` Index
For convenience, the **first time** a document is written to a new store, Indinis automatically creates a default secondary index on the `name` field. This allows you to start querying immediately on new collections.
#### Listing Indexes
You can see which indexes are available for a specific collection.
```typescript
const userIndexes = await db.store('users').listIndexes();
console.log(userIndexes);
```
### Composable Queries with `.use()`
For complex applications, you can encapsulate and reuse query logic using `.use()`. Define reusable parts as functions that take and return a `Query` object.
```typescript
import { IQuery } from 'indinis';
// A reusable part to find only active users
const isActive = (query: IQuery<User>) => query.filter('status').equals('active');
// A parameterized part to find users by region
const byRegion = (region: string) =>
(query: IQuery<User>) => query.filter('region').equals(region);
// Compose the parts to build a clean, readable query
const recentActiveWestCoastUsers = await db.store<User>('users')
.query() // Start a composable query
.use(isActive)
.use(byRegion('us-west'))
.sortBy('createdAt', 'desc')
.take(20);
```
## Configuration
You can customize the database behavior by passing an `IndinisOptions` object to the constructor.
```typescript
import { Indinis, IndinisOptions } from 'indinis';
const dbOptions: IndinisOptions = {
enableCache: true,
cacheOptions: {
maxSize: 10000
},
// ... other options
};
const db = new Indinis('./my-database', dbOptions);
```
## License
[MIT](LICENSE)