Skip to content

@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

bash
# Using the KickJS CLI (recommended)
kick add prisma

# Manual install
pnpm add @forinda/kickjs-prisma @prisma/client

Quick Start (Prisma 5/6)

ts
import { PrismaClient } from '@prisma/client'
import { PrismaAdapter } from '@forinda/kickjs-prisma'

bootstrap({
  modules,
  adapters: [PrismaAdapter({ client: new PrismaClient(), logging: true })],
})

Quick Start (Prisma 7+)

ts
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:

ts
export default defineConfig({
  modules: {
    repo: 'prisma',
    prismaClientPath: '@/generated/prisma/client', // Prisma 7+
  },
})

PrismaAdapter

Implements AppAdapter to manage the Prisma lifecycle:

  • beforeStart({ container }: AdapterContext) — registers the PrismaClient in the DI container under the PRISMA_CLIENT symbol. Sets up query logging if enabled.
  • shutdown() — calls prisma.$disconnect()

Options

OptionTypeDefaultDescription
clientanyrequiredPrismaClient instance (any Prisma version)
loggingbooleanfalseLog 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).

ts
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.

ts
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

ScenarioRecommended
Scaffolding a new module quicklyPrismaModelDelegate — works immediately
Production app with complex queriesFull PrismaClient — field validation + autocomplete
Multi-model repos with relationsFull PrismaClient — typed include
Libraries or generic codePrismaModelDelegate — no schema dependency

PrismaModelDelegate Methods

PrismaModelDelegate Methods

MethodSignatureDescription
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.

ts
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:

ts
const config: PrismaQueryConfig = {
  searchColumns: ['name', 'email'],
}

Filter Operator Mapping

OperatorPrisma ClauseExample 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

ts
interface PrismaQueryResult {
  where?: Record<string, any>
  orderBy?: Record<string, 'asc' | 'desc'>[]
  skip?: number
  take?: number
}

Exports

ts
import {
  PrismaAdapter,
  PrismaQueryAdapter,
  PRISMA_CLIENT,
  type PrismaAdapterOptions,
  type PrismaModelDelegate,
  type PrismaQueryConfig,
  type PrismaQueryResult,
} from '@forinda/kickjs-prisma'