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

locality-idb

v1.2.1

Published

SQL-like query builder for IndexedDB with Drizzle-style API

Readme

Locality IDB

SQL-like query builder for IndexedDB with Drizzle-style API

npm version npm downloads bundle size license

API ReferenceExamplesContributing


Why Locality IDB?

IndexedDB is a powerful browser-native database, but its low-level API can be cumbersome and complex to work with. Locality IDB simplifies IndexedDB interactions by providing a modern, type-safe, and SQL-like query builder inspired by Drizzle ORM.


📋 Table of Contents


✨ Features

  • 🎯 Type-Safe: Full TypeScript support with automatic type inference
  • 🔍 SQL-like Queries: Familiar query syntax inspired by Drizzle ORM
  • 🚀 Modern API: Clean and intuitive interface for IndexedDB operations
  • 📦 Zero Dependencies: Lightweight with only development dependencies
  • 🔄 Auto-Generation: Automatic UUID and timestamp generation
  • 🎨 Schema-First: Define your database schema with a simple, declarative API
  • 🛠️ Rich Column Types: Support for various data types including custom types
  • Built-in Validation: Automatic data type validation for built-in column types during insert and update operations
  • 🔧 Custom Validators: Define custom validation logic for columns to enforce complex rules

📦 Installation

# npm
npm install locality-idb

# pnpm
pnpm add locality-idb

# yarn
yarn add locality-idb

🚀 Quick Start

import { Locality, defineSchema, column } from 'locality-idb';

// Define your schema
const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    name: column.text(),
    email: column.text().unique(),
    createdAt: column.timestamp(),
  },
  posts: {
    id: column.int().pk().auto(),
    userId: column.int().index(),
    title: column.varchar(255),
    content: column.text(),
    createdAt: column.timestamp(),
  },
});

// Initialize database
const db = new Locality({
  dbName: 'my-app-db',
  version: 1,
  schema,
});

// Wait for database to be ready (optional)
await db.ready();

// Insert data
const user = await db
  .insert('users')
  .values({
    name: 'Alice',
    email: '[email protected]',
  })
  .run();

// Query data
const users = await db.from('users').findAll();
const alice = await db
  .from('users')
  .where((user) => user.email === '[email protected]')
  .findFirst();

// Update data
await db
  .update('users')
  .set({ name: 'Alice Wonderland' })
  .where((user) => user.id === 1)
  .run();

// Delete data
await db
  .delete('users')
  .where((user) => user.id === 1)
  .run();

🧩 Core Concepts

Schema Definition

Define your database schema using the defineSchema function:

import { defineSchema, column } from 'locality-idb';

const schema = defineSchema({
  tableName: {
    columnName: column.type().modifier(),
    // ... more columns
  },
  // ... more tables
});

Important: Each table must have exactly one primary key defined using .pk(). Having zero or multiple primary keys will result in a runtime error.

// ✅ Valid - single primary key
const validSchema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    name: column.text(),
  },
});

// ❌ Invalid - no primary key (runtime error)
const noPkSchema = defineSchema({
  users: {
    name: column.text(),
  },
});

// ❌ Invalid - multiple primary keys (runtime error)
const multiPkSchema = defineSchema({
  users: {
    id: column.int().pk(),
    uuid: column.uuid().pk(), // Error!
    name: column.text(),
  },
});

Column Types

Locality IDB supports a wide range of column types:

| Type | Description | Example | | ---------------------- | ---------------------------------------------------------- | -------------------------------- | | number() / float() | Numeric values (integer or float) | column.number() | | int() | Numeric values (only integer is allowed) | column.int() | | numeric() | Number or numeric string | column.numeric() | | bigint() | Large integers | column.bigint() | | text() / string() | Text strings | column.text() | | char(length?) | Fixed-length string | column.char(10) | | varchar(length?) | Variable-length string | column.varchar(255) | | bool() / boolean() | Boolean values | column.bool() | | date() | Date objects | column.date() | | timestamp() | ISO 8601 timestamps (auto-generated) | column.timestamp() | | uuid() | UUID strings (auto-generated v4) | column.uuid() | | object<T>() | Generic objects | column.object<UserData>() | | array<T>() | Arrays | column.array<number>() | | list<T>() | Read-only arrays | column.list<string>() | | tuple<T>() | Fixed-size tuples | column.tuple<string, number>() | | set<T>() | Sets | column.set<string>() | | map<K,V>() | Maps | column.map<string, number>() | | custom<T>() | Custom types | column.custom<MyType>() |

