Deposit API Reference
API At a Glance
| Symbol | Purpose | Execution mode | Common gotcha |
|---|---|---|---|
defineSchema() | Declare typed tables and indexes | Sync | Indexes must match keys on stored records |
createIndexedDB() | Create an IndexedDB adapter | Async | Run migrations before large writes in production |
createLocalStorage() | Create a LocalStorage adapter | Sync | Storage limits are lower than IndexedDB |
Package Entry Points
| Import | Purpose |
|---|---|
@vielzeug/deposit | Main API and exported types/classes |
@vielzeug/deposit/core | Pre-bundled standalone build with the same exports |
Factory Functions
defineSchema(schema)
Creates a fully-typed schema definition. The type parameter S maps table names to record types.
Signature:
function defineSchema<S extends Record<string, Record<string, unknown>>>(schema: {
[K in keyof S]: { key: keyof S[K] & string; indexes?: (keyof S[K] & string)[] };
}): Schema<S>;Example:
const schema = defineSchema<{ users: User; posts: Post }>({
users: { key: 'id', indexes: ['name', 'age'] },
posts: { key: 'id', indexes: ['authorId'] },
});createLocalStorage(options)
Creates a LocalStorage-backed adapter.
Signature:
function createLocalStorage<S extends Schema<any>>(options: LocalStorageOptions<S>): Adapter<S>;Options — LocalStorageOptions<S>:
| Property | Type | Description |
|---|---|---|
dbName | string | Namespace prefix for all localStorage keys |
schema | S | Schema object (typically from defineSchema()) |
logger? | Logger | Custom logger; defaults to console |
storeField(field)
Returns the IDB key path for a given record field, accounting for deposit's internal envelope format. Use this inside migrationFn when creating indexes or object stores to stay decoupled from deposit's storage internals.
Signature:
function storeField(field: string): string;
// storeField('email') === 'v.email'Example:
import { storeField } from '@vielzeug/deposit';
const migrationFn: MigrationFn = (db, oldVersion, _newVersion, tx) => {
if (oldVersion < 2) {
tx.objectStore('users').createIndex('email', storeField('email'), { unique: true });
}
};createIndexedDB(options)
Creates an IndexedDB-backed adapter. The database connection is opened lazily on the first operation.
Signature:
function createIndexedDB<S extends Schema<any>>(options: IndexedDBOptions<S>): IndexedDBHandle<S>;Options — IndexedDBOptions<S>:
| Property | Type | Description |
|---|---|---|
dbName | string | IDB database name |
version | number | Database version — required; increment to trigger migrationFn |
schema | S | Schema object (typically from defineSchema()) |
migrationFn? | MigrationFn | Called inside onupgradeneeded on version upgrade |
logger? | Logger | Custom logger; defaults to console |
Adapter Interface
Adapter<S> is the common interface implemented by both adapters.
get(table, key)
Returns the record for the given primary key, or undefined when absent or expired.
get<K extends keyof S>(table: K, key: KeyType<S, K>): Promise<RecordType<S, K> | undefined>getOr(table, key, defaultValue)
Returns the record when present, or defaultValue when absent or expired. The return type is always RecordType<S, K> — never undefined.
getOr<K extends keyof S>(
table: K,
key: KeyType<S, K>,
defaultValue: RecordType<S, K>,
): Promise<RecordType<S, K>>const user = await db.getOr('users', 1, defaultUser); // User — never undefinedgetAll(table)
Returns all live (non-expired) records. The IndexedDB adapter asynchronously evicts expired entries from the store after returning.
getAll<K extends keyof S>(table: K): Promise<RecordType<S, K>[]>getMany(table, keys[])
Batch fetch by a list of primary keys. Missing or expired records are omitted from the result.
getMany<K extends keyof S>(table: K, keys: KeyType<S, K>[]): Promise<RecordType<S, K>[]>put(table, value, ttl?)
Upserts a single record. ttl is the time-to-live in milliseconds. For multiple records use putMany.
put<K extends keyof S>(table: K, value: RecordType<S, K>, ttl?: number): Promise<void>LocalStorage: throws a descriptive
QuotaExceededErrorwhen the storage quota is exceeded.
putMany(table, values[], ttl?)
Upserts multiple records. The same optional ttl is applied to every record.
putMany<K extends keyof S>(table: K, values: RecordType<S, K>[], ttl?: number): Promise<void>patch(table, key, partial)
Merges partial into the existing record and returns the result. Returns undefined when the key is absent or expired. TTL is preserved.
patch<K extends keyof S>(
table: K,
key: KeyType<S, K>,
partial: Partial<RecordType<S, K>>,
ttl?: number,
): Promise<RecordType<S, K> | undefined>delete(table, key)
Removes a single record by key. Silently ignores missing keys. For multiple keys use deleteMany.
delete<K extends keyof S>(table: K, key: KeyType<S, K>): Promise<void>deleteMany(table, keys[])
Removes multiple records by key list. Silently ignores missing keys.
deleteMany<K extends keyof S>(table: K, keys: KeyType<S, K>[]): Promise<void>deleteAll(table)
Removes all records in the given table.
deleteAll<K extends keyof S>(table: K): Promise<void>has(table, key)
Returns true when a live record exists for the given key. Respects TTL.
has<K extends keyof S>(table: K, key: KeyType<S, K>): Promise<boolean>count(table)
Counts records in the given table (see adapter-specific semantics below).
Adapter behavior:
createLocalStorage().count()is TTL-accurate (O(n));createIndexedDB().count()uses nativeIDBObjectStore.count()(O(1), may include not-yet-evicted expired records). Usedb.from(table).count()for a TTL-accurate count on both.
count<K extends keyof S>(table: K): Promise<number>getOrPut(table, key, factory, ttl?)
Returns the cached record when present; otherwise calls factory(), stores the result with optional TTL, and returns it.
getOrPut<K extends keyof S>(
table: K,
key: KeyType<S, K>,
factory: () => RecordType<S, K> | Promise<RecordType<S, K>>,
ttl?: number,
): Promise<RecordType<S, K>>from(table)
Creates a lazy QueryBuilder<T>. No query runs until a terminal method is called.
from<K extends keyof S>(table: K): QueryBuilder<RecordType<S, K>>IndexedDBHandle
IndexedDBHandle<S> extends Adapter<S> with two additional members.
transaction(tables, fn)
Runs fn inside a single readwrite IDB transaction spanning all listed tables. All writes commit atomically — if fn throws, the transaction is aborted and nothing is persisted.
transaction<K extends keyof S>(
tables: K[],
fn: (tx: TransactionContext<S, K>) => Promise<void>,
): Promise<void>TransactionContext<S, K> methods:
| Method | Description |
|---|---|
get(table, key) | Read a record by key |
getOr(table, key, default) | Read a record; return default when absent |
getAll(table) | Read all live records |
getMany(table, keys[]) | Batch read by key list, omitting misses |
put(table, value, ttl?) | Write or upsert a record |
putMany(table, values[], ttl?) | Upsert multiple records |
patch(table, key, partial) | Partial update — returns merged record or undefined |
delete(table, key) | Delete a single record |
deleteMany(table, keys[]) | Delete multiple records |
deleteAll(table) | Delete all records in a table |
has(table, key) | Check existence |
count(table) | Native IDB record count (includes TTL-expired records) |
from(table) | Create a lazy QueryBuilder |
Note:
getOrPutis absent fromTransactionContext. The read-then-conditionally-write pattern is not safely atomic within a shared transaction scope.
count() in transactions: Returns the native IDB count which includes TTL-expired records. Use
(await tx.getAll(table)).lengthfor a TTL-accurate count.
close()
Closes the underlying IDBDatabase connection and resets internal state.
close(): voidQueryBuilder
QueryBuilder<T> is an immutable, lazy pipeline. Each method returns a new instance.
Filtering
equals(field, value)
equals<K extends keyof T>(field: K, value: T[K]): QueryBuilder<T>Strict equality filter (===).
between(field, lower, upper)
between<K extends keyof T>(
field: K,
lower: T[K] extends number | string ? T[K] : never,
upper: T[K] extends number | string ? T[K] : never,
): QueryBuilder<T>Inclusive range filter. The bound types are inferred from the field type, so passing a string bound for a number field is a compile-time error.
startsWith(field, prefix, options?)
startsWith<K extends keyof T>(
field: K,
prefix: string,
options?: { ignoreCase?: boolean },
): QueryBuilder<T>Filters string fields that start with prefix. Case-sensitive by default.
filter(fn)
filter(fn: Predicate<T>): QueryBuilder<T>Filters using a custom predicate.
and(...predicates)
and(...predicates: Predicate<T>[]): QueryBuilder<T>Keeps records that satisfy all predicates.
or(...predicates)
or(...predicates: Predicate<T>[]): QueryBuilder<T>Keeps records that satisfy at least one predicate.
Sorting & Pagination
orderBy(field, direction?)
orderBy<K extends keyof T>(field: K, direction?: 'asc' | 'desc'): QueryBuilder<T>Sorts by field. Default direction is 'asc'.
limit(n)
limit(n: number): QueryBuilder<T>Takes the first n records.
offset(n)
offset(n: number): QueryBuilder<T>Skips the first n records.
page(pageNumber, pageSize)
page(pageNumber: number, pageSize: number): QueryBuilder<T>Slices by 1-based page number. page(2, 10) returns records 11–20.
reverse()
reverse(): QueryBuilder<T>Reverses the order of the result.
Transformation
map(callback)
map<U>(callback: (record: T) => U): ProjectedQuery<U>Transforms each record to a new value. Returns a ProjectedQuery<U> rather than QueryBuilder<U> — U is unconstrained, so primitive projections like map(u => u.name) work correctly. ProjectedQuery<U> exposes the same terminal methods (toArray, first, last, count, [Symbol.asyncIterator]) but is not further chainable.
const names = await db
.from('users')
.map((u) => u.name)
.toArray(); // string[]
const dtos = await db
.from('users')
.map((u) => ({ id: u.id }))
.toArray(); // { id: number }[]search(query, tone?)
search(query: string, tone?: number): QueryBuilder<T>Fuzzy full-text search across all fields, powered by @vielzeug/toolkit. tone controls the match threshold in the range [0, 1] — lower values are more permissive. Defaults to 0.25.
contains(query, fields?)
contains(query: string, fields?: (keyof T & string)[]): QueryBuilder<T>Case-insensitive substring match. When fields is omitted, all string-valued fields are checked.
reduce(callback, initial)
reduce<A>(callback: (accumulator: A, record: T) => A, initial: A): Promise<A>Reduces all matching records to a single value. Applied after all filters, sorting, and pagination operators.
const totalAge = await db.from('users').reduce((sum, u) => sum + u.age, 0);
const ids = await db
.from('users')
.filter((u) => u.active)
.reduce<number[]>((acc, u) => [...acc, u.id], []);Terminals
toArray()
toArray(): Promise<T[]>Executes the pipeline and returns all results.
first()
first(): Promise<T | undefined>Executes the pipeline and returns the first result.
last()
last(): Promise<T | undefined>Executes the pipeline and returns the last result.
count()
count(): Promise<number>Executes the pipeline and returns the count of matching records.
Note:
limit,offset, andpageare applied before counting. Callcount()before adding pagination operators if you need the total match count.
[Symbol.asyncIterator]()
[Symbol.asyncIterator](): AsyncGenerator<T>Enables for await...of iteration.
for await (const record of db.from('users').orderBy('name')) {
process(record);
}Types
ttl
A convenience constant of named duration helpers. Returns raw millisecond values for use with put, putMany, and getOrPut.
const ttl: {
ms(n: number): number; // identity: ttl.ms(500) === 500
seconds(n: number): number; // ttl.seconds(30) === 30_000
minutes(n: number): number; // ttl.minutes(15) === 900_000
hours(n: number): number; // ttl.hours(1) === 3_600_000
days(n: number): number; // ttl.days(1) === 86_400_000
};import { ttl } from '@vielzeug/deposit';
await db.put('sessions', session, ttl.hours(1));
await db.put('cache', entry, ttl.minutes(15));
await db.getOrPut('users', id, fetchUser, ttl.seconds(30));ProjectedQuery<U>
The return type of QueryBuilder.map(). Exposes terminal methods only — it is not chainable with further query operators.
class ProjectedQuery<U> {
toArray(): Promise<U[]>;
first(): Promise<U | undefined>;
last(): Promise<U | undefined>;
count(): Promise<number>;
[Symbol.asyncIterator](): AsyncGenerator<U>;
}Schema<S>
Use defineSchema<S>(schema) rather than constructing this type directly.
type Schema<S> = {
[K in keyof S]: {
key: keyof S[K] & string;
indexes?: (keyof S[K] & string)[];
};
};RecordOf<S, K>
Extracts the record type for table K from a schema S.
export type RecordOf<S extends Schema<any>, K extends keyof S> = /* ... */import type { RecordOf } from '@vielzeug/deposit';
type User = RecordOf<typeof schema, 'users'>; // { id: number; name: string; age: number }KeyOf<S, K>
Extracts the primary key type for table K from a schema S.
export type KeyOf<S extends Schema<any>, K extends keyof S> = /* ... */import type { KeyOf } from '@vielzeug/deposit';
type UserId = KeyOf<typeof schema, 'users'>; // numberMigrationFn
type MigrationFn = (
db: IDBDatabase,
oldVersion: number,
newVersion: number | null,
transaction: IDBTransaction,
) => void;Provide to createIndexedDB to handle schema migrations across database versions.
LocalStorageOptions<S>
type LocalStorageOptions<S extends Schema<any>> = {
dbName: string;
schema: S;
logger?: Logger;
};IndexedDBOptions<S>
type IndexedDBOptions<S extends Schema<any>> = {
dbName: string;
/** Increment to trigger `migrationFn` on next open. Required. */
version: number;
schema: S;
migrationFn?: MigrationFn;
logger?: Logger;
};TransactionContext<S, K>
type TransactionContext<S extends Schema<any>, K extends keyof S> = {
count<T extends K>(table: T): Promise<number>;
delete<T extends K>(table: T, key: KeyType<S, T>): Promise<void>;
deleteAll<T extends K>(table: T): Promise<void>;
deleteMany<T extends K>(table: T, keys: KeyType<S, T>[]): Promise<void>;
from<T extends K>(table: T): QueryBuilder<RecordType<S, T>>;
get<T extends K>(table: T, key: KeyType<S, T>): Promise<RecordType<S, T> | undefined>;
getAll<T extends K>(table: T): Promise<RecordType<S, T>[]>;
getMany<T extends K>(table: T, keys: KeyType<S, T>[]): Promise<RecordType<S, T>[]>;
getOr<T extends K>(table: T, key: KeyType<S, T>, defaultValue: RecordType<S, T>): Promise<RecordType<S, T>>;
has<T extends K>(table: T, key: KeyType<S, T>): Promise<boolean>;
patch<T extends K>(
table: T,
key: KeyType<S, T>,
partial: Partial<RecordType<S, T>>,
): Promise<RecordType<S, T> | undefined>;
put<T extends K>(table: T, value: RecordType<S, T>, ttl?: number): Promise<void>;
putMany<T extends K>(table: T, values: RecordType<S, T>[], ttl?: number): Promise<void>;
};Logger
type Logger = {
error(...args: unknown[]): void;
warn(...args: unknown[]): void;
};Pass a custom logger to createLocalStorage or createIndexedDB to redirect internal warnings. Defaults to console.