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

drizzle-service

v0.0.13

Published

A service for managing Drizzle ORM entities with advanced CRUD operations, pagination, soft deletes, relations, and bulk operations full type-safe

Readme

Drizzle Service

A powerful, type-safe service layer library for Drizzle ORM that provides advanced CRUD operations, pagination, soft deletes, relations, and bulk operations for PostgreSQL and SQLite.

License: MIT TypeScript

✨ Features

  • 🛡️ Type-Safe: Full TypeScript support with inferred types from your Drizzle schemas
  • 🗄️ Multi-Database: Supports both PostgreSQL and SQLite with database-specific optimizations
  • 📄 Pagination: Built-in offset-based and cursor-based pagination
  • 🔗 Relations: Advanced join operations (left, inner, right joins)
  • 🗑️ Soft Deletes: Configurable soft delete functionality with custom fields
  • Bulk Operations: Efficient bulk create, update, and delete operations
  • 🪝 Lifecycle Hooks: Before/after action hooks for all mutation operations
  • 🏢 Multi-Tenant: Workspace-based filtering for multi-tenant applications
  • 🔧 Extensible: Override and extend default service methods
  • 🎯 Error Handling: Consistent error handling with success/error patterns

📦 Installation

# npm
npm install drizzle-service

# pnpm
pnpm add drizzle-service

# bun
bun add drizzle-service

Peer Dependencies:

npm install drizzle-orm typescript

🚀 Quick Start

1. Define Your Schema

// schema.ts
import { pgTable, serial, text, timestamp, pgEnum } from 'drizzle-orm/pg-core'

export const statusEnum = pgEnum('status', ['active', 'inactive', 'deleted'])

export const users = pgTable('users', (t) => ({
  id: t.serial('id').primaryKey(),
  email: t.text('email').notNull().unique(),
  name: t.text('name').notNull(),
  status: statusEnum('status').default('active').notNull(),
  createdAt: t.timestamp('created_at').notNull().$defaultFn(() => new Date()),
  updatedAt: t.timestamp('updated_at').notNull().$defaultFn(() => new Date()),
}))

2. Setup Database Connection

PostgreSQL

// db.ts
import { drizzle } from 'drizzle-orm/postgres-js'
import { drizzleService } from 'drizzle-service/pg'
import postgres from 'postgres'
import * as schema from './schema'

const client = postgres(process.env.DATABASE_URL!)
const db = drizzle({ client, schema })

// Create service factory
const service = drizzleService(db)

// Create user service with configuration
export const userService = service(schema.users, {
  defaultLimit: 50,
  maxLimit: 500,
  soft: { 
    field: 'status', 
    deletedValue: 'deleted',
    notDeletedValue: 'active' 
  }
})

SQLite

// db.ts
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { drizzleService } from 'drizzle-service/sqlite'
import Database from 'better-sqlite3'
import * as schema from './schema'

const sqlite = new Database('sqlite.db')
const db = drizzle({ client: sqlite, schema })

// Create service factory
const service = drizzleService(db)

// Create user service
export const userService = service(schema.users, {
  defaultLimit: 50,
  maxLimit: 500,
})

3. Use the Service


// main.ts
import { userService } from './db'

async function main() {
  // Create a user
  const [error, user] = await userService.create({
    email: '[email protected]',
    name: 'John Doe'
  })
  
  if (error) throw error
  console.log('Created user:', user)

  // Find users with pagination
  const users = await userService.find({
    page: 1,
    limit: 10,
    orderBy: { createdAt: 'desc' }
  })

  // Find by criteria
  const activeUsers = await userService.findBy({
    status: 'active'
  }, {
    orderBy: { name: 'asc' },
    limit: 20
  })

  // Bulk operations
  const [bulkError, newUsers] = await userService.bulkCreate([
    { email: '[email protected]', name: 'Alice Smith' },
    { email: '[email protected]', name: 'Bob Johnson' }
  ])

  if (bulkError) throw bulkError
  console.log('Created users:', newUsers)
}

📚 API Reference

Query Options Interface Tables

QueryOpts<T, TResult, TRels>

| Property | Type | Description | |----------|------|-------------| | page? | number | Optional page number for pagination | | limit? | number | Optional limit for number of results | | orderBy? | { [P in keyof T['$inferSelect']]?: 'asc' \| 'desc' } | Optional sorting configuration for entity fields | | withDeleted? | boolean | Optional flag to include soft-deleted records | | cursor? | Date \| null | Optional cursor for cursor-based pagination | | relations? | TRels | Optional relations to include in the query | | workspace? | { field: keyof T['$inferSelect'], value: T['$inferSelect'][keyof T['$inferSelect']] } | Optional workspace filtering configuration | | custom? | SQL | Optional custom SQL query | | parse? | Conditional type based on relations | Optional parser function for transforming results |

Parse Function Types

  • Without relations: (data: T['$inferSelect'][]) => TResult
  • With relations: (data: RelationType<T, TRels>[]) => TResult

FindOneOpts<T, TResult, TRelations>

| Property | Type | Description | |----------|------|-------------| | withDeleted? | boolean | Optional flag to include soft-deleted records | | relations? | TRelations | Optional relations to include in the query | | workspace? | { field: keyof T['$inferSelect'], value: T['$inferSelect'][keyof T['$inferSelect']] } | Optional workspace filtering configuration | | custom? | SQL | Optional custom SQL query | | parse? | Conditional type based on relations | Optional parser function for transforming results |

Parse Function Types

  • Without relations: (data: T['$inferSelect'] \| null) => TResult \| null
  • With relations: (data: RelationType<T, TRelations>[] \| null) => TResult \| null

Notes

  • FindOneOpts extends QueryOpts but excludes: page, limit, orderBy, cursor, and parse
  • The parse function signature changes based on whether relations are included
  • T extends BaseEntity in both interfaces
  • Generic type parameters allow for flexible typing of entities, results, and relations

Query Operations

find(options?)

Retrieves all records with optional filtering, pagination, and relations.

const users = await userService.find({
  page: 1,
  limit: 20,
  orderBy: { createdAt: 'desc' },
  where: { status: 'active' }
})

findOne(id, options?)

Finds a single record by primary key.

const user = await userService.findOne(1)

findBy(criteria, options?)

Finds records matching specific criteria.

const activeUsers = await userService.findBy(
  { status: 'active' },
  { limit: 10, orderBy: { name: 'asc' } }
)

findByField(field, value, options?)

Finds records by a specific field and its value.

const usersByStatus = await userService.findByField('status', 'active', {
  limit: 10,
  orderBy: { name: 'asc' }
})

findByMatching(criteria, options?)

Finds records that exactly match the specified criteria.

const exactMatches = await userService.findByMatching(
  { status: 'active', name: 'John Doe' },
  { limit: 5 }
)

findWithCursor(options)

Performs cursor-based pagination for efficient large dataset navigation.

const paginatedResult = await userService.findWithCursor({
  limit: 20,
  cursor: new Date('2023-01-01'),
  orderBy: { createdAt: 'desc' }
})

console.log(paginatedResult.items)
console.log(paginatedResult.nextCursor)
console.log(paginatedResult.pagination)

count(where?)

Counts records matching optional criteria.

const activeUserCount = await userService.count({ status: 'active' })

Mutation Operations

create(data, hooks?)

Creates a new entity with optional lifecycle hooks.

const [error, user] = await userService.create({
  email: '[email protected]',
  name: 'New User'
}, {
  beforeAction: async (data) => {
    console.log('Before creating:', data)
  },
  afterAction: async (result) => {
    console.log('User created:', result.id)
  }
})

if (error) {
  console.error('Creation failed:', error)
} else {
  console.log('Created user:', user)
}

update(id, data, hooks?)

Updates an existing entity.

const [error, user] = await userService.update(1, {
  name: 'Updated Name'
})

if (error) {
  console.error('Update failed:', error)
} else {
  console.log('Updated user:', user)
}

