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

@kysera/repository

v0.8.5

Published

Repository pattern implementation for Kysely with smart validation

Readme

@kysera/repository

Repository pattern implementation with unified plugin support for Kysera.

Note: The createORM function creates a plugin container for the Repository pattern - not a traditional ORM with entity mapping, Unit of Work, or Identity Map.

Version License: MIT TypeScript

Overview

The Repository package provides a repository pattern interface for Kysely with full plugin support via @kysera/executor. It supports flexible validation adapters (Zod, Valibot, TypeBox, or custom), CQRS-lite patterns, and works seamlessly with plugins like soft-delete and RLS.

Key Features:

  • Repository pattern with CRUD operations
  • Unified plugin system via @kysera/executor
  • Flexible validation adapters (Zod, Valibot, TypeBox, custom)
  • Primary key flexibility (single, composite, UUID)
  • Bulk operations and pagination
  • CQRS-lite support (Repository writes + DAL reads)
  • Full TypeScript type safety

Installation

pnpm add @kysera/repository kysely

Optional validation libraries:

# For Zod validation
pnpm add zod

# For Valibot validation
pnpm add valibot

# For TypeBox validation
pnpm add @sinclair/typebox

Quick Start

Basic Repository

import { createRepositoryFactory, nativeAdapter } from '@kysera/repository';
import { Kysely } from 'kysely';

interface User {
  id: number;
  name: string;
  email: string;
}

interface Database {
  users: User;
}

const db: Kysely<Database> = /* ... */;
const factory = createRepositoryFactory(db);

const userRepo = factory.create({
  tableName: 'users',
  mapRow: (row) => row,
  schemas: {
    create: nativeAdapter<{ name: string; email: string }>(),
  },
});

// CRUD operations
const user = await userRepo.create({ name: 'Alice', email: '[email protected]' });
const found = await userRepo.findById(user.id);
const updated = await userRepo.update(user.id, { name: 'Alice Smith' });
await userRepo.delete(user.id);

With Zod Validation

import { createRepositoryFactory, zodAdapter } from '@kysera/repository'
import { z } from 'zod'

const CreateUserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email()
})

const userRepo = factory.create({
  tableName: 'users',
  mapRow: row => row,
  schemas: {
    create: zodAdapter(CreateUserSchema),
    update: zodAdapter(CreateUserSchema.partial())
  }
})

// Validation happens automatically
const user = await userRepo.create({
  name: 'Bob',
  email: 'invalid-email' // Throws validation error
})

Core API

createORM

Create a plugin container with unified plugin management via @kysera/executor.

import { createORM } from '@kysera/repository'
import { softDeletePlugin } from '@kysera/soft-delete'
import { rlsPlugin } from '@kysera/rls'

const orm = await createORM(db, [softDeletePlugin(), rlsPlugin({ schema: rlsSchema })])

Plugin Container Interface:

interface PluginOrm<DB> {
  // Plugin-aware executor (Kysely instance with plugin interception)
  executor: Kysely<DB>

  // Create a repository with plugin support
  createRepository<T>(factory: (executor: Kysely<DB>, applyPlugins: ApplyPluginsFunction) => T): T

  // Apply plugin interceptors to query builders
  applyPlugins<QB>(qb: QB, operation: string, table: string, metadata?: Record<string, unknown>): QB

  // Registered plugins in resolved dependency order
  plugins: readonly Plugin[]

  // Create a DAL context with plugins
  createContext(): DbContext<DB>

  // Execute a transaction with both Repository and DAL patterns
  transaction<T>(fn: (ctx: DbContext<DB>) => Promise<T>): Promise<T>
}

createRepositoryFactory

Create a factory for building type-safe repositories.

import { createRepositoryFactory } from '@kysera/repository'

const factory = createRepositoryFactory(db)

const userRepo = factory.create({
  tableName: 'users',
  primaryKey: 'id', // Optional, default: 'id'
  primaryKeyType: 'number', // Optional, default: 'number'
  mapRow: row => row,
  schemas: {
    create: nativeAdapter<CreateUserInput>(),
    update: nativeAdapter<UpdateUserInput>()
  }
})

Plugin Integration

Plugins work by intercepting queries and extending repository interfaces. The plugin container uses @kysera/executor internally for unified plugin management.

Using Plugins

import { createORM } from '@kysera/repository'
import { softDeletePlugin } from '@kysera/soft-delete'

const orm = await createORM(db, [softDeletePlugin()])

// Create repository with plugin extensions
const userRepo = orm.createRepository((executor, applyPlugins) => {
  const factory = createRepositoryFactory(executor)
  return factory.create({
    tableName: 'users',
    mapRow: row => row,
    schemas: {
      create: nativeAdapter<CreateUserInput>()
    }
  })
})

// Plugin methods are automatically available
await userRepo.create({ name: 'Alice', email: '[email protected]' })
await userRepo.softDelete(1) // Added by soft-delete plugin
await userRepo.restore(1) // Added by soft-delete plugin

Plugin Lifecycle

  1. Validation - Plugins validated for conflicts and dependencies
  2. Resolution - Plugins sorted by priority and dependencies
  3. Initialization - plugin.onInit() called (if defined)
  4. Interception - plugin.interceptQuery() applied to all queries
  5. Extension - plugin.extendRepository() adds methods to repositories

Manual Plugin Application

const orm = await createORM(db, [softDeletePlugin()])

// Manually apply plugins to custom queries
let query = orm.executor.selectFrom('users').selectAll()

query = orm.applyPlugins(query, 'select', 'users', {
  customMetadata: 'value'
})

const users = await query.execute()

CQRS-lite Pattern

Combine Repository writes with DAL reads in the same transaction with shared plugins.

import { createORM } from '@kysera/repository'
import { createQuery } from '@kysera/dal'
import { softDeletePlugin } from '@kysera/soft-delete'

const orm = await createORM(db, [softDeletePlugin()])

// Define DAL query for complex reads
const getUserStats = createQuery((ctx, userId: number) =>
  ctx.db
    .selectFrom('users')
    .leftJoin('posts', 'posts.user_id', 'users.id')
    .select(['users.id', 'users.name', eb => eb.fn.count('posts.id').as('postCount')])
    .where('users.id', '=', userId)
    .groupBy(['users.id', 'users.name'])
    .executeTakeFirst()
)

// Use in transaction with Repository
const result = await orm.transaction(async ctx => {
  // Create repository for writes
  const userRepo = orm.createRepository(executor => {
    const factory = createRepositoryFactory(executor)
    return factory.create({
      tableName: 'users',
      mapRow: row => row,
      schemas: { create: nativeAdapter<CreateUserInput>() }
    })
  })

  // Write: Create user via Repository
  const user = await userRepo.create({
    name: 'Alice',
    email: '[email protected]'
  })

  // Read: Get stats via DAL (plugins automatically applied)
  const stats = await getUserStats(ctx, user.id)

  return { user, stats }
})

Benefits:

  • Separation of concerns (Repository for writes, DAL for complex reads)
  • Shared transaction context
  • Plugins apply to both patterns
  • Full type safety

Repository API

All repositories implement the BaseRepository interface:

Core Operations

// Create
const user = await repo.create({ name: 'Alice', email: '[email protected]' })

// Read
const found = await repo.findById(1)
const all = await repo.findAll()
const filtered = await repo.find({ where: { name: 'Alice' } })
const one = await repo.findOne({ where: { email: '[email protected]' } })

// Update
const updated = await repo.update(1, { name: 'Alice Smith' })

// Delete
const deleted = await repo.delete(1) // Returns true if deleted

Bulk Operations

// Bulk create
const users = await repo.bulkCreate([
  { name: 'Alice', email: '[email protected]' },
  { name: 'Bob', email: '[email protected]' }
])

// Bulk update
const updated = await repo.bulkUpdate([
  { id: 1, data: { name: 'Alice Smith' } },
  { id: 2, data: { name: 'Bob Jones' } }
])

// Bulk delete
const deletedCount = await repo.bulkDelete([1, 2, 3])

Queries

// Count
const total = await repo.count()
const filtered = await repo.count({ where: { active: true } })

// Exists
const exists = await repo.exists({ where: { email: '[email protected]' } })

// Find by IDs
const users = await repo.findByIds([1, 2, 3])

Pagination

Offset-based pagination:

const result = await repo.paginate({
  limit: 10,
  offset: 0,
  orderBy: 'created_at',
  orderDirection: 'desc'
})

console.log(result.items) // Array of entities
console.log(result.total) // Total count
console.log(result.limit) // 10
console.log(result.offset) // 0

Cursor-based pagination:

const result = await repo.paginateCursor({
  limit: 10,
  orderBy: 'created_at',
  orderDirection: 'desc'
})

console.log(result.items) // Array of entities
console.log(result.nextCursor) // { value: Date, id: number }
console.log(result.hasMore) // boolean

// Next page
const nextPage = await repo.paginateCursor({
  limit: 10,
  cursor: result.nextCursor,
  orderBy: 'created_at',
  orderDirection: 'desc'
})

Transactions

await repo.transaction(async trx => {
  const user = await trx
    .insertInto('users')
    .values({ name: 'Alice', email: '[email protected]' })
    .returningAll()
    .executeTakeFirstOrThrow()

  await trx.insertInto('profiles').values({ user_id: user.id, bio: 'Hello!' }).execute()
})

Validation Adapters

The repository supports multiple validation libraries through a unified adapter interface.

Zod Adapter

import { z } from 'zod'
import { zodAdapter } from '@kysera/repository'

const UserSchema = z.object({
  name: z.string().min(1),
  email: z.string().email()
})

const repo = factory.create({
  tableName: 'users',
  mapRow: row => row,
  schemas: {
    create: zodAdapter(UserSchema),
    update: zodAdapter(UserSchema.partial())
  }
})

Valibot Adapter

import * as v from 'valibot'
import { valibotAdapter } from '@kysera/repository'

const UserSchema = v.object({
  name: v.string([v.minLength(1)]),
  email: v.string([v.email()])
})

const repo = factory.create({
  tableName: 'users',
  mapRow: row => row,
  schemas: {
    create: valibotAdapter(UserSchema, v)
  }
})

TypeBox Adapter

import { Type } from '@sinclair/typebox'
import { Value } from '@sinclair/typebox/value'
import { typeboxAdapter } from '@kysera/repository'

const UserSchema = Type.Object({
  name: Type.String({ minLength: 1 }),
  email: Type.String({ format: 'email' })
})

const repo = factory.create({
  tableName: 'users',
  mapRow: row => row,
  schemas: {
    create: typeboxAdapter(UserSchema, Value)
  }
})

Native Adapter (No Validation)

import { nativeAdapter } from '@kysera/repository'

const repo = factory.create({
  tableName: 'users',
  mapRow: row => row,
  schemas: {
    create: nativeAdapter<CreateUserInput>()
  }
})

Custom Adapter

import { customAdapter } from '@kysera/repository'

const isPositiveNumber = customAdapter<number>(data => {
  if (typeof data !== 'number' || data <= 0) {
    throw new Error('Must be a positive number')
  }
  return data
})

Primary Key Configuration

Repositories support flexible primary key configurations.

Default (id: number)

const repo = factory.create({
  tableName: 'users',
  // primaryKey defaults to 'id'
  // primaryKeyType defaults to 'number'
  mapRow: row => row,
  schemas: { create: nativeAdapter() }
})

Custom Column Name

const repo = factory.create({
  tableName: 'users',
  primaryKey: 'user_id',
  primaryKeyType: 'number',
  mapRow: row => row,
  schemas: { create: nativeAdapter() }
})

UUID Primary Key

const repo = factory.create({
  tableName: 'users',
  primaryKey: 'id',
  primaryKeyType: 'uuid',
  mapRow: row => row,
  schemas: { create: nativeAdapter() }
})

Composite Primary Key

const repo = factory.create({
  tableName: 'user_roles',
  primaryKey: ['user_id', 'role_id'],
  primaryKeyType: 'number',
  mapRow: row => row,
  schemas: { create: nativeAdapter() }
})

// Usage with composite key
const userRole = await repo.findById({ user_id: 1, role_id: 2 })
await repo.delete({ user_id: 1, role_id: 2 })

ContextAwareRepository

Abstract base class for repositories that need clean transaction handling via executor switching:

import { ContextAwareRepository } from '@kysera/repository'
import type { Executor } from '@kysera/core'

class UserRepository extends ContextAwareRepository<Database, 'users'> {
  async create(data: { email: string; name: string }): Promise<User> {
    return this.db.insertInto(this.tableName).values(data).returningAll().executeTakeFirstOrThrow()
  }

  async findById(id: number): Promise<User | null> {
    return (
      this.db.selectFrom(this.tableName).selectAll().where('id', '=', id).executeTakeFirst() ?? null
    )
  }
}

// Normal usage
const userRepo = new UserRepository(db, 'users')
const user = await userRepo.findById(1)

