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

@hammr/do-orm

v1.0.1

Published

Type-safe ORM for Cloudflare Durable Objects with zero runtime overhead

Readme

DO-ORM

Type-safe ORM for Cloudflare Durable Objects with zero runtime overhead

DO-ORM makes Durable Objects queryable like a real database while maintaining the performance and simplicity of Cloudflare's storage API. Built with pure TypeScript, zero dependencies, and automatic schema validation.

Features

Type-safe schema definitions - Full TypeScript inference for all CRUD operations
Automatic validation - Schema validation on every write operation
Efficient indexing - Single-field indexes for O(log n) queries instead of O(n) scans
Fluent query builder - Chain .where(), .after(), .before(), .limit(), .orderBy()
Full CRUD support - create(), find(), update(), delete(), and bulk operations
Zero dependencies - Pure TypeScript using DO storage primitives
Zero runtime overhead - Direct wrapper around Durable Objects storage API

Installation

npm install @hammr/do-orm

Quick Start

1. Define your model

import { DOModel, SchemaDefinition, InferSchemaType } from '@hammr/do-orm';

// Define schema with type annotations
interface EventSchema extends SchemaDefinition {
  id: 'string';
  workspaceId: 'string';
  timestamp: 'date';
  type: 'string';
  data: 'object';
}

// Create model class
class Event extends DOModel<EventSchema> {
  protected schema: EventSchema = {
    id: 'string',
    workspaceId: 'string',
    timestamp: 'date',
    type: 'string',
    data: 'object',
  };
  
  // Define indexes for efficient queries
  protected indexes = ['workspaceId', 'timestamp'] as const;
}

2. Use in your Durable Object

export class MyDurableObject {
  private eventModel: Event;

  constructor(state: DurableObjectState) {
    this.eventModel = new Event(state.storage);
  }

  async fetch(request: Request): Promise<Response> {
    // Create an event
    const event = await this.eventModel.create({
      id: 'evt_123',
      workspaceId: 'ws_abc',
      timestamp: new Date(),
      type: 'click',
      data: { button: 'submit' }
    });

    // Query events
    const recentEvents = await this.eventModel
      .where({ workspaceId: 'ws_abc' })
      .after(new Date('2024-01-01'))
      .limit(100)
      .orderBy('timestamp', 'desc')
      .execute();

    return new Response(JSON.stringify(recentEvents));
  }
}

API Reference

Schema Types

DO-ORM supports the following field types:

  • 'string' - String values
  • 'number' - Numeric values (integers and floats)
  • 'boolean' - Boolean values (true/false)
  • 'date' - Date objects (automatically serialized/deserialized)
  • 'object' - Plain JavaScript objects
  • 'array' - Arrays of any type

CRUD Operations

create(data: T): Promise<T>

Create a new record. Throws if validation fails or ID already exists.

const event = await eventModel.create({
  id: 'evt_1',
  workspaceId: 'ws_abc',
  timestamp: new Date(),
  type: 'pageview',
  data: { page: '/home' }
});

find(id: string): Promise<T | null>

Find a record by ID. Returns null if not found.

const event = await eventModel.find('evt_1');
if (event) {
  console.log(event.type); // Type-safe access
}

update(id: string, updates: Partial<T>): Promise<T>

Update a record with partial data. Validates the complete merged record.

const updated = await eventModel.update('evt_1', {
  data: { page: '/about' }
});

delete(id: string): Promise<boolean>

Delete a record by ID. Returns true if deleted, false if not found.

const deleted = await eventModel.delete('evt_1');

all(): Promise<T[]>

Get all records (unfiltered).

const allEvents = await eventModel.all();

count(): Promise<number>

Count all records.

const totalEvents = await eventModel.count();

Query Builder

Chain query methods for powerful filtering and sorting:

where(conditions: Partial<T>): QueryBuilder<T>

Filter by field values. Uses indexes when available.

const events = await eventModel
  .where({ workspaceId: 'ws_abc' })
  .execute();

after(date: Date): QueryBuilder<T>