Type Extensions

Most column types support generic type parameters for creating branded types, literal unions, or domain-specific types:

Numeric Types (int, float, number)
// Basic usage
const age = column.int();
const price = column.float();
const score = column.number();

// Branded types for type safety
type UserId = Branded<number, 'UserId'>;
type ProductId = Branded<number, 'ProductId'>;

const schema = defineSchema({
  users: {
    id: column.int<UserId>().pk().auto(),
    age: column.int(),
  },
  products: {
    id: column.int<ProductId>().pk().auto(),
    userId: column.int<UserId>(), // Type-safe foreign key
    price: column.float(),
  },
});

// ✅ Type safety prevents mixing IDs
const userId: UserId = 1 as UserId;
const productId: ProductId = 2 as ProductId;
// userId = productId; // ❌ Type error!
String Types (text, string, char, varchar)
// Literal unions for enum-like behavior
type Role = 'admin' | 'user' | 'guest';
type Status = 'draft' | 'published' | 'archived';

const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    role: column.text<Role>().default('user'),
    status: column.string<Status>().default('draft'),
  },
});

// Branded types for domain-specific strings
type Email = Branded<string, 'Email'>;
type URL = Branded<string, 'URL'>;

const profileSchema = defineSchema({
  profiles: {
    id: column.int().pk().auto(),
    email: column.varchar<Email>(255).unique(),
    website: column.varchar<URL>(500).optional(),
  },
});
Boolean Types (bool, boolean)
// Branded booleans for clarity
type EmailVerified = Branded<boolean, 'EmailVerified'>;
type TwoFactorEnabled = Branded<boolean, 'TwoFactorEnabled'>;

const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    emailVerified: column.bool<EmailVerified>().default(false as EmailVerified),
    twoFactorEnabled: column.boolean<TwoFactorEnabled>().default(false as TwoFactorEnabled),
  },
});
Complex Types (object, array, list, tuple, set, map)
// Object with typed structure
interface UserProfile {
  avatar: string;
  bio: string;
  socials: {
    twitter?: string;
    github?: string;
  };
}

// Array of typed elements
interface Comment {
  author: string;
  text: string;
  date: string;
}

// Map with typed keys and values
interface CacheEntry {
  value: any;
  expires: number;
}

const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    profile: column.object<UserProfile>(),
    tags: column.array<string>(),
    comments: column.array<Comment>(),
    permissions: column.set<'read' | 'write' | 'delete'>(),
    cache: column.map<string, CacheEntry>(),
  },
  
  // Tuples for fixed structures
  locations: {
    id: column.int().pk().auto(),
    coordinates: column.tuple<number, number>(), // [latitude, longitude]
    rgbColor: column.tuple<number, number, number>(), // [r, g, b]
  },
  
  // List (readonly array)
  config: {
    id: column.int().pk().auto(),
    allowedOrigins: column.list<string>(), // Immutable at type level
  },
});
Numeric & Bigint with Numeric & bigint Types
// Numeric accepts both number and numeric strings
const schema = defineSchema({
  products: {
    id: column.int().pk().auto(),
    serialNumber: column.numeric(), // Can be 123 or "123"
    largeId: column.bigint(), // For very large integers
  },
});

// Branded Numeric types
type SerialNumber = Branded<Numeric, 'SerialNumber'>;
type SnowflakeId = Branded<bigint, 'SnowflakeId'>;

const advancedSchema = defineSchema({
  items: {
    id: column.int().pk().auto(),
    serial: column.numeric<SerialNumber>(),
    snowflake: column.bigint<SnowflakeId>(),
  },
});

