prisma-custom-models-generator
v1.0.0
Published
Prisma generator that scaffolds custom models from your schema, without overwriting your code
Maintainers
Readme
Table of Contents
About
Prisma has no way to attach your own methods to a model. This generator writes the boilerplate for you: one file per model, wired to the right Prisma Client type, ready for you to add methods to.
Because those files are meant to be edited, the generator treats them as yours. Once a file exists it is never rewritten, and files it did not create are never touched. See Regeneration is safe.
Prisma documents three ways to add custom methods to a model, and this generator emits all three. Prisma recommends the first:
| Prisma's approach | behavior |
| --- | --- |
| Client extensions ($extends), recommended by Prisma | EXTEND_CLIENT |
| Wrapping a model in a class | WRAP |
| Extending a model delegate with Object.assign | EXTEND |
Requirements
- Node
^20.19 || ^22.12 || >=24.0 - Prisma 6 or 7 (verified against
7.9.1and6.19.3) - Works with both the
prisma-clientand the legacyprisma-client-jsgenerator providers
This generator does not bundle its own copy of Prisma. It reads the already parsed datamodel out of the options Prisma hands it, so it uses whatever Prisma version you installed.
Installation
npm install --save-dev prisma-custom-models-generatoryarn add --dev prisma-custom-models-generatorpnpm add -D prisma-custom-models-generatorUsage
Add the generator to your schema. output is where your custom model files go,
and it should be a directory you are happy to commit, because you will be
writing code in it.
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
generator custom_models {
provider = "prisma-custom-models-generator"
output = "../src/models"
behavior = "EXTEND_CLIENT"
}
datasource db {
provider = "sqlite"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}On Prisma 7 the connection URL lives in prisma.config.ts, not in the schema:
// prisma.config.ts
import { defineConfig } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
datasource: { url: process.env.DATABASE_URL ?? 'file:./dev.db' },
});Then generate:
npx prisma generateThat writes src/models/Users.ts:
// Custom model scaffold generated by prisma-custom-models-generator.
//
// This file is yours to edit. Later `prisma generate` runs detect your
// changes and leave the file alone; the generator only rewrites scaffolds it
// created that are still untouched. Delete the file to get a fresh scaffold.
import { Prisma } from "../generated/prisma/client";
/**
* Custom methods for the `User` model, as a Prisma Client extension.
* ...
*/
export const UsersExtension = Prisma.defineExtension({
name: 'Users',
model: {
user: {
// define methods here, comma-separated
},
},
});Fill in your methods:
export const UsersExtension = Prisma.defineExtension({
name: 'Users',
model: {
user: {
async findByEmail(email: string) {
const ctx = Prisma.getExtensionContext(this);
return (ctx as any).findFirst({ where: { email } });
},
},
},
});And apply the extension. On Prisma 7 the PrismaClient constructor requires a
driver adapter:
import { PrismaClient } from './generated/prisma/client';
import { PrismaBetterSqlite3 } from '@prisma/adapter-better-sqlite3';
import { UsersExtension } from './models/Users';
const adapter = new PrismaBetterSqlite3({ url: 'file:./dev.db' });
export const prisma = new PrismaClient({ adapter }).$extends(UsersExtension);
// now available on the model, fully typed
await prisma.user.findByEmail('[email protected]');Run npx prisma generate again as often as you like. Your findByEmail stays.
Regeneration is safe
Earlier versions deleted the entire output directory on every run. For a tool
whose output exists to hold hand-written code, that made it unusable: your
methods were destroyed each time you ran prisma generate, along with any
unrelated file that happened to live in that directory.
The generator now keeps a small manifest, .prisma-custom-models.json, in the
output directory. It records a hash of every file the generator wrote. That is
enough to tell your work apart from its own:
| Situation | What happens | | --- | --- | | File does not exist | Created | | File exists, generator wrote it, you have not edited it | Rewritten if the scaffold changed | | File exists, generator wrote it, you edited it | Left alone, and the run says so | | File exists, generator never wrote it | Left alone | | Model deleted from your schema, scaffold untouched | Removed | | Model deleted from your schema, scaffold edited | Kept, and the run says so |
Two rules follow from that, and they hold by default:
- The generator only writes a file it created and you have not edited.
- The generator only deletes a file it created and you have not edited.
Commit .prisma-custom-models.json along with your models. Without it the
generator cannot recognise its own past output, so it falls back to the safest
possible reading: everything already on disk belongs to you, and nothing gets
overwritten.
To get a fresh scaffold for one model, delete the file and regenerate. To
overwrite everything, set force = "true".
Behaviors
EXTEND_CLIENT (recommended)
A Prisma Client extension. This is the approach Prisma recommends, and the only
one where your methods appear directly on prisma.user.
import { Prisma } from "../generated/prisma/client";
export const UsersExtension = Prisma.defineExtension({
name: 'Users',
model: {
user: {
// define methods here, comma-separated
},
},
});Prisma.defineExtension type-checks the extension on its own, without needing a
client instance, which is why it is what gets emitted.
WRAP (default)
A class that wraps a single model delegate.
import type { PrismaClient } from "../generated/prisma/client";
export class Users {
constructor(private readonly prismaUser: PrismaClient['user']) {
}
}const users = new Users(prisma.user);EXTEND
Object.assign onto a model delegate. Your methods sit next to the built-in
ones on the returned object.
import type { PrismaClient } from "../generated/prisma/client";
export function Users(prismaUser: PrismaClient['user']) {
return Object.assign(prismaUser, {
// define methods here, comma-separated
});
}const users = Users(prisma.user);
await users.findMany();WRAP remains the default so that existing schemas keep generating what they
generated before.
Options
| Option | Description | Type | Default |
| --- | --- | --- | --- |
| output | Directory the model files are written to | string | ./generated |
| behavior | Which of Prisma's three approaches to emit | WRAP, EXTEND or EXTEND_CLIENT | WRAP |
| force | Overwrite files even if you have edited them | "true" or "false" | "false" |
generator custom_models {
provider = "prisma-custom-models-generator"
output = "../src/models"
behavior = "EXTEND_CLIENT"
force = "false"
}Model names that collide
File names come from pluralising the model name, so a schema containing both
Setting and Settings maps two models onto one Settings.ts. Other pairs
that collide the same way: User/Users, Person/People, Datum/Data.
Generation fails with an error naming the models involved. Rename one of them to proceed. Earlier versions wrote both models to the one path and kept whichever came last, so one of your models silently had no scaffold at all.
Development
npm install
npm run build # tsc -> ./lib
npm run typecheck # tsc --noEmit
npm run lint # prettier --check
npm test # vitest run
npm run gen # build, then run prisma generate on the fixture schemascripts/verify-package.sh is the check that matters before publishing. It
builds the package, npm packs it, installs the tarball into an empty project,
runs npx prisma generate, hand-edits a scaffold, regenerates, and asserts the
edit survived and the result still type-checks:
./scripts/verify-package.sh