Skip to main content

Database, Collections, and Cursors

Learn how to work with databases, collections, and cursors in LioranDB using the TypeScript/JavaScript driver. Understand how to organize data, access collections, query documents, and efficiently iterate through results.

Handles on this page​

  • Db
  • Collection<TSchema>
  • FindCursor<TSchema>
  • AggregationCursor<TResult>

Db​

The public Db surface is:

  • databaseName
  • collection(name)
  • createCollection(name)
  • listCollections()
  • dropCollection(name)
  • dropDatabase()
  • query(collection, filter?, options?)

databaseName​

The selected database for this handle.

collection(name)​

Creates a collection handle.

const db = client.db("default");
const products = db.collection("products");

createCollection(name)​

Creates the collection and returns:

type CreateCollectionResult = {
readonly collection: string;
};

listCollections()​

Returns a readonly array of collection names.

dropCollection(name)​

Drops one collection by name.

dropDatabase()​

Drops the current database handle's database.

query(collection, filter?, options?)​

Database-level query entrypoint that returns a FindCursor.

const rows = await db.query(
"products",
{inStock: true},
{sort: {price: 1}, limit: 10},
).toArray();

Collection<TSchema>​

This is the main typed document handle.

Typed collections​

type Product = {
_id?: string;
sku: string;
title: string;
price: number;
inStock: boolean;
docNumber: number;
category: string;
};

const products = client.db("default").collection<Product>("products");

collectionName​

Returns the collection name string.

Data model types​

The real exported document-related types are:

type Document = Record<string, unknown>;

type OptionalUnlessRequiredId<TSchema> = TSchema extends { _id: unknown }
? TSchema
: Omit<TSchema, "_id"> & { _id?: unknown };

type WithId<TSchema> = Omit<TSchema, "_id"> & {
_id: unknown;
};

Practical meaning:

  • if your schema does not require _id, insert methods allow it to be omitted
  • result rows usually come back as normal documents with _id included when present

Filters in detail​

The exported filter type is:

type Filter<TSchema = Document> = Document & {
readonly [K in Extract<keyof TSchema, string>]?: unknown;
};

This is intentionally broad. The driver accepts:

  • normal field equality filters
  • nested operator objects
  • server-specific query shapes like $text

Examples:

{inStock: true}
{price: {$gte: 10}}
{batchTag: "docs", docNumber: {$gte: 4}}
{
$text: {
$search: "marker",
$field: "title",
},
}

The type system does not try to fully describe every query operator. The final validation boundary is the server.

Query options in detail​

The real exported type is:

interface FindOptions<TSchema = Document> {
readonly limit?: number;
readonly skip?: number;
readonly projection?: Projection<TSchema>;
readonly sort?: Sort;
readonly cursor?: {
readonly token: string;
readonly direction: "next" | "previous";
};
}

limit​

Maximum number of rows to return.

await products.find({inStock: true}, {limit: 5}).toArray();

skip​

Number of matching rows to skip before returning data.

await products.find(
{inStock: true},
{sort: {sku: 1}, skip: 10, limit: 10},
).toArray();

projection​

The real exported type is:

type Projection<TSchema = Document> =
| readonly (Extract<keyof TSchema, string> | string)[]
| Readonly<Record<string, 0 | 1 | boolean>>;

That means these forms are valid:

{projection: ["sku", "title"]}
{projection: {sku: 1, title: 1, _id: 0}}

Examples:

const brief = await products.find(
{},
{projection: ["sku", "title"]},
).toArray();

const shaped = await products.find(
{},
{projection: {sku: 1, title: 1, docNumber: 1}},
).toArray();

sort​

The real exported sort types are:

type SortDirection = 1 | -1 | "asc" | "desc" | "ascending" | "descending";

type Sort =
| Readonly<Record<string, SortDirection>>
| readonly (readonly [string, SortDirection])[]
| readonly {field: string; direction: "asc" | "desc"}[];

All of these are accepted:

{sort: {price: 1}}
{sort: {price: "desc", sku: "asc"}}
{sort: [["price", -1], ["sku", 1]]}
{sort: [{field: "price", direction: "desc"}]}

cursor​

The token-based page-resume shape:

{
cursor: {
token: "opaque-server-token",
direction: "next",
},
}

Most application code will not set this directly. It is primarily for advanced paging flows.

Write methods​

insertOne(document, options?)​

Inserts one document.

const result = await products.insertOne(
{
sku: "bk-001",
title: "Blue Notebook",
price: 14,
inStock: true,
docNumber: 1,
category: "paper",
},
{idempotencyKey: "products-insert-one-001"},
);

console.log(result.insertedId);

insertMany(documents, options?)​

Inserts multiple documents and returns:

type InsertManyResult = {
readonly insertedIds: readonly unknown[];
};

updateOne(filter, update, options?)​

Updates one matching document.

The real options type is:

interface UpdateOptions {
readonly idempotencyKey?: string;
readonly upsert?: boolean;
}

Example:

await products.updateOne(
{sku: "bk-001"},
{$set: {inStock: false}},
{upsert: false, idempotencyKey: "products-update-one-001"},
);

updateMany(filter, update, options?)​

Updates all matching documents.

await products.updateMany(
{category: "tools"},
{$set: {inStock: true}},
{upsert: false},
);

deleteOne(filter, options?)​

Deletes one matching document.

deleteMany(filter, options?)​

Deletes every matching document.

Read methods​

find(filter?, options?)​

Returns a FindCursor<TSchema>.

const docs = await products
.find({inStock: true}, {sort: {price: 1}, limit: 5})
.toArray();

findOne(filter?, options?)​

Returns the first matching document or null.

findManyByIds(ids)​

