Skip to content

Instantly share code, notes, and snippets.

@jjhiggz
Last active August 6, 2025 13:22
Show Gist options
  • Save jjhiggz/a862dfcbc7f1ffe2027a54afce996ada to your computer and use it in GitHub Desktop.
Save jjhiggz/a862dfcbc7f1ffe2027a54afce996ada to your computer and use it in GitHub Desktop.
Prisma Effect Generator

Prisma Effect Generator

  1. Put the above prisma-effect-generator.ts in your root of your project.

  2. Install "@prisma/generator-helper" and "@prisma/internals" as dev dependecies.

  3. Add this to your schema.prisma file

generator sqlSchema {
  provider = "tsx ./generators/sql-schema-generator.ts"
  output   = "../src/generated"
}
  1. Run bunx prisma migrate dev

How to use:

import { Effect, Layer } from "effect"
import { PrismaClientService, PrismaService } from "./generated/effect-prisma/index.js"
import { PrismaClient } from "./generated/prisma/index.js"

const BasePrismaLayer = Layer.succeed(
  PrismaClientService,
  PrismaClientService.of({
    client: new PrismaClient(),
    tx: new PrismaClient()
  })
)
const program = Effect.gen(function* () {
  const prisma = yield* PrismaService
  console.log(yield* prisma.todo.findMany())
})
  .pipe(Effect.provide(PrismaService.Default))
  .pipe(Effect.provide(BasePrismaLayer))

// const prismaLayer = Layer.succeed(PrismaService, PrismaService.Default)
Effect.runPromise(program)

Todos