Note: Type extensions are compile-time only and do not affect runtime validation. Use custom validators for runtime type enforcement.

Type Inference

Locality IDB provides powerful type inference utilities:

import type { InferSelectType, InferInsertType, InferUpdateType } from 'locality-idb';

const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    name: column.text(),
    email: column.text().unique(),
    age: column.int().optional(),
    createdAt: column.timestamp(),
  },
});

// Infer types from schema
type User = InferSelectType<typeof schema.users>;
// { id: number; name: string; email: string; age?: number; createdAt: string }

type InsertUser = InferInsertType<typeof schema.users>;
// { name: string; email: string; age?: number; id?: number; createdAt?: string }

type UpdateUser = InferUpdateType<typeof schema.users>;
// { name?: string; email?: string; age?: number; createdAt?: string }

💻 Usage

Initialize Database

import { Locality, defineSchema, column } from 'locality-idb';

const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    name: column.text(),
    email: column.text().unique(),
  },
});

const db = new Locality({
  dbName: 'my-database',
  version: 1,
  schema,
});

// Optional: Wait for database initialization
await db.ready();

Insert Records

Single Insert

const user = await db
  .insert('users')
  .values({
    name: 'John Doe',
    email: '[email protected]',
  })
  .run();

console.log(user); // { id: 1, name: 'John Doe', email: '[email protected]' }

Batch Insert

const users = await db
  .insert('users')
  .values([
    { name: 'Alice', email: '[email protected]' },
    { name: 'Bob', email: '[email protected]' },
  ])
  .run();

console.log(users); // Array of inserted users

Auto-Generated Values

const schema = defineSchema({
  posts: {
    id: column.uuid().pk(),
    title: column.text(),
    createdAt: column.timestamp(),
    isPublished: column.bool().default(false),
  },
});

const post = await db
  .insert('posts')
  .values({ title: 'My First Post' })
  .run();

// id and createdAt are auto-generated, isPublished defaults to false
console.log(post);
// {
//   id: "550e8400-e29b-41d4-a716-446655440000",
//   title: "My First Post",
//   createdAt: "2026-01-29T12:34:56.789Z",
//   isPublished: false
// }

Select/Query Records

Get All Records

const allUsers = await db.from('users').findAll();

Filter with Where

// Predicate-based filtering (in-memory)
const admins = await db
  .from('users')
  .where((user) => user.role === 'admin')
  .findAll();

const activeUsers = await db
  .from('users')
  .where((user) => user.isActive && user.age >= 18)
  .findAll();

// Index-based filtering (optimized) - requires index or primary key
const usersByEmail = await db
  .from('users')
  .where('email', '[email protected]')
  .findAll();

// Range queries with IDBKeyRange
const adults = await db
  .from('users')
  .where('age', IDBKeyRange.bound(18, 65))
  .findAll();

Select Specific Columns

// Include only specified columns
const userNames = await db
  .from('users')
  .select({ name: true, email: true })
  .findAll();
// Returns: Array<{ name: string; email: string }>

// Exclude specified columns
const usersWithoutPassword = await db
  .from('users')
  .select({ password: false })
  .findAll();
// Returns: Array<Omit<User, 'password'>>

Order By

const sortedUsers = await db
  .from('users')
  .orderBy('name', 'asc') // or 'desc'
  .findAll();

// Supports nested keys
const sorted = await db
  .from('users')
  .orderBy('profile.age', 'desc')
  .findAll();

Limit Results

const topTenUsers = await db
  .from('users')
  .orderBy('createdAt', 'desc')
  .limit(10)
  .findAll();

Get First Match

const user = await db
  .from('users')
  .where((user) => user.email === '[email protected]')
  .findFirst();
// Returns: User | null

Find by Primary Key

// Optimized O(1) lookup using IndexedDB's get()
const user = await db.from('users').findByPk(1);
// Returns: User | null

// Works with select projection
const userName = await db
  .from('users')
  .select({ name: true, email: true })
  .findByPk(1);
// Returns: { name: string; email: string } | null

Find by Index

