@forinda/kickjs-prisma
Prisma ORM adapter with DI integration, type-safe query building, and PrismaModelDelegate for cast-free repositories. Supports Prisma 5, 6, and 7+.
Installation
# Using the KickJS CLI (recommended)
kick add prisma
# Manual install
pnpm add @forinda/kickjs-prisma @prisma/clientQuick Start (Prisma 5/6)
import { PrismaClient } from '@prisma/client'
import { PrismaAdapter } from '@forinda/kickjs-prisma'
bootstrap({
modules,
adapters: [PrismaAdapter({ client: new PrismaClient(), logging: true })],
})Quick Start (Prisma 7+)
import { PrismaClient } from './generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import pg from 'pg'
import { PrismaAdapter } from '@forinda/kickjs-prisma'
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
const client = new PrismaClient({ adapter: new PrismaPg(pool) })
bootstrap({
modules,
adapters: [PrismaAdapter({ client, logging: true })],
})Configure modules.prismaClientPath in kick.config.ts so kick g module --repo prisma generates the correct import:
export default defineConfig({
modules: {
repo: 'prisma',
prismaClientPath: '@/generated/prisma/client', // Prisma 7+
},
})PrismaAdapter
Implements AppAdapter to manage the Prisma lifecycle:
beforeStart({ container }: AdapterContext)— registers thePrismaClientin the DI container under thePRISMA_CLIENTsymbol. Sets up query logging if enabled.shutdown()— callsprisma.$disconnect()
Options
| Option | Type | Default | Description |
|---|---|---|---|
client | any | required | PrismaClient instance (any Prisma version) |
logging | boolean | false | Log queries — uses $on('query') for Prisma 5/6, $extends for Prisma 7+ |
Repository Approaches
There are two approaches for typing the injected Prisma client in repositories. Choose based on your needs:
Approach 1: PrismaModelDelegate (generated by CLI)
Uses the PrismaModelDelegate interface from @forinda/kickjs-prisma. Works immediately without knowing your Prisma schema — this is what kick g module --repo prisma generates.
Pros: Zero config, no as any, works with any Prisma version. Cons: No field-level autocomplete (methods return unknown).
import { Repository, HttpException, Inject } from '@forinda/kickjs'
import { PRISMA_CLIENT, type PrismaModelDelegate } from '@forinda/kickjs-prisma'
@Repository()
export class PrismaUserRepository {
// Type-narrow to just the 'user' model — no as any needed
@Inject(PRISMA_CLIENT) private prisma!: { user: PrismaModelDelegate }
async findById(id: string) {
// Returns Promise<unknown> — cast at the boundary if needed
return this.prisma.user.findUnique({ where: { id } }) as Promise<User | null>
}
async findAll() {
return this.prisma.user.findMany() as Promise<User[]>
}
async create(dto: CreateUserDTO) {
return this.prisma.user.create({
data: dto as Record<string, unknown>,
}) as Promise<User>
}
async update(id: string, dto: UpdateUserDTO) {
const existing = await this.prisma.user.findUnique({ where: { id } })
if (!existing) throw HttpException.notFound('User not found')
return this.prisma.user.update({
where: { id },
data: dto as Record<string, unknown>,
}) as Promise<User>
}
async delete(id: string) {
await this.prisma.user.deleteMany({ where: { id } })
}
async count() {
return this.prisma.user.count()
}
}Approach 2: Full PrismaClient (manual upgrade)
Import your actual PrismaClient type for full field-level autocomplete, validation on where and data fields, and relation support via include.
Pros: Full Prisma type safety — autocomplete, compile-time field validation. Cons: Requires importing from your generated client path.
import { Repository, HttpException, Inject } from '@forinda/kickjs'
import { PRISMA_CLIENT } from '@forinda/kickjs-prisma'
// Prisma 5/6
import type { PrismaClient } from '@prisma/client'
// Prisma 7+
// import type { PrismaClient } from '@/generated/prisma/client'
@Repository()
export class PrismaUserRepository {
// Full PrismaClient — all models, full autocomplete
@Inject(PRISMA_CLIENT) private prisma!: PrismaClient
async findById(id: string) {
// Full type safety — returns User | null, autocomplete on where fields
return this.prisma.user.findUnique({ where: { id } })
}
async findAll() {
return this.prisma.user.findMany()
}
async create(dto: CreateUserDTO) {
// Prisma validates that dto matches UserCreateInput at compile time
return this.prisma.user.create({ data: dto })
}
async update(id: string, dto: UpdateUserDTO) {
const existing = await this.prisma.user.findUnique({ where: { id } })
if (!existing) throw HttpException.notFound('User not found')
return this.prisma.user.update({ where: { id }, data: dto })
}
async delete(id: string) {
await this.prisma.user.delete({ where: { id } })
}
// Relations with include — fully typed
async findWithPosts(id: string) {
return this.prisma.user.findUnique({
where: { id },
include: { posts: true },
})
}
}When to Use Which
| Scenario | Recommended |
|---|---|
| Scaffolding a new module quickly | PrismaModelDelegate — works immediately |
| Production app with complex queries | Full PrismaClient — field validation + autocomplete |
| Multi-model repos with relations | Full PrismaClient — typed include |
| Libraries or generic code | PrismaModelDelegate — no schema dependency |
PrismaModelDelegate Methods
PrismaModelDelegate Methods
| Method | Signature | Description |
|---|---|---|
findUnique | ({ where, include? }) => Promise<unknown> | Find by unique field |
findFirst | (args?) => Promise<unknown> | Find first match |
findMany | (args?) => Promise<unknown[]> | Find multiple records |
create | ({ data }) => Promise<unknown> | Create a record |
update | ({ where, data }) => Promise<unknown> | Update a record |
delete | ({ where }) => Promise<unknown> | Delete a record |
deleteMany | ({ where? }) => Promise<{ count }> | Delete multiple records |
count | ({ where? }) => Promise<number> | Count records |
PrismaQueryAdapter
Translates ParsedQuery from ctx.qs() into Prisma-compatible findMany arguments.
import type { User } from '@prisma/client'
import { PrismaQueryAdapter, type PrismaQueryConfig } from '@forinda/kickjs-prisma'
const queryAdapter = new PrismaQueryAdapter()
// Type-safe — only User field names accepted in searchColumns
const config: PrismaQueryConfig<User> = {
searchColumns: ['name', 'email'],
}
const args = queryAdapter.build(parsed, config)
const users = await prisma.user.findMany(args)Without the generic, searchColumns accepts any string:
const config: PrismaQueryConfig = {
searchColumns: ['name', 'email'],
}Filter Operator Mapping
| Operator | Prisma Clause | Example Query |
|---|---|---|
eq | { equals: value } | ?filter[status]=eq:active |
neq | { not: value } | ?filter[status]=neq:banned |
gt | { gt: value } | ?filter[age]=gt:18 |
gte | { gte: value } | ?filter[age]=gte:21 |
lt | { lt: value } | ?filter[price]=lt:100 |
lte | { lte: value } | ?filter[price]=lte:50 |
contains | { contains: value, mode: 'insensitive' } | ?filter[name]=contains:john |
starts | { startsWith: value } | ?filter[name]=starts:J |
ends | { endsWith: value } | ?filter[email]=ends:@gmail.com |
in | { in: [...values] } | ?filter[role]=in:admin,editor |
between | { gte: min, lte: max } | ?filter[age]=between:18,65 |
PrismaQueryResult Shape
interface PrismaQueryResult {
where?: Record<string, any>
orderBy?: Record<string, 'asc' | 'desc'>[]
skip?: number
take?: number
}Exports
import {
PrismaAdapter,
PrismaQueryAdapter,
PRISMA_CLIENT,
type PrismaAdapterOptions,
type PrismaModelDelegate,
type PrismaQueryConfig,
type PrismaQueryResult,
} from '@forinda/kickjs-prisma'