Repository Pattern
Kysera's repository pattern provides a clean abstraction over database operations with type safety and validation built-in.
Creating Repositories
Using createORM with Plugins
The recommended way to create repositories in v0.7:
import { createORM } from '@kysera/repository'
import { softDeletePlugin } from '@kysera/soft-delete'
import { z } from 'zod'
// Define validation schemas
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1)
})
const UpdateUserSchema = CreateUserSchema.partial()
// Create ORM with plugins
const orm = await createORM(db, [softDeletePlugin()])
// Define repository factory function
const createUserRepository = (executor, applyPlugins) => ({
tableName: 'users',
executor,
async findById(id) {
return executor
.selectFrom('users')
.selectAll()
.where('id', '=', id)
.executeTakeFirst()
},
async create(data) {
const validated = CreateUserSchema.parse(data)
return executor
.insertInto('users')
.values(validated)
.returningAll()
.executeTakeFirstOrThrow()
},
async update(id, data) {
const validated = UpdateUserSchema.parse(data)
return executor
.updateTable('users')
.set(validated)
.where('id', '=', id)
.returningAll()
.executeTakeFirstOrThrow()
},
// ... other methods
})
// Create repository with plugin support
const userRepo = orm.createRepository(createUserRepository)
// Use repository methods (plugins automatically applied)
const user = await userRepo.findById(1)
// Plugin extension methods also available
await userRepo.softDelete(1)
await userRepo.restore(1)
Alternative: Using Repository Factory (No Plugins)
For simpler use cases without plugins:
import { createRepositoryFactory } from '@kysera/repository'
import { z } from 'zod'
// Create factory
const factory = createRepositoryFactory(db)
// Create repository (no plugin support)
const userRepo = factory.create({
tableName: 'users',
mapRow: (row) => ({
id: row.id,
email: row.email,
name: row.name,
createdAt: row.created_at
}),
schemas: {
create: CreateUserSchema,
update: UpdateUserSchema
}
})
Repository Configuration
interface RepositoryConfig<Table, Entity> {
tableName: string
primaryKey?: string | string[] // Default: 'id'
primaryKeyType?: 'number' | 'string' | 'uuid'
mapRow: (row: Selectable<Table>) => Entity
schemas: {
entity?: z.ZodType<Entity> // Optional result validation
create: z.ZodType // Required
update?: z.ZodType // Optional
}
// Validation controlled via KYSERA_VALIDATION_MODE environment variable
// or NODE_ENV fallback - see Validation guide
}
Repository Methods
Every repository provides these standard methods:
Single Record Operations
// Find by ID
const user = await userRepo.findById(1)
// Create new record
const newUser = await userRepo.create({
email: 'john@example.com',
name: 'John Doe'
})
// Update record
const updated = await userRepo.update(1, { name: 'John Smith' })
// Delete record
const deleted = await userRepo.delete(1)
Batch Operations
// Find multiple by IDs
const users = await userRepo.findByIds([1, 2, 3])
// Bulk create (efficient single query)
const newUsers = await userRepo.bulkCreate([
{ email: 'user1@example.com', name: 'User 1' },
{ email: 'user2@example.com', name: 'User 2' }
])
// Bulk update
const updated = await userRepo.bulkUpdate([
{ id: 1, data: { status: 'active' } },
{ id: 2, data: { status: 'active' } }
])
// Bulk delete
const count = await userRepo.bulkDelete([1, 2, 3])
Query Operations
// Find all
const allUsers = await userRepo.findAll()
// Find with conditions
const activeUsers = await userRepo.find({
where: { status: 'active' }
})
// Find one with conditions
const admin = await userRepo.findOne({
where: { role: 'admin' }
})
// Count records
const count = await userRepo.count({
where: { status: 'active' }
})
// Check existence
const exists = await userRepo.exists({
where: { email: 'test@example.com' }
})
Pagination
// Offset-based pagination
const page = await userRepo.paginate({
limit: 20,
offset: 0,
orderBy: 'created_at',
orderDirection: 'desc'
})
// Returns: { items: User[], total: number, limit: number, offset: number }
// Cursor-based pagination (more efficient for large datasets)
const result = await userRepo.paginateCursor({
limit: 20,
cursor: null, // null for first page
orderBy: 'created_at',
orderDirection: 'desc'
})
// Returns: { items: User[], nextCursor: string | null, hasMore: boolean }
Repository Bundles
With createORM and Plugins
Create multiple repositories with shared plugins:
import { createORM } from '@kysera/repository'
import { softDeletePlugin } from '@kysera/soft-delete'
// Create ORM with plugins
const orm = await createORM(db, [softDeletePlugin()])
// Create all repositories
const userRepo = orm.createRepository(createUserRepository)
const postRepo = orm.createRepository(createPostRepository)
const commentRepo = orm.createRepository(createCommentRepository)
// Use repositories (plugins automatically applied)
const user = await userRepo.findById(1)
// Transaction with orm.transaction() - plugins preserved
await orm.transaction(async (ctx) => {
const user = await userRepo.create({ ... })
// Can also use DAL queries in same transaction
const stats = await getAnalytics(ctx, user.id)
return { user, stats }
})
Without Plugins (Factory Pattern)
For simpler use cases without plugins:
import { createRepositoriesFactory } from '@kysera/repository'
// Define repository creators
const createRepos = createRepositoriesFactory({
users: (executor) => createUserRepository(executor),
posts: (executor) => createPostRepository(executor),
comments: (executor) => createCommentRepository(executor)
})
// Normal usage
const repos = createRepos(db)
const user = await repos.users.findById(1)
// Transaction usage - same API!
await db.transaction().execute(async (trx) => {
const repos = createRepos(trx)
const user = await repos.users.create({ ... })
await repos.posts.create({ user_id: user.id, ... })
})
Custom Primary Keys
Support for different primary key types:
// UUID primary key
const postRepo = factory.create({
tableName: 'posts',
primaryKey: 'uuid',
primaryKeyType: 'uuid',
// ...
})
// Composite primary key
const orderItemRepo = factory.create({
tableName: 'order_items',
primaryKey: ['order_id', 'product_id'],
// ...
})
// Custom primary key name
const accountRepo = factory.create({
tableName: 'accounts',
primaryKey: 'account_number',
primaryKeyType: 'string',
// ...
})
Row Mapping
Transform database rows to domain entities:
interface UserRow {
id: Generated<number>
email: string
first_name: string
last_name: string
created_at: Generated<Date>
}
interface User {
id: number
email: string
fullName: string
createdAt: Date
}
const userRepo = factory.create({
tableName: 'users',
mapRow: (row): User => ({
id: row.id,
email: row.email,
fullName: `${row.first_name} ${row.last_name}`,
createdAt: row.created_at
}),
// ...
})
Transaction Support
Repositories work seamlessly with transactions:
With createORM (Recommended for v0.7+)
import { createORM } from '@kysera/repository'
import { softDeletePlugin } from '@kysera/soft-delete'
const orm = await createORM(db, [softDeletePlugin()])
const userRepo = orm.createRepository(createUserRepository)
const postRepo = orm.createRepository(createPostRepository)
// Use orm.transaction() - plugins preserved automatically
await orm.transaction(async (ctx) => {
// All repos use the same transaction
const user = await userRepo.create({ ... })
await postRepo.create({ user_id: user.id, ... })
// Can also use DAL queries in same transaction
const stats = await getDashboardStats(ctx, user.id)
return { user, stats }
})
Without createORM (Repository Factory)
// Method 1: Using repository bundles (RECOMMENDED)
await db.transaction().execute(async (trx) => {
const repos = createRepos(trx)
// All repos use the same transaction
const user = await repos.users.create({ ... })
await repos.posts.create({ user_id: user.id, ... })
})
// Method 2: Using withTransaction for single repository
await db.transaction().execute(async (trx) => {
const txUserRepo = userRepo.withTransaction(trx)
const user = await txUserRepo.create({ ... })
})
// Method 3: Using transaction method (starts transaction automatically)
await userRepo.transaction(async (trx) => {
// Create transactional repository instances
const txUserRepo = userRepo.withTransaction(trx)
const txPostRepo = postRepo.withTransaction(trx)
const user = await txUserRepo.create({ ... })
await txPostRepo.create({ user_id: user.id, ... })
// Return value becomes the result of transaction()
return user
})
Best Practices
1. Keep Repositories Thin
Repositories should focus on data access only:
// Good - data access only
const user = await userRepo.findById(userId)
// Bad - business logic in repository
const user = await userRepo.findByIdWithValidationAndNotifications(userId)
2. Define Clear Schema Boundaries
Separate schemas for different operations:
const schemas = {
entity: z.object({
id: z.number(),
email: z.string().email(),
name: z.string(),
created_at: z.date()
}),
create: z.object({
email: z.string().email(),
name: z.string().min(1)
}),
update: z.object({
email: z.string().email().optional(),
name: z.string().min(1).optional()
})
}
3. Use Factory Pattern for DI
// Service with injectable repository
class UserService {
constructor(private repos = createRepos(db)) {}
async createUser(data: CreateUserInput) {
return this.repos.users.create(data)
}
}
// Easy to test with mock
const testService = new UserService(createRepos(testDb))