Skip to content

Instantly share code, notes, and snippets.

@beeman
Last active August 25, 2025 08:43
Show Gist options
  • Save beeman/8489d84dda58e586e2e34a43b7bfa263 to your computer and use it in GitHub Desktop.
Save beeman/8489d84dda58e586e2e34a43b7bfa263 to your computer and use it in GitHub Desktop.
A Prisma generator that wraps operations in a tryCatch function
#!/usr/bin/env bun
import gh from '@prisma/generator-helper'
import fs from 'node:fs/promises'
import path from 'node:path'
import type { Model } from '@prisma/dmmf'
const header = `// This file was generated by a generator. Do not edit manually.`
// These are the operations we want to wrap in tryCatch.
const operations = [
'Aggregate',
'Count',
'Create',
'CreateMany',
'CreateManyAndReturn',
'Delete',
'DeleteMany',
'FindFirst',
'FindFirstOrThrow',
'FindMany',
'FindUnique',
'FindUniqueOrThrow',
'Update',
'UpdateMany',
'Upsert',
]
// Utility function to convert PascalCase to camelCase
function toCamelCase(str: string) {
return str.charAt(0).toLowerCase() + str.slice(1)
}
gh.generatorHandler({
onManifest() {
return {
defaultOutput: '../src/generated/effect-prisma',
prettyName: 'Prisma Generator',
requiresEngines: ['queryEngine'],
}
},
async onGenerate(options) {
const models = options.dmmf.datamodel.models
const outputDir = options.generator.output?.value
const generatedClients = '../generated/client.js'
const generatedModels = '../generated/models.js'
if (!outputDir) {
throw new Error('No output directory specified')
}
await fs.rm(outputDir, { recursive: true, force: true })
await fs.mkdir(outputDir, { recursive: true })
await generate([...models], { generatedClients, generatedModels, outputDir })
},
})
async function generate(
models: Model[],
{
generatedClients,
generatedModels,
outputDir,
}: {
generatedClients: string
generatedModels: string
outputDir: string
},
) {
await fs.writeFile(
path.join(outputDir, 'index.ts'),
[
// Header and imports
header,
generateImportsClient(generatedClients),
generateImportsModels(models, generatedModels),
// Declarations
generateTryCatchImpl(),
// Operations
generateTryCatchOperations(models),
].join('\n'),
)
}
function generateTryCatchOperations(models: Model[]) {
return models
.map((model) => {
return [
`/**`,
` * ${model.name}`,
` */`,
operations.map((operation) => modelOperationFunction({ model, operation })).join(''),
].join('\n')
})
.join('\n\n')
}
function generateImportsClient(from: string) {
return `import { type Prisma, PrismaClient } from "${from}"`
}
function generateImportsModels(models: Model[], from: string) {
const imports = models.map((model) => `${operations.map((arg) => `${model.name}${arg}Args`).join(',\n ')}`).join(',')
return `import type {\n ${imports} } from "${from}"`
}
function generateTryCatchImpl() {
return `
type Success<T> = {
data: T
error: null
}
type Failure<E> = {
data: null
error: E
}
type Result<T, E = Error> = Success<T> | Failure<E>
export async function tryCatch<T, E = Error>(promise: Promise<T>): Promise<Result<T, E>> {
try {
const data = await promise
return { data, error: null }
} catch (error) {
return { data: null, error: error as E }
}
}
`
}
function modelOperationFunction({
model,
operation,
prefix = '',
}: {
model: Model
operation: string
prefix?: string
}) {
const optionalArgs = [
'CreateMany',
'CreateManyAndReturn',
'DeleteMany',
'FindFirst',
'FindFirstOrThrow',
'FindMany',
].includes(operation)
const setType = ['Aggregate'].includes(operation) ? 'Subset' : 'SelectSubset'
return `
export function ${toCamelCase(`${prefix}${model.name}${operation}`)}<T extends ${model.name}${operation}Args>(
prisma: PrismaClient,
args${optionalArgs ? '?' : ''}: Prisma.${setType}<T, ${model.name}${operation}Args>,
) {
return tryCatch(prisma.${toCamelCase(model.name)}.${toCamelCase(operation)}<T>(args))
}`
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment