npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

anigo

v0.1.11

Published

A lightweight, local / embedded NoSQL database.

Downloads

1,851

Readme

Installing Anigo

$ npm install -g anigo

Quick Start

import { Anigo } from 'anigo'

const db = Anigo.connect({ path: './app.db' })
const users = db.collection('users')

const id = users.insertOne({ name: 'Alice', age: 30, email: '[email protected]' })
// id is a UUID v7 string

const found = users.findOne({ _id: id })
// { _id: '...', name: 'Alice', age: 30, email: '[email protected]' }

const results = users.find({ age: { $gte: 25 } })
  .sort({ name: 1 })
  .skip(0)
  .limit(20)
  .toArray()

db.close()

Features

  • MongoDB-compatible CRUD API — all synchronous
  • Query operators: $gt, $gte, $lt, $lte, $ne, $in, $nin, $exists, $regex
  • Logical operators: $and, $or, $not
  • Update operators: $set, $unset, $inc, $push, $pull, $addToSet
  • Compound indexes with direction and uniqueness
  • Aggregation pipeline: $match, $sort, $skip, $limit, $project, $count, $group
  • Dot-notation for nested field queries
  • Vector search — semantic similarity via local HuggingFace ONNX embeddings (async, no HTTP calls)
  • UUID v7 (time-sortable, no dependencies)
  • Page-level encryption (AES-256-CBC, ChaCha20, SQLCipher, etc.)
  • WAL mode, 64 MB cache, optimized defaults

Table of Contents

Connection

import { Anigo } from 'anigo'

const db = Anigo.connect({ path: './data.db' })

The path is a file path where the SQLite database will be stored. The file is created on first write.

| Option | Type | Description | |--------|------|-------------| | path | string | Path to the SQLite database file | | key | string | Passphrase for AES-256-CBC encryption | | cipher | { key, algorithm } | Advanced cipher configuration | | vector | VectorOptions | Vector search configuration (model, dimensions, autoEmbed) |

isClosed

db.isClosed // boolean

Methods

query

const rows = db.query('SELECT id, doc FROM "users"')

Runs a raw SQL query and returns all matching rows. Useful for inspecting internal tables (_vec_config, _vec_pending, etc.).

exec

db.exec('INSERT INTO "users_vec_content_vx" (rowid, embedding) VALUES (?, ?)')

Runs a raw SQL statement that does not return rows (INSERT, UPDATE, DELETE, DDL).

close

db.close()

Closes the database. All subsequent operations throw.

flushVectorIndex

await db.flushVectorIndex()

Processes all pending vector embedding entries. Required when autoEmbed is false.

optimize

db.optimize()

Runs PRAGMA optimize. Call periodically or during maintenance windows.

backup

db.backup('./backup.db')

Creates a point-in-time snapshot via VACUUM INTO. The backup is a fully functional database file.

listCollections

const names = db.listCollections()
// ['users', 'products']

Returns the names of all user-defined collections. Excludes internal tables.

Collections

Collections are created lazily — the table is created on first write:

const users = db.collection('users')

users.insertOne({ name: 'Alice', age: 30, tags: ['admin'] })

const result = users.findOne({ name: 'Alice' })

Collections are cached per connection. Calling db.collection('users') twice returns the same instance.

CRUD

Insert

// Auto-generated UUID v7 _id
const id = users.insertOne({ name: 'Alice', age: 30 })

// Custom _id
const id = users.insertOne({ _id: 'user-1', name: 'Bob' })

// Bulk insert
const ids = users.insertMany([
  { name: 'Charlie', age: 25 },
  { name: 'Diana', age: 28 },
])
  • insertOne returns the document's _id (UUID v7 string). The generated _id is also set on the input document object.
  • Duplicate _id values throw DuplicateKeyError.
  • insertMany wraps all inserts in a transaction.
  • If vector search is configured and the document contains a vector-indexed field, the field is enqueued for embedding.

Find

// Find by _id (uses indexed column)
const user = users.findOne({ _id: id })

// Find by field
const user = users.findOne({ email: '[email protected]' })

// Find with operators
const results = users.find({ age: { $gte: 18, $lte: 65 } }).toArray()

// Find all
const all = users.find({}).toArray()
  • findOne returns null when no document matches.
  • find returns a Cursor — no query executes until .toArray(), .first(), or .forEach() is called.

Update

// $set — set field values
users.updateOne({ _id: id }, { $set: { age: 31 } })

// $inc — increment numeric fields
users.updateOne({ _id: id }, { $inc: { loginCount: 1 } })

// $unset — remove fields
users.updateOne({ _id: id }, { $unset: { tempField: '' } })

// $push — append to array
users.updateOne({ _id: id }, { $push: { tags: 'premium' } })

// $pull — remove from array by value
users.updateOne({ _id: id }, { $pull: { tags: 'inactive' } })

// $addToSet — add to array if not present
users.updateOne({ _id: id }, { $addToSet: { tags: 'vip' } })

// Update many
users.updateMany({ role: 'guest' }, { $set: { role: 'user' } })

Returns { matched, modified }:

  • matched — number of documents matching the filter
  • modified — number of documents actually changed

$push, $pull, and $addToSet use a read-modify-write pattern (read doc into JS, mutate, write back). $set, $unset, and $inc compile to SQL json_set()/json_remove() and run server-side.

Delete

// Delete one
users.deleteOne({ _id: id })

// Delete many
users.deleteMany({ status: 'archived' })

Returns { deleted } — the number of documents removed.

Count

// Total documents
const total = users.countDocuments()

// With filter
const active = users.countDocuments({ active: true })

Query Operators

Comparison

{ field: value }              // Equality
{ field: { $gt: value } }    // Greater than
{ field: { $gte: value } }   // Greater than or equal
{ field: { $lt: value } }    // Less than
{ field: { $lte: value } }   // Less than or equal
{ field: { $ne: value } }    // Not equal
{ field: { $in: [a, b] } }   // In array
{ field: { $nin: [a, b] } }  // Not in array
{ field: { $exists: true } } // Field exists / does not exist
{ field: { $regex: 'pat' } } // Regex match (JavaScript RegExp)

Logical

{ $and: [filter1, filter2] }
{ $or: [filter1, filter2] }
{ $not: filter }

Nested Fields

Use dot notation for nested fields:

{ 'address.city': 'New York' }
{ 'address.coordinates.lat': { $gt: 40 } }

_id

The _id field is special — it maps to the indexed id column:

{ _id: 'abc-123' }           // Direct PK lookup
{ _id: { $in: ['a', 'b'] } } // IN query on PK

Cursor

The Cursor is a lazy, chainable iterator returned by find() and aggregate().

const cursor = users.find({ age: { $gte: 25 } })
  .sort({ name: 1 })        // ORDER BY in SQL
  .skip(10)                 // OFFSET in SQL
  .limit(5)                 // LIMIT in SQL
  .project({ name: 1 })     // field projection in JS

// Terminal methods execute the query:
const results = cursor.toArray() // T[]
const first = cursor.first()     // T | null
cursor.forEach(doc => { ... })   // void

SQL Push Optimization

When sort(), skip(), or limit() are chained on a find() cursor, Anigo pushes these clauses directly into the SQL query sent to SQLite. This means:

  • ORDER BY runs in SQLite — can use indexes for sorting
  • LIMIT / OFFSET run in SQLite — only fetches needed rows from disk
  • find({}).sort({ name: 1 }).limit(10).toArray() fetches exactly 10 rows

Without sort/skip/limit, the cursor falls back to fetching all matching rows and applying operations in memory (including project()).

Projection

// Inclusion — only specified fields (plus _id by default)
cursor.project({ name: 1, age: 1 })

// Exclusion — all fields except specified
cursor.project({ _id: 0, password: 0, __v: 0 })

Projection is always applied in JavaScript (not pushed to SQL).

Indexes

Indexes are backed by SQLite indexes on json_extract() expressions.

Single-Field Index

users.createIndex({ email: 1 })
// SQL: CREATE INDEX "idx_users_email_1" ON "users" (json_extract(doc, '$.email') ASC)

Compound Index

A single index covering multiple fields. Supports queries on leading fields (index prefix matching):

users.createIndex({ lastName: 1, firstName: -1 })
// SQL: CREATE INDEX "idx_users_lastName_1_firstName_-1" ON "users"
//         (json_extract(doc, '$.lastName') ASC, json_extract(doc, '$.firstName') DESC)

This index accelerates:

  • find({ lastName: 'Smith' }) — prefix match on leading field
  • find({ lastName: 'Smith', firstName: 'John' }) — full match
  • sort({ lastName: 1, firstName: -1 }) — sort direction matches index
  • sort({ lastName: 1 }) — prefix sort

Unique Index