// Find records using an indexed field (optimized index query)
const usersByEmail = await db
  .from('users')
  .findByIndex('email', '[email protected]');
// Returns: User[]

// Find by numeric index
const youngUsers = await db
  .from('users')
  .findByIndex('age', 25);
// Returns: User[]

// Works with all query modifiers
const result = await db
  .from('users')
  .select({ name: true, age: true })
  .findByIndex('age', 30)
  .where((user) => user.isActive)
  .limit(5);

Sort by Index

// Optimized cursor-based sorting (no in-memory sort needed!)
const sortedUsers = await db
  .from('users')
  .sortByIndex('age', 'desc')
  .findAll();

// Combine with limit for efficient pagination
const topTenOldest = await db
  .from('users')
  .sortByIndex('age', 'desc')
  .limit(10)
  .findAll();

// Works with select projection
const names = await db
  .from('users')
  .select({ name: true, age: true })
  .sortByIndex('age', 'asc')
  .findAll();

Note: sortByIndex() uses IndexedDB cursor iteration for optimal performance when where() filter is applied without index.

Chain Multiple Methods

const result = await db
  .from('users')
  .select({ id: true, name: true, email: true })
  .where((user) => user.age >= 18)
  .orderBy('name', 'asc')
  .limit(5)
  .findAll();

Update Records

// Update with condition
const updatedCount = await db
  .update('users')
  .set({ name: 'Jane Doe', age: 30 })
  .where((user) => user.id === 1)
  .run();

console.log(`Updated ${updatedCount} records`);

// Update all matching records
await db
  .update('users')
  .set({ isActive: false })
  .where((user) => user.lastLogin < '2025-01-01')
  .run();

Delete Records

// Delete with condition
const deletedCount = await db
  .delete('users')
  .where((user) => user.id === 1)
  .run();

console.log(`Deleted ${deletedCount} records`);

// Delete multiple records
await db
  .delete('users')
  .where((user) => user.isDeleted === true)
  .run();

📚 API Reference

Locality Class

Constructor

new Locality<DBName, Version, Schema>(config: LocalityConfig)

Parameters:

  • config.dbName: Database name (string)
  • config.version: Database version (optional, default: 1)
  • config.schema: Schema definition object

Example:

const db = new Locality({
  dbName: 'my-database',
  version: 1,
  schema: mySchema,
});

Methods

ready(): Promise<void>

Waits for database initialization to complete.

await db.ready();
from<T>(table: T): SelectQuery<T>

Creates a SELECT query for the specified table.

const query = db.from('users');
insert<T>(table: T): InsertQuery<T>

Creates an INSERT query for the specified table.

const query = db.insert('users');
update<T>(table: T): UpdateQuery<T>

Creates an UPDATE query for the specified table.

const query = db.update('users');
delete<T>(table: T): DeleteQuery<T>

Creates a DELETE query for the specified table.

const query = db.delete('users');

clearTable<T>(table: T): Promise<void>

Clears all records from the specified table.

await db.clearTable('users');

Warning: This will remove all data and cannot be undone.

deleteDB(): Promise<void>

Deletes the entire database (current database).

Note: This method uses the deleteDB utility function internally and closes the database connection before deletion.

await db.deleteDB();

Warning: This will remove all data and cannot be undone.

close(): void

Closes the database connection.

db.close();

getDBInstance(): Promise<IDBDatabase>

Gets the underlying IDBDatabase instance.

const idb = await db.getDBInstance();

seed<T>(table: T, data: InferInsertType<Schema[T]>[]): Promise<InferSelectType<Schema[T]>[]>

Inserts seed data into the specified table.

Note:

  • This is a convenience method for inserting initial data.
  • It uses the insert method internally.
  • It does not clear existing data before inserting.
  • Accepts only an array of records (for single record insertion, use insert().values().run())

Parameters:

  • table: Table name
  • data: Array of records to insert

Returns: Array of inserted record(s)

Example:

await db.seed('users', [
  { name: 'Alice', email: '[email protected]', },
  { name: 'Bob', email: '[email protected]', },
]);

const allUsers = await db.from('users').findAll();