Filter records with date fields after the specified date.

const recentEvents = await eventModel
  .after(new Date('2024-01-01'))
  .execute();

before(date: Date): QueryBuilder<T>

Filter records with date fields before the specified date.

const oldEvents = await eventModel
  .before(new Date('2023-12-31'))
  .execute();

limit(count: number): QueryBuilder<T>

Limit the number of results returned.

const topEvents = await eventModel
  .where({ workspaceId: 'ws_abc' })
  .limit(10)
  .execute();

orderBy(field: keyof T, direction: 'asc' | 'desc'): QueryBuilder<T>

Sort results by a field.

const sortedEvents = await eventModel
  .where({ workspaceId: 'ws_abc' })
  .orderBy('timestamp', 'desc')
  .execute();

execute(): Promise<T[]>

Execute the query and return results.

const events = await eventModel
  .where({ workspaceId: 'ws_abc' })
  .limit(100)
  .execute();

Query Chaining Example

const events = await eventModel
  .where({ workspaceId: 'ws_abc' })
  .after(new Date('2024-01-01'))
  .before(new Date('2024-12-31'))
  .orderBy('timestamp', 'desc')
  .limit(50)
  .execute();

Indexing

Indexes dramatically improve query performance by avoiding full table scans:

  • Without index: O(n) - scans every record
  • With index: O(log n) - uses sorted index lookup

How to define indexes

class Event extends DOModel<EventSchema> {
  protected schema: EventSchema = {
    id: 'string',
    workspaceId: 'string',
    timestamp: 'date',
    type: 'string',
  };
  
  // Index these fields for efficient queries
  protected indexes = ['workspaceId', 'timestamp'] as const;
}

When queries use indexes

  • .where({ indexedField: value }) - Uses index if first field is indexed
  • Without indexed where clause - Falls back to full scan

Index maintenance

Indexes are automatically maintained:

  • Created during create()
  • Updated during update() (if indexed fields change)
  • Removed during delete()

Schema Validation

DO-ORM validates all data against your schema:

// ✅ Valid - passes validation
await eventModel.create({
  id: 'evt_1',
  workspaceId: 'ws_abc',
  timestamp: new Date(),
  type: 'click',
  data: {}
});

// ❌ Invalid - throws error
await eventModel.create({
  id: 'evt_1',
  workspaceId: 123, // Error: must be string
  timestamp: new Date(),
  type: 'click',
  data: {}
});

// ❌ Invalid - throws error
await eventModel.create({
  id: 'evt_1',
  // Missing required fields
});

Validation errors

try {
  await eventModel.create(invalidData);
} catch (error) {
  // "Field 'workspaceId' must be a string, got number"
  // "Missing required field: timestamp"
}

TypeScript Inference

DO-ORM provides full type inference:

// Define schema
interface EventSchema extends SchemaDefinition {
  id: 'string';
  workspaceId: 'string';
  timestamp: 'date';
}

class Event extends DOModel<EventSchema> {
  protected schema: EventSchema = {
    id: 'string',
    workspaceId: 'string',
    timestamp: 'date',
  };
  protected indexes = ['workspaceId'] as const;
}

// TypeScript knows the exact type!
const event = await eventModel.find('evt_1');
//    ^? Event | null

if (event) {
  event.id;           // string
  event.workspaceId;  // string
  event.timestamp;    // Date
  event.unknown;      // ❌ TypeScript error
}

Advanced Usage

Custom table names

class Event extends DOModel<EventSchema> {
  constructor(storage: DurableObjectStorage) {
    super(storage, 'custom_events_table');
  }
  
  protected schema: EventSchema = { /* ... */ };
  protected indexes = [] as const;
}

Multiple models in one DO

export class MyDurableObject {
  private events: Event;
  private users: User;

  constructor(state: DurableObjectState) {
    this.events = new Event(state.storage);
    this.users = new User(state.storage, 'users_table');
  }

  async fetch(request: Request): Promise<Response> {
    const event = await this.events.find('evt_1');
    const user = await this.users.find('user_1');
    // ...
  }
}