// Transaction usage - switch executor cleanly
await db.transaction().execute(async trx => {
  const txUserRepo = userRepo.withExecutor(trx)
  const txPostRepo = postRepo.withExecutor(trx)

  const user = await txUserRepo.create({ email: '[email protected]', name: 'Test' })
  await txPostRepo.create({ userId: user.id, title: 'Hello' })
})

Benefits:

  • Clean API: No executor parameter in every method
  • Type-safe: withExecutor() returns same repository type
  • Preserves instance: Custom properties preserved

Upsert Helpers

Functions for INSERT ... ON CONFLICT DO UPDATE operations:

import { upsert, upsertMany } from '@kysera/repository'

// Single record upsert
const wallet = await upsert(
  db,
  'wallets',
  {
    name: 'Main Wallet',
    balance: 1000
  },
  {
    conflictColumns: ['name'],
    returning: true
  }
)

// Batch upsert
const prices = await upsertMany(
  db,
  'price_history',
  [
    { pair: 'BTC/USD', timestamp: now, price: 50000 },
    { pair: 'ETH/USD', timestamp: now, price: 3000 }
  ],
  {
    conflictColumns: ['pair', 'timestamp'],
    updateColumns: ['price'],
    returning: true
  }
)

// Upsert with specific update columns
await upsert(
  db,
  'users',
  {
    email: '[email protected]',
    name: 'Alice Updated',
    role: 'admin'
  },
  {
    conflictColumns: ['email'],
    updateColumns: ['name'] // Only update name, not role
  }
)

UpsertOptions:

  • conflictColumns: Columns defining the conflict constraint
  • updateColumns: Columns to update (default: all except conflictColumns)
  • returning: Whether to return upserted record(s)

Helper Functions

withPlugins

Create a repository with plugins in one step:

import { withPlugins } from '@kysera/repository'
import { softDeletePlugin } from '@kysera/soft-delete'

const userRepo = await withPlugins(
  (executor, applyPlugins) => {
    const factory = createRepositoryFactory(executor)
    return factory.create({
      tableName: 'users',
      mapRow: row => row,
      schemas: { create: nativeAdapter() }
    })
  },
  db,
  [softDeletePlugin()]
)

createSimpleRepository

Create a repository without schemas (useful for plugins):

import { createSimpleRepository } from '@kysera/repository'

const userRepo = createSimpleRepository(db, 'users', row => row, {
  primaryKey: 'id',
  primaryKeyType: 'number'
})

createRepositoriesFactory

Create a bundle of repositories for use in transactions:

import { createRepositoriesFactory } from '@kysera/repository'

const createRepositories = createRepositoriesFactory({
  users: executor => createUserRepository(executor),
  posts: executor => createPostRepository(executor),
  comments: executor => createCommentRepository(executor)
})

// Use with database instance
const repos = createRepositories(db)
await repos.users.findById(1)

// Use within transaction
await db.transaction().execute(async trx => {
  const repos = createRepositories(trx)
  await repos.users.create({ name: 'Alice' })
  await repos.posts.create({ userId: 1, title: 'Hello' })
})

Architecture

The repository package architecture in v0.7.0:

@kysera/repository
├── createORM
│   └── uses @kysera/executor internally
│       ├── Validates, resolves, and initializes plugins
│       ├── Creates plugin-aware executor (Kysely instance)
│       └── Provides applyPlugins function
├── createRepositoryFactory
│   ├── Creates table operations (selectById, insert, update, etc.)
│   └── Creates base repository (CRUD + pagination + validation)
└── Plugin integration
    ├── interceptQuery: Modifies queries before execution
    └── extendRepository: Adds methods to repository instances

Key design principles:

  • Unified Execution Layer - @kysera/executor provides plugin interception for both Repository and DAL
  • Type Safety - Full TypeScript support with strict typing
  • Plugin Compatibility - Both interceptQuery and extendRepository applied to repositories
  • Transaction Support - Plugins automatically propagate through transactions

Best Practices

  1. Use createORM for plugin management - Let @kysera/executor handle plugin lifecycle
  2. Prefer validation - Use Zod or similar for runtime safety
  3. Use transactions - Wrap related operations in transactions
  4. Leverage CQRS-lite - Use Repository for writes, DAL for complex reads
  5. Bulk operations - Use bulkCreate/bulkUpdate/bulkDelete for efficiency
  6. Cursor pagination - Prefer cursor-based pagination for large datasets

Related Packages

License

MIT