console.log(allUsers);

Schema Functions

defineSchema<Schema extends ColumnRecord, Keys extends keyof Schema>(schema: Schema): SchemaRecord<Schema, Keys>

Defines a database schema from an object mapping table names to column definitions.

Parameters:

  • schema: Object with table names as keys and column definitions as values

Returns: Schema object with typed tables

Example:

const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    name: column.text(),
  },
  posts: {
    id: column.int().pk().auto(),
    title: column.text(),
  },
});

table<T>(name: string, columns: T): Table<T>

Creates a single table definition (alternative to defineSchema).

Parameters:

  • name: Table name
  • columns: Column definitions object

Returns: Table instance

Example:

const userTable = table('users', {
  id: column.int().pk().auto(),
  name: column.text(),
});

Column Modifiers

All column types support the following modifiers:

pk(): Column

Marks the column as the primary key.

column.int().pk()

auto(): Column

Enables auto-increment (only for numeric columns: int, float, number).

column.int().pk().auto()

unique(): Column

Marks the column as unique and creates an index.

column.text().unique()

index(): Column

Creates an index on the column.

column.int().index()

optional(): Column

Makes the column optional (nullable).

column.text().optional()

default<T>(value: T): Column

Sets a default value for the column.

column.bool().default(true)
column.text().default('N/A')

validate(validator: (value: T) => string | null | undefined): Column

Adds custom validation logic to the column. The validation function receives the column value and should return:

  • null or undefined if the value is valid
  • An error message string if the value is invalid

When it runs: During insert and update operations, before data is saved to IndexedDB.

Error Handling: If validation fails, a TypeError is thrown with details about the invalid field.

Precedence: Custom validators override built-in type validation. If you provide a custom validator, the built-in type check for that column will be skipped.

Note:

  • Custom validation is not applied to auto-generated values (e.g. auto-increment, UUID, timestamp). But default values are validated if .default(value) is used.
  • If multiple validators are chained, only the last one is used.
  • Built-in type validation still applies to all other columns without custom validators.
  • If the column is optional, the validator is only called when a value is provided (not undefined).
// Email validation
const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    email: column.text().validate((val) => {
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      return emailRegex.test(val) ? null : 'Invalid email format';
    }),
    age: column.int().validate((val) => {
      if (val < 0) return 'Age cannot be negative';
      if (val > 120) return 'Age must be 120 or less';
      return null; // Valid
    }),
    username: column.text().validate((val) => {
      if (val.length < 3) return 'Username must be at least 3 characters';
      if (!/^[a-zA-Z0-9_]+$/.test(val)) return 'Username can only contain letters, numbers, and underscores';
      return null;
    }),
  },
});

// ✅ Valid insert
await db.insert('users').values({
  email: '[email protected]',
  age: 25,
  username: 'john_doe'
}).run();

// ❌ Throws TypeError: Invalid value for field 'email' in table 'users': Invalid email format
await db.insert('users').values({
  email: 'invalid-email',
  age: 25,
  username: 'john_doe'
}).run();

// ❌ Throws TypeError: Invalid value for field 'age' in table 'users': Age cannot be negative
await db.insert('users').values({
  email: '[email protected]',
  age: -5,
  username: 'john_doe'
}).run();

Combining with .optional():

const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    // Custom validation only runs when value is provided
    bio: column.text().optional().validate((val) => {
      return val.length <= 500 ? null : 'Bio must be 500 characters or less';
    }),
  },
});

// ✅ Valid - bio is optional and omitted
await db.insert('users').values({}).run();

// ✅ Valid - bio is provided and valid
await db.insert('users').values({ bio: 'Short bio' }).run();

// ❌ Throws TypeError - bio provided but exceeds 500 chars
await db.insert('users').values({ bio: 'x'.repeat(501) }).run();

Access the ValidateFn symbol (advanced):

import { ValidateFn } from 'locality-idb';

// Access validator function programmatically
const emailColumn = column.text().validate((val) => { /* ... */ });
const validatorFn = emailColumn[ValidateFn]; // Function reference

Query Methods

SelectQuery Methods

select<Selection>(columns: Selection): SelectQuery

Selects or excludes specific columns.

// Include specific columns
db.from('users').select({ name: true, email: true })

// Exclude specific columns
db.from('users').select({ password: false })
where(predicate: (row: T) => boolean): SelectQuery

Filters rows based on a predicate function.

db.from('users').where((user) => user.age >= 18)
where<IdxKey>(indexName: IdxKey, query: T[IdxKey] | IDBKeyRange): SelectQuery

Filters rows using an indexed field or primary key.

Type Safety: indexName must be either an indexed field or the primary key.

Performance: Uses IndexedDB's optimized index/key query for efficient lookups.

// Using an indexed field
db.from('users').where('age', IDBKeyRange.bound(18, 30))

// Using primary key
db.from('users').where('id', IDBKeyRange.bound(1, 100))
sortByIndex<IdxKey>(indexName: IdxKey, dir?: 'asc' | 'desc'): SelectQuery

Sorts results by an indexed field using IndexedDB cursor iteration (avoiding in-memory sorting).

Type Safety: indexName must be a field with an index or the primary key.

Performance: Uses IndexedDB's cursor for optimized sorting. For large datasets, this is significantly more efficient than in-memory sorting.

For sorting on non-indexed fields, use orderBy() which performs in-memory sorting.

// Optimized cursor-based sort
const sorted = await db.from('users').sortByIndex('age', 'desc').findAll();

// Efficient pagination
const page = await db.from('users').sortByIndex('createdAt', 'desc').limit(20).findAll();
orderBy<Key>(key: Key, direction?: 'asc' | 'desc'): SelectQuery

Orders results by a specified key. Supports nested keys using dot notation.

db.from('users').orderBy('name', 'asc')
db.from('users').orderBy('profile.age', 'desc')

Note: This method performs in-memory sorting. For large datasets, consider using sortByIndex() with an indexed field for better performance.

limit(count: number): SelectQuery

Limits the number of results.

db.from('users').limit(10)
findAll(): Promise<T[]>

Fetches all matching records.

const users = await db.from('users').findAll()
findFirst(): Promise<T | null>

Fetches the first matching record.

const user = await db.from('users').findFirst()
findByPk<Key>(key: Key): Promise<T | null>

Finds a single record by its primary key value using IndexedDB's optimized get() method.

Performance: O(1) lookup

const user = await db.from('users').findByPk(1);
const post = await db.from('posts').findByPk('some-uuid-string');
findByIndex<IdxKey>(indexName: IdxKey, query: T[IdxKey] | IDBKeyRange): Promise<T[]>

Finds records using an indexed field. Only accepts field names that are marked with .index() or .unique().

Type Safety: indexName must be a field with an index.

Performance: Uses IndexedDB's index query for optimized lookups.

// Type-safe: 'email' must be indexed
const users = await db.from('users').findByIndex('email', '[email protected]');

// Works with IDBKeyRange for range queries
const adults = await db.from('users').findByIndex('age', IDBKeyRange.bound(18, 65));

Note:

  • Unique columns are automatically indexed.
  • Unique indexes are recommended for this method to ensure a single result.
count(): Promise<number>

Counts the number of matching records.

const userCount = await db.from('users').where((user) => user.isActive).count()

Note:

  • Uses IndexedDB's optimized count() when:
    • No where() clause is applied, OR
    • where() uses an index or primary key
  • Falls back to in-memory counting when where() uses a predicate function
exists(): Promise<boolean>

Checks if any matching records exist.

const hasAdmins = await db.from('users').where((user) => user.role === 'admin').exists()

Note: This method internally uses count() for checking existence.


InsertQuery Methods

values<T>(data: T | T[]): InsertQuery

Sets the data to insert (single object or array).

db.insert('users').values({ name: 'John' })
db.insert('users').values([{ name: 'John' }, { name: 'Jane' }])
run(): Promise<T | T[]>

Executes the insert query and returns the inserted record(s).

const user = await db.insert('users').values({ name: 'John' }).run()

