|
#!/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) |
|
} |
Do you plan to turn this into a npm package? Would be awesome.