Looks up a readonly list of ids and returns a readonly array where each entry is:

  • the matching document
  • or null if that id was not found
const rows = await products.findManyByIds([
"id-1",
"id-2",
"missing-id",
]);

The returned array preserves positional alignment with the input ids.

countDocuments(filter?)​

Counts matching documents and returns a number.

The docs test currently verifies:

  • countDocuments() on all inserted test rows
  • countDocuments(filter) on filtered subsets

Example:

const total = await products.countDocuments();
const inStock = await products.countDocuments({inStock: true});

Representative output:

countDocuments output
42

Real query patterns verified by the docs test​

Equality query​

await products.find({sku: "bk-001"}).toArray();

Range query​

await products.find(
{docNumber: {$gte: 4}},
{sort: {docNumber: 1}},
).toArray();
await products.find(
{price: {$gte: 20}},
{sort: {price: 1}},
).toArray();

Sorted and paged query​

await products.find(
{inStock: true},
{sort: {docNumber: 1}, skip: 1, limit: 3},
).toArray();

Projected query​

await products.find(
{inStock: true},
{projection: {docNumber: 1, title: 1}},
).toArray();

Text query​

await products.find({
$text: {
$search: "Marker",
$field: "title",
},
}).toArray();

Index methods​

createIndex(fieldOrDefinition, options?)​

Creates a secondary index.

The real option type is:

interface CreateIndexOptions {
readonly name?: string;
readonly unique?: boolean;
readonly sparse?: boolean;
readonly partialFilter?: Document;
}

The real definition type is:

interface CreateIndexDefinition extends CreateIndexOptions {
readonly field?: string;
readonly fields?: readonly (
| string
| {
readonly field: string;
readonly direction?: SortDirection | "Asc" | "Desc";
}
)[];
}

Examples:

await products.createIndex("sku", {
name: "sku_idx",
unique: true,
});

await products.createIndex({
name: "category_price_idx",
fields: [
{field: "category", direction: "asc"},
{field: "price", direction: "desc"},
],
});

await products.createIndex({
name: "active_price_idx",
fields: ["price"],
partialFilter: {active: true},
});

The docs test verifies that created secondary indexes can be:

  • listed
  • queried against
  • dropped

createTextIndex(field, options?)​

Creates a text index.

The real text-index option type is:

interface TextIndexOptions {
readonly normalize?: boolean;
readonly stopwords?: readonly string[];
}

Example:

await products.createTextIndex("title", {
normalize: true,
stopwords: ["set"],
});

listIndexes()​

Returns readonly CollectionIndexDefinition[].

The real returned definition shape includes:

  • name
  • fields
  • unique
  • sparse
  • partialFilter
  • buildState
  • buildProgress
  • isText
  • implicit

dropIndex(name, options?)​

Drops an index by name and returns:

type DropIndexResult = {
readonly name: string;
};

Aggregation​

aggregate(pipeline, options?)​

Returns an AggregationCursor<TResult>.

The currently exported options type is intentionally minimal:

interface AggregateOptions {
readonly allowDiskUse?: never;
}

That means the driver does not currently expose rich aggregation options. The main control point is the pipeline itself.

Current server-compatible aggregation stages verified by the docs test:

  • $match
  • $group
  • $project for simple field inclusion
  • $skip
  • $limit

Example:

const results = await products.aggregate([
{$match: {inStock: true}},
{$group: {_id: "$category", count: {$sum: 1}}},
]).toArray();

Caveats backed by current testing:

  • aggregation $sort is not documented as supported here
  • grouped row ordering should not be assumed stable

drop(options?)​

Drops the collection and returns:

{
readonly collection: string;
}

FindCursor<TSchema>​

FindCursor is:

  • chainable
  • async iterable
  • lazily paged
  • lifecycle-bound to the client

Query-shaping methods​

  • filter(filter)
  • limit(limit)
  • skip(skip)
  • sort(sort)
  • project(projection)

These methods return a new cursor definition.

Important source-backed rule:

  • once execution starts, changing the definition throws CursorInitializedError

Execution methods​

  • next()
  • tryNext()
  • hasNext()
  • toArray()

Iteration helpers​

  • forEach(iterator)
  • map(mapper)

Lifecycle helpers​

  • rewind()
  • clone()
  • close()

next()​

Returns the next document or null.

tryNext()​

Currently behaves the same as next().

hasNext()​

Returns whether another row is available.

toArray()​

Drains the remaining cursor contents and returns a readonly array.

If you already consumed some rows with next(), toArray() returns only the remaining rows.

forEach()​

Runs a sync or async callback for each row.

map()​

Transforms rows and returns a readonly array of mapped values.

rewind()​

Resets local consumption state so the cursor can be read again from the same definition.

clone()​

Creates a fresh cursor with the same definition.

Use this when you want multiple independent consumers.

Mutation rule​

Once execution has started, query-shaping methods are no longer allowed.

Representative runtime error:

Cursor error example
Cursor options cannot be changed after execution has started. Clone the cursor first if you need a variant.

AggregationCursor<TResult>​

The aggregation cursor supports:

  • next()
  • tryNext()
  • hasNext()
  • toArray()
  • forEach()
  • map()
  • rewind()
  • clone()
  • close()

Important implementation detail from the real source:

  • FindCursor pages incrementally
  • AggregationCursor fetches the full aggregation result set when first loaded

So for large flows:

  • use find() when you want more streaming-like pagination
  • use aggregate() when you want a derived result set and can tolerate full-result materialization

Cursor cancellation​

The real cursor operation type is:

interface CursorOperationOptions {
readonly signal?: AbortSignal;
}

That signal can be passed to methods like:

await cursor.next({signal});
await cursor.toArray({signal});
await aggregateCursor.toArray({signal});