UpdateQuery Methods

set<T>(values: Partial<T>): UpdateQuery

Sets the values to update.

db.update('users').set({ name: 'Jane', age: 30 })
where(predicate: (row: T) => boolean): UpdateQuery

Filters rows to update.

db.update('users').set({ isActive: false }).where((user) => user.id === 1)
run(): Promise<number>

Executes the update query and returns the number of updated records.

const count = await db.update('users').set({ name: 'Jane' }).run()

DeleteQuery Methods

where(predicate: (row: T) => boolean): DeleteQuery

Filters rows to delete.

db.delete('users').where((user) => user.id === 1)
run(): Promise<number>

Executes the delete query and returns the number of deleted records.

const count = await db.delete('users').where((user) => user.id === 1).run()

Utility Functions

uuidV4(uppercase?: boolean): UUID<'v4'>

Generates a random UUID v4 string.

Parameters:

  • uppercase: Whether to return uppercase format (optional, default: false)

Returns: UUID v4 string

Example:

import { uuidV4 } from 'locality-idb';

const id = uuidV4(); // "550e8400-e29b-41d4-a716-446655440000"
const upperId = uuidV4(true); // "550E8400-E29B-41D4-A716-446655440000"

getTimestamp(value?: string | number | Date): Timestamp

Gets a timestamp in ISO 8601 format from various input types.

Can be used to generate current timestamp or convert existing date inputs to use as timestamp.

Parameters:

  • value: Optional date input:
    • string: ISO date string or any valid date string
    • number: Unix timestamp (milliseconds)
    • Date: Date object

Returns: ISO 8601 timestamp string

Remarks: If no value is provided or the provided value is invalid, the current date and time will be used.

Example:

import { getTimestamp } from 'locality-idb';

// Current timestamp
const now = getTimestamp(); // "2026-01-29T12:34:56.789Z"

// From Date object
const fromDate = getTimestamp(new Date('2025-01-01')); // "2025-01-01T00:00:00.000Z"

// From ISO string
const fromString = getTimestamp('2025-06-15T10:30:00.000Z'); // "2025-06-15T10:30:00.000Z"

// From Unix timestamp
const fromUnix = getTimestamp(1704067200000); // "2024-01-01T00:00:00.000Z"

// Invalid input falls back to current time
const fallback = getTimestamp('invalid'); // Current timestamp

isTimestamp(value: unknown): value is Timestamp

Checks if a value is a valid Timestamp string in ISO 8601 format.

Parameters:

  • value: The value to check

Returns: true if the value is a valid Timestamp, otherwise false

Example:

import { isTimestamp } from 'locality-idb';

isTimestamp('2026-01-29T12:34:56.789Z'); // true
isTimestamp('2026-01-29'); // false (not full ISO 8601)
isTimestamp('invalid'); // false
isTimestamp(123); // false

openDBWithStores(name: string, stores: StoreConfig[], version?: number): Promise<IDBDatabase>

Opens an IndexedDB database with specified stores (low-level API).

Internally used by Locality class. Can be used for custom setups.

Parameters:

  • name: Database name
  • stores: Array of store configurations
  • version: Database version (optional, default: 1)

Returns: Promise resolving to IDBDatabase instance

Example:

import { openDBWithStores } from 'locality-idb';

const db = await openDBWithStores(
    'my-db',
    [
        {
            name: 'users',
            keyPath: 'id',
            autoIncrement: true,
        },
    ],
    1
);

deleteDB(name: string): Promise<void>

Deletes an IndexedDB database by name.

Parameters:

  • name: The name of the database to delete

Returns: A promise that resolves when the database is deleted

Example:

import { deleteDB } from 'locality-idb';
await deleteDB('my-database');

Warning: This will remove all data and cannot be undone.


Validation

Locality IDB includes built-in validation that automatically validates data types for built-in column types during insert and update operations based on your schema definitions.

Automatic Validation

When you insert or update records, Locality IDB automatically validates that the values match their expected column types:

const schema = defineSchema({
  users: {
    id: column.int().pk().auto(),
    name: column.text(),
    age: column.int(),
    email: column.varchar(255),
  },
});