users.createIndex({ email: 1 }, { unique: true })
// SQL: CREATE UNIQUE INDEX "idx_users_email_1" ON "users" (json_extract(doc, '$.email') ASC)

Inserts or updates that would create a duplicate value throw an error. Unique constraint violations surface as typed errors (extend AnigoError).

Managing Indexes

// Create idempotently (IF NOT EXISTS)
users.createIndex({ name: 1 })

// Alias
users.ensureIndex({ email: 1 }, { unique: true })

// Drop
users.dropIndex({ name: 1 })

// List all indexes
const indexes = users.listIndexes()
// [
//   { name: 'idx_users_email_1', unique: true, columns: [...] },
//   { name: 'idx_users_lastName_1_firstName_-1', unique: false, columns: [...] },
// ]

Naming Convention

Indexes are named automatically: idx_{collection}_{field1}_{dir1}_{field2}_{dir2}...

  • createIndex({ age: 1 }) ? idx_users_age_1
  • createIndex({ lastName: 1, firstName: -1 }) ? idx_users_lastName_1_firstName_-1

Notes

  • _id already has a primary key index — createIndex({ _id: 1 }) is silently skipped.
  • Indexes on $regex queries are not used (uses JavaScript RegExp).
  • The Cursor SQL push optimization allows SQLite's query planner to use indexes for ORDER BY + LIMIT.

Aggregation Pipeline

const results = users.aggregate([
  { $match: { active: true } },
  { $sort: { age: -1 } },
  { $skip: 0 },
  { $limit: 10 },
  { $project: { name: 1, age: 1 } },
]).toArray()

Supported Stages

| Stage | Description | |-------|-------------| | $match | Filter documents (same operators as find) | | $sort | Sort by field: 1 (ASC) or -1 (DESC) | | $skip | Skip N documents | | $limit | Limit to N documents | | $project | Include/exclude fields | | $count | Count and output as document | | $group | Group with accumulators |

$group

users.aggregate([
  { $group: {
      _id: '$category',
      total: { $sum: 1 },
      avgPrice: { $avg: '$price' },
      minPrice: { $min: '$price' },
      maxPrice: { $max: '$price' },
  }},
]).toArray()

Supported accumulators: $sum, $avg, $min, $max, $first, $last.

Pipeline Optimization

Pipelines consisting only of $match, $sort, $skip, and $limit are collapsed into a single SQL query — sorting and pagination happen in SQLite, indexes apply.

Pipelines containing $project, $count, or $group execute entirely in memory (all matching documents are loaded into JavaScript).

Vector Search

Vector search enables semantic similarity queries. Field values are embedded into float vectors via a local HuggingFace ONNX model; documents closest to a prompt vector are returned ranked by distance. Embedding runs via ONNX Runtime — no HTTP calls.

Vector search operations are async (embedding inference runs asynchronously). CRUD operations remain synchronous.

const db = Anigo.connect({
  path: './app.db',
  vector: {
    model: 'onnx-community/bge-m3-onnx',
    dimensions: 1024,
    autoEmbed: true,
  },
})

const coll = db.collection<{ content: string; title: string }>('docs')

// Insert is sync; enqueues the document for embedding
coll.insertOne({ title: 'machine learning', content: 'neural networks' })

// Create vector indexes
coll.createVectorIndex('content')
coll.createVectorIndex('title')

// Flush pending embeddings (await required)
await db.flushVectorIndex()

Key Concepts

| Concept | Description | |---------|-------------| | Vector index | A vec0 virtual table per (collection, field). Created via createVectorIndex(field). | | Pending queue | Table _vec_pending records documents needing embedding after insert/update when autoEmbed=false. | | autoEmbed | When true (default), a background drain loop processes pending entries. When false, call flushVectorIndex() manually. | | Collection API | createVectorIndex(), dropVectorIndex(), listVectorIndexes(), flushPending(), search() all live on Collection — no need to access an internal vectorManager property. |

Creating a Vector Index

coll.createVectorIndex('content')
  • createVectorIndex accepts only the field name. Dimension and distance are inferred from the global VectorOptions config.
  • The method is synchronous — it creates the vec0 virtual table and enqueues existing documents for embedding.
  • Supported distance metrics: cosine (default), l2, inner_product.

Listing Vector Indexes

const indexes = coll.listVectorIndexes()
// [
//   { field: 'content', dimensions: 1024, distance: 'cosine', pendingCount: 0 },
//   { field: 'title', dimensions: 1024, distance: 'cosine', pendingCount: 0 },
// ]

Dropping a Vector Index

