- Owner: @timsuchanek
- Stakeholders: @schickling
- State:
- Spec: ✅ - current, stable
- Implementation: ✅ - current, stable
Description of how the interface for generation in the individual languages like JavaScript and TypeScript looks like, and which protocol is used under the hood to achieve that interface.
- Context
- Generator architecture
- Terminology
- Information passed into a generator
- Interface for a generator
- Interface between Generator SDK and each language helper
An important mechanism in Prisma 2 is the generation of artifacts based on the Prisma schema. Prisma Framework itself for example includes the built-in generator Photon, which is used to generate a Prisma Client JS client that can be used to access data via Prisma in applications in Javascript/Typescript apps. This pattern of generating artifacts based on the schema can also be used by generators created by the community.
A generator is an executable, which gets metadata of the Prisma Schema like the models passed in by the Generator SDK and generates code based on it. The generator is responsible for saving that code to the filesystem.
┌────────────────┐
│ Generator SDK │
└─┬──────────────┘
│
│ passes schema meta data
│
▼
┌─────────────────┐
│ Generator │
└─┬───────────────┘
│
│ generates artifacts
│
▼
┌─────────────────┐
│ Filesystem │
└─────────────────┘- The
Generator SDKis part of thePrisma SDK, which is being implemented by Prisma. It is responsible for calling a generator and gives generator authors a library called@prisma/generator-helperto ease the generator development in TypeScript/JavaScript. Other languages will follow. - A
Generatoris an executable, which hooks in to the Generator SDK. It gets meta data about the schema as the input and can have arbitrary generation artifacts as outputs. There are both built-in Prisma generators like Photon, which are maintained by Prisma and also custom user created generators, which anyone can create. Generated artifact: The output of a generator. This can for example be generated code. It could also be an UML diagram showing the schema.
A generator gets passed in a JSON blob including the following information:
-
Everything which is declared in the
schema.prismafile:- Models
- Enums
- Datasources
- Generators, including the configuration this generator points to
-
The available Query Schema of the Query Engine
The following TypeScript type definition GeneratorOptions describes the content of that JSON blob:
type GeneratorConfig = {
output: string | null
name: string
provider: string
config: Dictionary<string>
binaryTargets: string[]
pinnedBinaryTarget?: string | null
}
type ConnectorType = 'mysql' | 'mongo' | 'sqlite' | 'postgresql'
type Datasource = {
name: string
connectorType: ConnectorType
url: EnvValue
config: { [key: string]: string }
}
type GeneratorOptions = {
generator: GeneratorConfig
otherGenerators: GeneratorConfig[]
schemaPath: string
dmmf: DMMF.Document
datasources: Datasource[]
datamodel: string
binaryPaths?: BinaryPaths
}
type BinaryPaths = {
migrationEngine?: { [binaryTarget: string]: string } // key: target, value: path
queryEngine?: { [binaryTarget: string]: string }
introspectionEngine?: { [binaryTarget: string]: string }
}To understand, what binaryTargets means, you can read more here.
`DMMF` Types
This is just here for completeness and will move into a separate document.
export namespace DMMF {
export interface Document {
datamodel: Datamodel
schema: Schema
mappings: Mapping[]
}
export interface Enum {
name: string
values: string[]
dbName?: string | null
}
export interface Datamodel {
models: Model[]
enums: Enum[]
}
export interface Model {
name: string
isEmbedded: boolean
dbName: string | null
fields: Field[]
}
export type FieldKind = 'scalar' | 'object' | 'enum'
export type DatamodelFieldKind = 'scalar' | 'relation' | 'enum'
export interface Field {
kind: DatamodelFieldKind
name: string
isRequired: boolean
isList: boolean
isUnique: boolean
isId: boolean
type: string
dbName: string | null
isGenerated: boolean
relationToFields?: any[]
relationOnDelete?: string
relationName?: string
}
export interface Schema {
rootQueryType?: string
rootMutationType?: string
inputTypes: InputType[]
outputTypes: OutputType[]
enums: Enum[]
}
export interface QueryOutput {
name: string
isRequired: boolean
isList: boolean
}
export type ArgType = string
export interface SchemaArg {
name: string
inputType: {
isRequired: boolean
isList: boolean
type: ArgType
kind: FieldKind
}
isRelationFilter?: boolean
}
export interface OutputType {
name: string
fields: SchemaField[]
isEmbedded?: boolean
}
export interface SchemaField {
name: string
outputType: {
type: string // note that in the serialized state we don't have the reference to MergedOutputTypes
isList: boolean
isRequired: boolean
kind: FieldKind
}
args: SchemaArg[]
}
export interface InputType {
name: string
isWhereType?: boolean // this is needed to transform it back
isOrderType?: boolean
atLeastOne?: boolean
atMostOne?: boolean
fields: SchemaArg[]
}
export interface Mapping {
model: string
findOne?: string
findMany?: string
create?: string
update?: string
updateMany?: string
upsert?: string
delete?: string
deleteMany?: string
}
export enum ModelAction {
findOne = 'findOne',
findMany = 'findMany',
create = 'create',
update = 'update',
updateMany = 'updateMany',
upsert = 'upsert',
delete = 'delete',
deleteMany = 'deleteMany',
}
}To add a generator to a Prisma project and run it, the following steps are needed:
- Create an executable file, which handles the generation
- Point to that file in your
schema.prisma - Run
prisma2 generate
In the following we describe how this can be done in JavaScript or TypeScript. In section Interface between Generator SDK and each language you can read more about other languages.
To create a generator in TypeScript, create a new package, which needs two files: A executable generator file, e.g. generator.ts and optionally a generator manifest, called generator-manifest.json.
A generator.ts file can use a helper function from the @prisma/generator-helper npm package to get a callback for when a generation is being requested:
#!/usr/bin/env ts-node
import { onGenerate, GeneratorOptions } from '@prisma/generator-helper'
onGenerate((options: GeneratorOptions) => {
// implement generator here
})Optionally, in the same folder as the generator.ts, there can be a generator-manifest.json, which includes the following information:
prettyName(optional): The "pretty name" of the generator, e.g. "My beautiful Generator"defaultOutput(optional): The default output path of the generator, e.g.node_modules/@generated/generatordenylist(optional): A list of models or enums which are not allowed to be used in the schema.requiresGenerators: A list of other generators this generator depends on. E.g.["photonjs"]requiresEngines: A list of binaries this generator depends on. E.g.["query-engine", "migration-engine", "introspection-engine"]
Example:
{
"prettyName": "Prisma Test Utils",
"defaultOutput": "node_modules/@generated/prisma-test-utils",
"denylist": ["TestUtilsGlobalType"],
"requiresGenerators": ["photonjs"],
"requiresEngines": ["query-engine", "migration-engine"]
}To add the generator to the schema.prisma file, the following block has to be added to the schema.prisma file:
generator myGenerator {
provider = "./node_modules/generator-package/generator.js"
}Optionally additional configuration can be passed in to the generator:
generator myGenerator {
provider = "./node_modules/generator-package/generator.js"
output = "./custom-generated/my-generator"
someArbitraryConfig = "some value"
}As soon, as prisma2 generate is being executed, the provided generator file will executed by the prisma2 CLI.
A generator is being spawned as a subprocess. The GeneratorOptions JSON is being passed in over the following JSON-RPC 2.0 protocol:
Over the Stdin of the spawned child process, the following json is being passed in:
{"jsonrpc": "2.0", "method": "generate", "params": {... GeneratorOptions}, "id": 1}Stderr is being used to communicate back to the SDK:
{"jsonrpc": "2.0", "result": { error: "Could not find directory" }, "id": 1}In order to debug / log, the generator can use Stdout. Everything logged via Stderr, which is not a JSON adhering to the JSON RPC standard, will just be dropped.
These are the available RPCs:
generatewith the inputGeneratorOptions. If the generator process crashes or returns an object containing anerrorproperty, the generator SDK can handle that error and show it to the user. If it simply returns an empty object{}for the result, it was a successful generation.