@moltendb-web/query

May 20, 2026 ยท View on GitHub

Type-safe, chainable query builder for MoltenDb.

Works in vanilla JavaScript and TypeScript. Compiles as an npm module (CJS + ESM + .d.ts).

๐ŸŒ‹ Explore the Full Functionality

The best way to experience the MoltenDb query builder is through our * *Interactive Demo on StackBlitz **. It contains a complete, live environment where you can test query builder expressions, perform mutations, and see real-time events without any local setup.


What's New in v2.0.0

  • Bulk Delete with .where() โ€” delete documents matching a filter clause without listing individual keys.
  • Capped Collections (.maxSize()) โ€” cap a collection to a maximum number of documents; oldest entries are evicted automatically when the limit is reached.
  • TTL Collections (.ttl()) โ€” set a time-to-live (in seconds) on a collection; documents are removed automatically after expiry.

Installation

npm install @moltendb-web/query

Quick start

import {MoltenDb} from '@moltendb-web/core';
import {MoltenDbClient, WorkerTransport} from '@moltendb-web/query';

// 1. Initialize the Core Engine (boots WASM worker)
const db = new MoltenDb('moltendb_demo');
await db.init();

// 2. Connect the Query Builder to the worker
const client = new MoltenDbClient(db);

// SET โ€” insert / upsert
await client.collection('laptops')
    .set({
      lp1: {brand: 'Lenovo', model: 'ThinkPad X1', price: 1499, in_stock: true},
      lp2: {brand: 'Apple', model: 'MacBook Pro', price: 3499, in_stock: true},
    })
    .exec();

// GET โ€” query with WHERE, field projection, sort and pagination
const results = await client.collection('laptops')
    .get()
    .where({brand: 'Apple'})
    .fields(['brand', 'model', 'price'])
    .sort([{field: 'price', order: 'asc'}])
    .count(5)
    .exec();

// UPDATE โ€” partial patch (only listed fields are changed)
await client.collection('laptops')
    .update({lp1: {price: 1749, in_stock: false}})
    .exec();

// DELETE โ€” single key
await client.collection('laptops').delete().keys('lp1').exec();

// DELETE โ€” batch
await client.collection('laptops').delete().keys(['lp1', 'lp2']).exec();

// DELETE โ€” drop entire collection
await client.collection('laptops').delete().drop().exec();

// DELETE โ€” bulk delete with a where clause (v2.0.0)
await client.collection('laptops').delete().where({in_stock: {$eq: false}}).exec();

// DELETE โ€” bulk delete up to 5 oldest out-of-stock laptops (v2.0.0)
await client.collection('laptops')
    .delete()
    .where({in_stock: {$eq: false}})
    .offset(10)
    .count(5)
    .exec();

// SET โ€” capped collection (v2.0.0)
await client.collection('recent_events')
    .set({evt_001: {type: 'login', user: 'alice'}})
    .maxSize(5)
    .exec();

// SET โ€” TTL collection (v2.0.0)
await client.collection('sessions')
    .set({sess_abc: {userId: 'u1', token: 'xyz123'}})
    .ttl(1800)
    .exec();

Operations & allowed fields

Each operation only exposes the methods that are valid for it โ€” invalid combinations are caught at compile time by TypeScript.

get() โ€” read / query

MethodDescription
.keys(key | key[])Fetch one or more documents by key
.where(clause)Filter with operators: $eq $ne $gt $gte $lt $lte $in $nin $contains (and aliases)
.fields(string[])Return only these fields (dot-notation supported)
.excludedFields(string[])Return everything except these fields
.joins(JoinSpec[])Embed related documents from other collections
.sort(SortSpec[])Sort results (multi-field, asc/desc)
.count(n)Limit results to N documents
.offset(n)Skip first N results (pagination)
.build()Return the raw JSON payload without sending
.exec()Send the query and return the result

set(data) โ€” insert / upsert

MethodDescription
.extends(map)Embed snapshots from other collections at insert time
.maxSize(n)(v2.0.0) Cap the collection to N documents; oldest are evicted when full
.ttl(seconds)(v2.0.0) Set a time-to-live in seconds; documents are removed after expiry
.build()Return the raw JSON payload without sending
.exec()Send and return { count, status }

data can be a { key: document } map or a Document[] array (UUIDv7 keys are auto-assigned for arrays).

update(data) โ€” partial patch

MethodDescription
.build()Return the raw JSON payload without sending
.exec()Send and return { count, status }

Only the fields present in each patch object are updated โ€” all other fields are left unchanged.

delete() โ€” delete documents or drop collection

MethodDescription
.keys(key | key[])Delete one or more documents by key
.where(clause)(v2.0.0) Bulk-delete all documents matching the filter clause
.count(n)(v2.0.0) Limit the number of documents to delete
.offset(n)(v2.0.0) Skip the first N matching documents before deleting
.drop()Drop the entire collection
.build()Return the raw JSON payload without sending
.exec()Send and return { count, status }

WHERE operators

// Exact equality (implicit or explicit)
.where({brand: 'Apple'})
.where({brand: {$eq: 'Apple'}})
.where({brand: {$equals: 'Apple'}}) // alias

// Comparison
.where({price: {$gt: 1000, $lt: 3000}})
.where({price: {$greaterThan: 1000, $lessThan: 3000}}) // aliases
.where({'specs.cpu.cores': {$gte: 12}})

// Not equal
.where({'specs.cpu.brand': {$ne: 'Intel'}})
.where({'specs.cpu.brand': {$notEquals: 'Intel'}}) // alias

// In / not-in list
.where({brand: {$in: ['Apple', 'Dell']}})
.where({brand: {$oneOf: ['Apple', 'Dell']}}) // alias
.where({brand: {$nin: ['Framework']}})
.where({brand: {$notIn: ['Framework']}}) // alias

// Contains (string substring or array element)
.where({model: {$contains: 'Pro'}})
.where({model: {$ct: 'Pro'}}) // alias
.where({tags: {$contains: 'gaming'}})

// Multiple conditions (implicit AND)
.where({in_stock: true, 'specs.cpu.brand': 'Intel'})

Joins

const results = await client.collection('laptops')
    .get()
    .fields(['brand', 'model', 'price'])
    .joins([
      {alias: 'ram', from: 'memory', on: 'memory_id', fields: ['capacity_gb', 'type']},
      {alias: 'screen', from: 'display', on: 'display_id', fields: ['refresh_hz', 'panel']},
    ])
    .exec();
// Each result: { brand, model, price, ram: { capacity_gb, type }, screen: { refresh_hz, panel } }

Extends (snapshot embedding at insert time)

await client.collection('laptops')
    .set({
      lp7: {
        brand: 'MSI', model: 'Titan GT77', price: 3299,
        specs: {cpu: {brand: 'Intel', cores: 16, ghz: 5.0}},
      },
    })
    .extends({ram: 'memory.mem4', screen: 'display.dsp3'})
    .exec();
// lp7 is stored with the full mem4 and dsp3 documents embedded inline.

When to use extends vs joins:

extendsjoins
Resolved atInsert time (once)Query time (every request)
Data freshnessSnapshot โ€” may become staleAlways live
Read costO(1) โ€” data already embeddedO(1) per join per document
Use whenData rarely changes, fast reads matterData changes frequently, freshness matters

Custom transport

Implement MoltenTransport to connect to any backend:

import {MoltenTransport, MoltenDbClient, Document, JsonValue} from '@moltendb-web/query';

class FetchTransport implements MoltenTransport {
  constructor(private baseUrl: string, private token: string) {
  }

  async send(action: 'get' | 'set' | 'update' | 'delete', payload: Document): Promise<JsonValue> {
    const res = await fetch(`${this.baseUrl}/${action}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.token}`,
      },
      body: JSON.stringify(payload),
    });
    return res.json();
  }
}

const db = new MoltenDbClient(new FetchTransport('[https://api.mydomain.com](https://api.mydomain.com)', myToken));

Build

npm run build      # Builds CJS, ESM, and type declarations
npm run typecheck  # Type-check without emitting
npm run test       # Run the Jest test suite

Contributing & Feedback

Found a bug or have a feature request? Please open an issue on the GitHub issue tracker.


License

MIT