const db = new Locality({ dbName: 'app', schema });

// ✅ Valid - all types match
await db.insert('users').values({ name: 'Alice', age: 25, email: '[email protected]' }).run();

// ❌ Throws TypeError - age must be an integer
await db.insert('users').values({ name: 'Bob', age: 'twenty', email: '[email protected]' }).run();

// ❌ Throws TypeError - email exceeds varchar(255) length
await db.insert('users').values({ name: 'Charlie', age: 30, email: 'a'.repeat(300) }).run();

validateColumnType<T>(type: T, value: unknown): string | null

Manually validate if a value matches the specified column data type.

Parameters:

  • type: The column data type (e.g., 'int', 'text', 'uuid', 'varchar(255)')
  • value: The value to validate

Returns: null if valid, otherwise an error message string

Example:

import { validateColumnType } from 'locality-idb';

validateColumnType('int', 42);          // null (valid)
validateColumnType('int', 'hello');     // "'\"hello\"' is not an integer"
validateColumnType('text', 'hello');    // null (valid)
validateColumnType('uuid', '550e8400-e29b-41d4-a716-446655440000'); // null (valid)
validateColumnType('varchar(5)', 'hi'); // null (valid)
validateColumnType('varchar(5)', 'hello world'); // error message
validateColumnType('numeric', 42);      // null (valid)
validateColumnType('numeric', '3.14');  // null (valid)
validateColumnType('numeric', 'abc');   // error message

Validated Column Types

The following column types are validated:

| Type | Validation Rule | | -------------------------- | --------------------------------------------------------------------------- | | int | Must be an integer | | float / number | Must be a number | | numeric | Must be a number or numeric string | | bigint | Must be a BigInt | | text / string | Must be a string | | char(n) | Must be a string with exactly n characters | | varchar(n) | Must be a string with at most n characters | | bool / boolean | Must be a boolean | | uuid | Must be a valid UUID string | | timestamp | Must be a valid ISO 8601 timestamp string | | date | Must be a Date object | | array / list / tuple | Must be an array | | set | Must be a Set | | map | Must be a Map | | object | Must be an object | | custom | No validation (always passes, custom validation integration coming soon...) |


🔧 Type System

Locality IDB provides comprehensive TypeScript support:

Type Inference Utilities

import type {
  InferSelectType,
  InferInsertType,
  InferUpdateType,
  $InferRow,
} from 'locality-idb';

// InferSelectType: Full row type with all fields
type User = InferSelectType<typeof schema.users>;

// InferInsertType: Insert type with auto-generated fields optional
type InsertUser = InferInsertType<typeof schema.users>;

// InferUpdateType: Partial type for updates (excluding primary key)
type UpdateUser = InferUpdateType<typeof schema.users>;

// $InferRow: Direct inference from column definitions
type UserRow = $InferRow<typeof schema.users.columns>;

Branded Types

Locality IDB uses branded types for better type safety:

import type { UUID, Timestamp } from 'locality-idb';

// UUID types are branded with their version
type UserId = UUID<'v4'>; // Branded UUID v4

// Timestamps are branded ISO 8601 strings
type CreatedAt = Timestamp; // Branded timestamp string

Helper Types

import type {
  GenericObject,
  Prettify,
  NestedPrimitiveKey,
  SelectFields,
} from 'locality-idb';

// GenericObject: Record<string, any>
type MyObj = GenericObject;

// Prettify: Flattens complex types for better readability
type Pretty = Prettify<ComplexType>;

// NestedPrimitiveKey: Extracts nested primitive keys with dot notation
type Keys = NestedPrimitiveKey<{ user: { profile: { age: number } } }>;
// "user.profile.age"

// SelectFields: Projects selected fields from a type
type Selected = SelectFields<User, { name: true; email: true }>;
// { name: string; email: string }

📄 License

MIT © Nazmul Hassan


🔗 Links


Made with ❤️ by Nazmul Hassan

If you find this package useful, please consider giving it a ⭐ on GitHub!