Luke Howsam
Software Engineer
What is a prisma generator?
A prisma Generator is a tool that introspects your Prisma schema and outputs code from it. If you've used Prisma before, you should already be quite familiar with prisma-client-js
which generates a prisma client and TypeScript types from your schema.
When you're working with Prisma, you'll often notice that there are a few things you always want to change after modifying your prisma Schema, and that is where generators come into the picture.
Now that we've gone over a high level overview of what Prisma generators are, let's go over a real world example where we build a custom generator to generate custom enums and publish it to NPM
How to create a custom Prisma generator
Create a new NPM project
First we'll create a new folder and initialize a new git + NPM project
mkdir prisma-ts-enums && cd prisma-ts-enums && git init . && pnpm init
Install dependencies
Secondly, we'll install all the required dependencies
Production dependencies:
pnpm i @prisma/generator-helper
Dev dependencies:
pnpm i -D @prisma/client @types/node typescript prisma prettier eslint eslint-config-airbnb eslint-config-prettier eslint-plugin-import eslint-import-resolver-typescript lint-staged husky @typescript-eslint/eslint-plugin
We won't go over the linting dependencies too much for brevity's sake. You can find a copy of the linting config files on the repo. These config files are optional and are not required to follow along with this tutorial
The important file that we need to include is the tsconfig.json
file in order to build the project correctly and enforce good TypeScript practices throughout the project:
{
"compilerOptions": {
"allowJs": true,
"alwaysStrict": true,
"allowSyntheticDefaultImports": true,
"module": "esnext",
"moduleResolution": "node",
"outDir": "./dist",
"noEmit": true,
"noImplicitAny": true,
"strict": true,
"strictNullChecks": true,
"jsx": "preserve",
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"baseUrl": ".",
},
"exclude": ["node_modules"],
"include": ["src"]
}
Now we're ready to start writing our new generator
Create a prisma generator
Now with the setup out of the way, we'll get started with creating the generator. We will seperate each part of the generator into its own file for organisation purposes. Create a new file in the src folder, call it generator.ts
and add the following code:
// src/generator.ts
import { generatorHandler } from "@prisma/generator-helper";
import onManifest from "./onManifest";
import { onGenerate } from "./onGenerate";
generatorHandler({
onManifest: onManifest,
onGenerate: onGenerate,
});
Let's start with writing the onManifest
function. This allows us to specify defaults & other information for our generator. Create a new file called onManifest
in src
and add the following code:
// src/onManifest.ts
import type { GeneratorManifest } from '@prisma/generator-helper';
export default function onManifest(): GeneratorManifest {
return {
defaultOutput: './types',
prettyName: 'Prisma enum custom generator',
version: '0.0.1',
};
}
Now we'll move onto the onGenerate function
. This is responsible for actually generating our custom types. From this point forward I'll explain what each piece of code does. Create a new file called onGenerate
in src
and add the following code:
// src/onGenerate.ts
import { GeneratorOptions } from '@prisma/generator-helper';
import fs from 'node:fs/promises';
import path from 'node:path';
const header = `// This file was generated by a custom prisma generator, do not edit manually.\n`;
export default async function onGenerate(options: GeneratorOptions) {
const start = Date.now();
const enums = options.dmmf.datamodel.enums;
}
// rest of generator code...
Here we are defining the onGenerate
function. We are defining a start variable which we will use later on for calculating how fast the custom generation takes and displaying this to the end user. We are then grabbing the enums out of the datamodel as we will want to modify this.
const output = enums.map(e => {
let enumString = `export const ${e.name} = {\n`;
e.values.forEach(({ name: value }) => {
enumString += ` ${value}: "${value}",\n`;
});
enumString += `} as const;\n\n`;
enumString += `export type ${e.name} = (typeof ${e.name})[keyof typeof ${e.name}];\n`;
return enumString;
});
Here we are tranforming the prisma enum type from something that looks like:
enum Status {
DRAFT = "DRAFT",
PUBLISHED = "PUBLISHED",
}
to
const Status = {
DRAFT: 'DRAFT',
PUBLISHED: 'PUBLISHED',
} as const;
type Status = keyof typeof Status;
At this point you may be wondering why we're going to all this trouble to not use native TypeScript enums. The main reason I don't use native TypeScript enums is because the TypeScript team themselves have mentioned it was a mistake on their part to create them. A few other reasons include:
- Bloats bundle size
- Numeric enums are NOT type-safe
Because of these issues we are using the const assertion feature. This helps us keep our bundle light and at the same time, provides us with the same typing feature as the enum
.
To use this feature you:
- declare the object like you would in plain JS
- Add
as const
after the declaration
export const httpStatusCode = {
Ok: 200,
BadRequest: 400,
Unauthorized: 401,
Forbidden: 403,
NotFound: 404,
ServerError: 500,
} as const;
Extract the type from it:
type HttpStatusCodeKey = keyof typeof httpStatusCode;
export type HttpStatusCode = typeof httpStatusCode[HttpStatusCodeKey];
Now we have the real type which represents the union of the httpStatusCode
object's values. The good news is that this type is only used for building TS and is then removed from the transpiled output.
class HttpResponse {
code: HttpStatusCode = httpStatusCode.Ok;
// other stuff here
}
The native TypeScript enum is a C# flavor brought to TypeScript. While it provides inoccent enough syntax when you're coming from a C# background, it can be harmful to end users. As mentioned before TypeScript authors are also aware of these pitfalls and are doing great work to provide alternative solutions. So if you don't know what to use you can follow this logic:
- Do you need reverse mapping? (If you don't know, then the answer is probably "No")
- If Yes, and your enum is not about strings, then you'll have to use a standard enum
- If No, continue at point 2
- Do you need to loop over enum members?
- If Yes, then use const assertion
- If No, continue at point 3
- Use const enum and live happy
Back to the generator code:
const outputFile = options.generator.output;
if (!outputFile || !outputFile.value) {
throw new Error('No output file specified');
}
const outputPath = path.resolve(outputFile.value);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, header + output.join('\n'));
const end = Date.now();
console.log(`[custom-enum-generator] - Generated enums in ${end - start}ms`);
Here we are specifying an output file where the generated code will be written to. We're also logging to the console how long it took for the onGenerate
function to run.
Final code
import { GeneratorOptions } from '@prisma/generator-helper';
import fs from 'node:fs/promises';
import path from 'node:path';
const header = `// This file was generated by a custom prisma generator, do not edit manually.\n`;
export default async function onGenerate(options: GeneratorOptions) {
const start = Date.now();
const enums = options.dmmf.datamodel.enums;
const output = enums.map(e => {
let enumString = `export const ${e.name} = {\n`;
e.values.forEach(({ name: value }) => {
enumString += ` ${value}: "${value}",\n`;
});
enumString += `} as const;\n\n`;
enumString += `export type ${e.name} = (typeof ${e.name})[keyof typeof ${e.name}];\n`;
return enumString;
});
const outputFile = options.generator.output;
if (!outputFile || !outputFile.value) {
throw new Error('No output file specified');
}
const outputPath = path.resolve(outputFile.value);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
await fs.writeFile(outputPath, header + output.join('\n'));
const end = Date.now();
console.log(`[custom-enum-generator] - Generated enums in ${end - start}ms`);
}
Wrapping up
We also need a node bash file to run the generator. Prisma will look for this file when consumers run prisma generate
. Create a file called bin.ts
in src
and add the following code:
#!/usr/bin/env node
import './generator'
Publishing
before we can go ahead with publishing the pacakge, we need to modify add the following properties to the package.json
file:
"main": "dist/generator.js",
"bin": {
"prisma-generator-types": "dist/bin.js"
},
"files": ["dist"],
"scripts": {
"build": "tsc",
}
main
is the entry point. This is where prisma will look for the generatorfiles
is the files that will be includes when publishing a packagebin
is the executable file that Prisma will run when consumers runprisma
generatebuild
- build command to transpile TypeScript down to JS
Once you've added the above, we can build the package:
pnpm build
Publish to NPM
npm publish
Usage
Consumers will now be able to use this utility in their schema.prisma
file like so:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
previewFeatures = ["fullTextSearch"]
}
generator enums {
provider = "custom-enum-generator",
outputFile = "./types"
}
Repo
You can find the full source code for this generator here: https://github.com/luke-h1/prisma-custom-enums