Performance Considerations

Index usage

  • Indexed queries: Fast O(log n) lookups
  • Non-indexed queries: Slower O(n) full scans
  • Best practice: Index frequently queried fields

Storage efficiency

  • Records stored as: {tableName}:{id}
  • Indexes stored as: index:{tableName}:{field}:{value}
  • Dates serialized as ISO strings for efficient sorting

Query optimization tips

  1. Use indexes - Define indexes for frequently queried fields
  2. Limit results - Always use .limit() for large datasets
  3. Specific where clauses - Filter by indexed fields first
  4. Batch operations - Consider batching writes for bulk inserts

Limitations

  • No compound indexes - Only single-field indexes (for now)
  • No transactions - Each operation is atomic but not grouped
  • No joins - Each model is independent
  • No migrations - Schema changes require manual data migration

Examples

Analytics events tracker

interface AnalyticsSchema extends SchemaDefinition {
  id: 'string';
  sessionId: 'string';
  userId: 'string';
  event: 'string';
  timestamp: 'date';
  properties: 'object';
}

class Analytics extends DOModel<AnalyticsSchema> {
  protected schema: AnalyticsSchema = {
    id: 'string',
    sessionId: 'string',
    userId: 'string',
    event: 'string',
    timestamp: 'date',
    properties: 'object',
  };
  
  protected indexes = ['userId', 'sessionId', 'timestamp'] as const;
}

// Track an event
await analytics.create({
  id: generateId(),
  sessionId: 'session_abc',
  userId: 'user_123',
  event: 'purchase',
  timestamp: new Date(),
  properties: { amount: 99.99, currency: 'USD' }
});

// Get user's recent events
const userEvents = await analytics
  .where({ userId: 'user_123' })
  .after(thirtyDaysAgo)
  .orderBy('timestamp', 'desc')
  .limit(100)
  .execute();

Task queue

interface TaskSchema extends SchemaDefinition {
  id: 'string';
  status: 'string';
  priority: 'number';
  createdAt: 'date';
  payload: 'object';
}

class Task extends DOModel<TaskSchema> {
  protected schema: TaskSchema = {
    id: 'string',
    status: 'string',
    priority: 'number',
    createdAt: 'date',
    payload: 'object',
  };
  
  protected indexes = ['status', 'priority'] as const;
}

// Add task
await task.create({
  id: 'task_1',
  status: 'pending',
  priority: 1,
  createdAt: new Date(),
  payload: { action: 'send_email' }
});

// Get pending tasks
const pending = await task
  .where({ status: 'pending' })
  .orderBy('priority', 'asc')
  .limit(10)
  .execute();

// Process and mark complete
for (const t of pending) {
  await processTask(t);
  await task.update(t.id, { status: 'completed' });
}

Testing

Unit Tests

Run the unit test suite:

npm test

Tests include:

  • Schema validation (type checking, required fields)
  • CRUD operations (create, read, update, delete)
  • Query builder (where, limit, orderBy, date ranges)
  • Index usage and maintenance
  • Edge cases (duplicates, missing records)

Integration Tests (with Cloudflare Workers)

Test the ORM in a real Cloudflare Workers environment:

# Terminal 1: Start the worker
npm run dev

# Terminal 2: Run integration tests
npm run test:worker

The integration tests verify the complete stack:

  • Worker HTTP endpoints
  • Durable Object instantiation
  • DO-ORM with real DO storage
  • Schema validation in production
  • Query performance with indexes

See TESTING.md for more details on testing with Cloudflare Workers.

Contributing

This is v1 - there's lots of room for improvement!

Potential enhancements:

  • Compound indexes (multiple fields)
  • Transactions support
  • Query result streaming
  • Migration helpers
  • Soft deletes
  • Hooks (beforeCreate, afterUpdate, etc.)

License

Apache-2.0


Built for the Cloudflare Workers ecosystem. Works seamlessly with Durable Objects and provides a better developer experience than raw storage API calls.