delete(id, hooks?)

Deletes an entity (soft delete if configured).

const result = await userService.delete(1)
console.log('Delete result:', result.success, result.message)

hardDelete(id, hooks?)

Permanently removes an entity from the database.

const result = await userService.hardDelete(1)
console.log('Hard delete result:', result.success, result.message)

restore(id, hooks?)

Restores a soft-deleted entity.

const result = await userService.restore(1)
console.log('Restore result:', result.success, result.message)

Bulk Operations

bulkCreate(data[], hooks?)

Creates multiple entities in a single transaction.

const [error, users] = await userService.bulkCreate([
  { email: '[email protected]', name: 'User 1' },
  { email: '[email protected]', name: 'User 2' }
])

if (error) {
  console.error('Bulk create failed:', error)
} else {
  console.log('Created users:', users)
}

bulkUpdate(updates[], hooks?)

Updates multiple records with different data for each.

const [error, updatedUsers] = await userService.bulkUpdate([
  { id: 1, changes: { name: 'Updated Name 1' } },
  { id: 2, changes: { name: 'Updated Name 2' } }
])

bulkDelete(ids[], hooks?)

Deletes multiple records by their IDs.

const result = await userService.bulkDelete([1, 2, 3])
console.log('Bulk delete result:', result.success, result.message)

bulkHardDelete(ids[], hooks?)

Permanently removes multiple records from the database.

const result = await userService.bulkHardDelete([1, 2, 3])
console.log('Bulk hard delete result:', result.success, result.message)

⚙️ Configuration

Service Options

interface ServiceOptions<T extends BaseEntity> {
  defaultLimit?: number      // Default pagination limit (default: 100)
  maxLimit?: number         // Maximum allowed limit (default: 1000)
  id?: keyof T['$inferSelect'] // Custom ID field (default: 'id')
  soft?: {                  // Soft delete configuration
    field: keyof T['$inferSelect']     // Field to mark as deleted
    deletedValue: T['$inferSelect'][keyof T['$inferSelect']]      // Value indicating deleted state
    notDeletedValue?: T['$inferSelect'][keyof T['$inferSelect']]  // Value indicating active state
  }
  override?: (baseMethods: ServiceMethods<T>) => Partial<ServiceMethods<T>>
}

Lifecycle Hooks

interface ServiceHooks<T extends BaseEntity> {
  beforeAction?: (data: T['$inferSelect']) => Promise<void>
  afterAction?: (data: T['$inferSelect']) => Promise<void>
  onError?: (error: Error) => Promise<void>
}

🎯 Why Drizzle Service?

While Drizzle ORM is powerful and flexible, it can require significant boilerplate for common operations. Drizzle Service eliminates this repetition while maintaining type safety.

Before (Plain Drizzle)

// Manual pagination and error handling
const page = 1
const limit = 20
const offset = (page - 1) * limit
try {
  const users = await db
    .select()
    .from(userTable)
    .where(ne(userTable.status, 'deleted'))
    .limit(limit)
    .offset(offset)
    .orderBy(desc(userTable.createdAt))
    
  const total = await db
    .select({ count: count() })
    .from(userTable)
    .where(ne(userTable.status, 'deleted'))
    
  // Manual result formatting and error handling...
} catch (error) {
  // Manual error handling...
}

After (Drizzle Service)

// Clean, declarative API with built-in pagination and error handling
const users = await userService.find({
  page: 1,
  limit: 20,
  orderBy: { createdAt: 'desc' }
})
// Soft-deleted records are automatically excluded
// Error handling is built into the service layer

📖 Documentation

For complete documentation, examples, and advanced usage, visit: Drizzle Service Documentation

🤝 Contributing

Contributions are welcome! Please read our Contributing Guide for details on our code of conduct and the process for submitting pull requests.

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Built on top of the excellent Drizzle ORM
  • Inspired by Saas-Js and the need for a consistent, type-safe service layer
  • Thanks to all contributors and the TypeScript community

📞 Support


Made with ❤️ by Angel Lopez