coll.dropVectorIndex('content')

Removes the vec0 virtual table and its config entry.

Searching

// By raw vector array (no embedding needed)
const docs = await coll.search([0.1, 0.2, ...], { limit: 10 })

// By natural language prompt (embeds the prompt locally, then searches)
const docs = await coll.search('what is machine learning?', { limit: 10 })

// Cross-collection search
const results = await db.search('semantic query')
// Returns { collection, doc, score, field }[]
  • coll.search() merges results across all vector indexes on that collection, taking the minimum distance per document. Returns WithId<T>[].
  • db.search() searches all vector-indexed collections. Returns VectorSearchResult[] with collection, doc, score, and field.
  • Both methods are async — use await.

Flushing Pending Entries

await db.flushVectorIndex()

Processes all pending embedding jobs. Required when autoEmbed is false.

Error Handling

| Scenario | Behavior | |----------|----------| | Model load failure | Throws at connect (eager) or first use (lazy) | | Embedding dimension mismatch | Throws at createVectorIndex | | search(string) with no embedder | Throws InvalidOperationError | | Operation after close() | Throws InvalidOperationError |

Detailed documentation: docs/VECTOR.md

Encryption

Databases can be encrypted at rest by passing a key option:

const db = Anigo.connect({ path: './secure.db', key: 'passphrase' })

This uses SQLite's page-level AES-256-CBC cipher. The API is identical — everything is transparent.

For advanced cipher configurations:

const db = Anigo.connect({
  path: './secure.db',
  cipher: { key: 'passphrase', algorithm: 'chacha20' },
})

Available algorithms: aes256cbc, aes128cbc, chacha20, sqlcipher, rc4, ascon128, aegis.

Note: AES-256-GCM is not supported.

Backup & Optimize

// Point-in-time backup (VACUUM INTO)
db.backup('./snapshot.db')

// Optimize for performance (PRAGMA optimize)
db.optimize()

CLI

Anigo ships with a command-line interface. Install it globally:

npm install -g anigo

Then use the anigo command in one of three modes:

REPL Mode

Start an interactive shell with auto-await, dot-commands, and tab completion:

anigo path/to/db.db

Supports db.collectionName shorthand (via Proxy), bare commands (show collections, exit, quit), and the it variable:

> db.users.insertOne({ name: 'Alice', age: 30 })
'...uuid...'
> db.users.find({ age: { $gte: 25 } }).toArray()
[ { _id: '...', name: 'Alice', age: 30 } ]
> it
[ { _id: '...', name: 'Alice', age: 30 } ]

Expression Mode

Evaluate a single expression in a VM sandbox:

anigo path/to/db.db 'db.users.find({}).toArray()'

The sandbox provides access to db, console, JSON, Math, Buffer, Date, Map, Set, RegExp, setTimeout, and other standard globals. require, process, and import() are blocked.

Script Mode

Run a JavaScript file with access to the same sandbox:

anigo path/to/db.db --file ./scripts/migrate.js

Flags

| Flag | Description | |------|-------------| | --file | Run a script file instead of a single expression | | --json | Output results as JSON (both REPL and expression modes) | | --key | Database encryption passphrase | | --cipher-key | Advanced cipher key | | --cipher-algorithm | Cipher algorithm (e.g., chacha20, sqlcipher) | | --vector-model | Embedding model name | | --vector-dimensions | Embedding vector dimensions | | --vector-autoembed | Enable/disable autoEmbed |

Environment Variables

All flags can be set via environment variables:

  • ANIGO_KEY — encryption passphrase
  • ANIGO_CIPHER_KEY, ANIGO_CIPHER_ALGORITHM — cipher config
  • ANIGO_VECTOR_MODEL, ANIGO_VECTOR_DIMENSIONS, ANIGO_VECTOR_AUTOEMBED — vector config

Flags take precedence over environment variables.

Error Handling

All errors are typed subclasses of AnigoError:

| Error | When | |-------|------| | DuplicateKeyError | Insert/update violates unique constraint or duplicate _id | | InvalidFilterError | Malformed query filter | | InvalidUpdateError | Malformed update document | | InvalidOperationError | Operation not supported (including after close()) |

import { DuplicateKeyError } from 'anigo'

try {
  users.insertOne({ _id: 'dup-id' })
} catch (err) {
  if (err instanceof DuplicateKeyError) {
    console.log('Duplicate:', err.details)
  }
}

API Reference

Detailed API documentation in the docs/ directory:

License

MIT