*note: the goal of this is that now you can write business logic the same whether it uses a client or a transaction, and reuse that in different contexts`

  • Make an example of providing custom transactions as an alternative to prisma.$transaction(trx => /* do something */)
  • Make a sqlite testing example that creates new db per test
  • Make a postgres testing example that creates new db per tst
#!/usr/bin/env tsx
import gh from "@prisma/generator-helper"
import fs from "node:fs/promises"
import path from "node:path"
const header = `// This file was generated by prisma-effect-generator, do not edit manually.\n`
// Utility function to convert PascalCase to camelCase
function toCamelCase(str) {
return str.charAt(0).toLowerCase() + str.slice(1)
}
gh.generatorHandler({
onManifest() {
return {
defaultOutput: "../src/generated/effect-prisma",
prettyName: "Prisma Effect Generator",
requiresEngines: ["queryEngine"]
}
},
async onGenerate(options) {
const models = options.dmmf.datamodel.models
const outputDir = options.generator.output?.value
if (!outputDir) {
throw new Error("No output directory specified")
}
console.log(`🔄 Generating Effect Prisma services to ${outputDir}`)
// Clean output directory
await fs.rm(outputDir, { recursive: true, force: true })
await fs.mkdir(outputDir, { recursive: true })
// Generate unified index file with PrismaService
await generateUnifiedService([...models], outputDir)
// Generate types file
await generateTypes([...models], outputDir)
console.log(`🎉 Successfully generated unified PrismaService for ${models.length} models`)
}
})
function generateRawSqlOperations() {
return `
// eslint-disable-next-line @typescript-eslint/array-type
$executeRaw: (args: Prisma.Sql | [Prisma.Sql, ...any[]]) =>
Effect.tryPromise({
try: () => (Array.isArray(args) ? client.$executeRaw(args[0], ...args.slice(1)) : client.$executeRaw(args)),
catch: (error) =>
new PrismaError({
error,
operation: "$executeRaw",
model: "Prisma"
})
}),
// eslint-disable-next-line @typescript-eslint/array-type
$executeRawUnsafe: (query: string, ...values: any[]) =>
Effect.tryPromise({
try: () => client.$executeRawUnsafe(query, ...values),
catch: (error) =>
new PrismaError({
error,
operation: "$executeRawUnsafe",
model: "Prisma"
})
}),
// eslint-disable-next-line @typescript-eslint/array-type
$queryRaw: (args: Prisma.Sql | [Prisma.Sql, ...any[]]) =>
Effect.tryPromise({
try: () => (Array.isArray(args) ? client.$queryRaw(args[0], ...args.slice(1)) : client.$queryRaw(args)),
catch: (error) =>
new PrismaError({
error,
operation: "$queryRaw",
model: "Prisma"
})
}),
// eslint-disable-next-line @typescript-eslint/array-type
$queryRawUnsafe: (query: string, ...values: any[]) =>
Effect.tryPromise({
try: () => client.$queryRawUnsafe(query, ...values),
catch: (error) =>
new PrismaError({
error,
operation: "$queryRawUnsafe",
model: "Prisma"
})
}),`
}
function generateModelOperations(models) {
return models
.map((model) => {
const modelName = model.name
const modelNameCamel = toCamelCase(modelName)
return ` ${modelNameCamel}: {
// Find operations
findMany: (args?: Prisma.${modelName}FindManyArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.findMany(args),
catch: (error) =>
new PrismaError({
error,
operation: "findMany",
model: "${modelName}"
})
}),
findUnique: (args: Prisma.${modelName}FindUniqueArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.findUnique(args),
catch: (error) =>
new PrismaError({
error,
operation: "findUnique",
model: "${modelName}"
})
}),
findFirst: (args?: Prisma.${modelName}FindFirstArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.findFirst(args),
catch: (error) =>
new PrismaError({
error,
operation: "findFirst",
model: "${modelName}"
})
}),
findUniqueOrThrow: (args: Prisma.${modelName}FindUniqueOrThrowArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.findUniqueOrThrow(args),
catch: (error) =>
new PrismaError({
error,
operation: "findUniqueOrThrow",
model: "${modelName}"
})
}),
findFirstOrThrow: (args?: Prisma.${modelName}FindFirstOrThrowArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.findFirstOrThrow(args),
catch: (error) =>
new PrismaError({
error,
operation: "findFirstOrThrow",
model: "${modelName}"
})
}),
// Create operations
create: (args: Prisma.${modelName}CreateArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.create(args),
catch: (error) =>
new PrismaError({
error,
operation: "create",
model: "${modelName}"
})
}),
createMany: (args: Prisma.${modelName}CreateManyArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.createMany(args),
catch: (error) =>
new PrismaError({
error,
operation: "createMany",
model: "${modelName}"
})
}),
createManyAndReturn: (args: Prisma.${modelName}CreateManyAndReturnArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.createManyAndReturn(args),
catch: (error) =>
new PrismaError({
error,
operation: "createManyAndReturn",
model: "${modelName}"
})
}),
// Update operations
update: (args: Prisma.${modelName}UpdateArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.update(args),
catch: (error) =>
new PrismaError({
error,
operation: "update",
model: "${modelName}"
})
}),
updateMany: (args: Prisma.${modelName}UpdateManyArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.updateMany(args),
catch: (error) =>
new PrismaError({
error,
operation: "updateMany",
model: "${modelName}"
})
}),
upsert: (args: Prisma.${modelName}UpsertArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.upsert(args),
catch: (error) =>
new PrismaError({
error,
operation: "upsert",
model: "${modelName}"
})
}),
// Delete operations
delete: (args: Prisma.${modelName}DeleteArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.delete(args),
catch: (error) =>
new PrismaError({
error,
operation: "delete",
model: "${modelName}"
})
}),
deleteMany: (args?: Prisma.${modelName}DeleteManyArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.deleteMany(args),
catch: (error) =>
new PrismaError({
error,
operation: "deleteMany",
model: "${modelName}"
})
}),
// Aggregate operations
count: (args?: Prisma.${modelName}CountArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.count(args),
catch: (error) =>
new PrismaError({
error,
operation: "count",
model: "${modelName}"
})
}),
aggregate: (args: Prisma.${modelName}AggregateArgs) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.aggregate(args),
catch: (error) =>
new PrismaError({
error,
operation: "aggregate",
model: "${modelName}"
})
}),
groupBy: <T extends Prisma.${modelName}GroupByArgs>(args: T) =>
Effect.tryPromise({
try: () => client.${modelNameCamel}.groupBy(args as any),
catch: (error) =>
new PrismaError({
error,
operation: "groupBy",
model: "${modelName}"
})
})
}`
})
.join(",\n\n")
}
async function generateUnifiedService(models, outputDir) {
const rawSqlOperations = generateRawSqlOperations()
const modelOperations = generateModelOperations(models)
const serviceContent = `${header}
import { Context, Data, Effect, Layer } from "effect"
import { Service } from "effect/Effect"
import { type Prisma, PrismaClient } from "../prisma/index.js"
export class PrismaClientService extends Context.Tag("PrismaClientService")<
PrismaClientService,
{
tx: PrismaClient | Prisma.TransactionClient
client: PrismaClient
}
>() {}
export const LivePrismaLayer = Layer.effect(
PrismaClientService,
Effect.sync(() => {
const prisma = new PrismaClient()
return {
// The \`tx\` property (transaction) can be shared and overridden,
// but the \`client\` property must always be a PrismaClient instance.
tx: prisma,
client: prisma
}
})
)
export class PrismaError extends Data.TaggedError("PrismaError")<{
error: unknown
operation: string
model: string
}> {}
export class PrismaService extends Service<PrismaService>()("PrismaService", {
effect: Effect.gen(function* () {
const { tx: client } = yield* PrismaClientService
return {
${rawSqlOperations}
${modelOperations}
}
})
}) {}
`
await fs.writeFile(path.join(outputDir, "index.ts"), serviceContent)
}
async function generateTypes(models, outputDir) {
const modelTypeDefinitions = models
.map((model) => {
const modelName = model.name
const modelNameCamel = toCamelCase(modelName)
return ` ${modelNameCamel}: {
findMany: (args?: Prisma.${modelName}FindManyArgs) => Effect.Effect<Array<${modelName}>, PrismaError>
findUnique: (args: Prisma.${modelName}FindUniqueArgs) => Effect.Effect<${modelName} | null, PrismaError>
findFirst: (args?: Prisma.${modelName}FindFirstArgs) => Effect.Effect<${modelName} | null, PrismaError>
findUniqueOrThrow: (args: Prisma.${modelName}FindUniqueOrThrowArgs) => Effect.Effect<${modelName}, PrismaError>
findFirstOrThrow: (args?: Prisma.${modelName}FindFirstOrThrowArgs) => Effect.Effect<${modelName}, PrismaError>
create: (args: Prisma.${modelName}CreateArgs) => Effect.Effect<${modelName}, PrismaError>
createMany: (args: Prisma.${modelName}CreateManyArgs) => Effect.Effect<Prisma.BatchPayload, PrismaError>
createManyAndReturn: (args: Prisma.${modelName}CreateManyAndReturnArgs) => Effect.Effect<Array<${modelName}>, PrismaError>
update: (args: Prisma.${modelName}UpdateArgs) => Effect.Effect<${modelName}, PrismaError>
updateMany: (args: Prisma.${modelName}UpdateManyArgs) => Effect.Effect<Prisma.BatchPayload, PrismaError>
upsert: (args: Prisma.${modelName}UpsertArgs) => Effect.Effect<${modelName}, PrismaError>
delete: (args: Prisma.${modelName}DeleteArgs) => Effect.Effect<${modelName}, PrismaError>
deleteMany: (args?: Prisma.${modelName}DeleteManyArgs) => Effect.Effect<Prisma.BatchPayload, PrismaError>
count: (args?: Prisma.${modelName}CountArgs) => Effect.Effect<number, PrismaError>
aggregate: (args: Prisma.${modelName}AggregateArgs) => Effect.Effect<any, PrismaError>
groupBy: <T extends Prisma.${modelName}GroupByArgs>(args: T) => Effect.Effect<any, PrismaError>
}`
})
.join("\n")
const typeContent = `${header}
import type { Effect } from "effect"
import type { Prisma } from "../prisma/index.js"
import type { PrismaError } from "./index.js"
export type EffectPrismaService = {
// eslint-disable-next-line @typescript-eslint/array-type
$executeRaw: (args: Prisma.Sql | [Prisma.Sql, ...any[]]) => Effect.Effect<number, PrismaError>
// eslint-disable-next-line @typescript-eslint/array-type
$executeRawUnsafe: (query: string, ...values: any[]) => Effect.Effect<number, PrismaError>
// eslint-disable-next-line @typescript-eslint/array-type
$queryRaw: (args: Prisma.Sql | [Prisma.Sql, ...any[]]) => Effect.Effect<unknown, PrismaError>
// eslint-disable-next-line @typescript-eslint/array-type
$queryRawUnsafe: (query: string, ...values: any[]) => Effect.Effect<unknown, PrismaError>
${modelTypeDefinitions}
}
// Individual model types
${models
.map(
(model) => `
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export type ${model.name} = Prisma.${model.name}GetPayload<{}>
`
)
.join("\n")}
`
await fs.writeFile(path.join(outputDir, "types.ts"), typeContent)
}
@maltesa
Copy link

maltesa commented Aug 6, 2025

Do you plan to turn this into a npm package? Would be awesome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment