Problem
Software projects usually have the 90% / 10% problem, where the 90% of the project usually are CRUD systems and the other 10% business logic.
The integration between TypeGraphQL and Prisma provided by the package typegraph-prisma
provides a code generation system that based in the schema from prisma.schema
emits a CRUD system in top of TypeGraphQL, having a single source of truth where Prisma is the main driver of the CRUD metadata and TypeGraphQL implements the surface API that interops with it.
So, now we have our first 90% of the project automatically generated with just the adition of Prisma + TypeGraphQL code generator, lets speak about that other 10%.
In this 10%, 9% of the code will be to provide business rules to the CRUD, as resources needs usually validation and authorization to be compliant with the business rules.
So, users of this library needs to implement in top of the basic CRUD provided by TypeGraphQL that 9% of the code required to follow the business rules.
This proposal tries to create a way of creating solutions for that 9% of the code and abstract it to the schema.prisma
as a series of features that would let the user have all the CRUD + rules generated as code from a single source of truth, so all the CRUD abstractions would get generated behind the scenes and only that final 1% of the business logic would need to be created, mantained and updated by the user.
To start working in that 9%, a way to extend TypeGraphQL code generation would be needed, and to not limitate users to opinioned validation and authorization it would need to be contained in to separate, importable packages, hence this RFC for a plugin system.
Current state
- Rejected.
- Abandoned, working on a new system as we found that code generation at our required scale was really inneficient for the cold start (Almost a minute for 49 tables).
Proposers
Proposer |
Github |
Organization |
Fox Salva |
@SrZorro |
Intricom Resources SL |
Bernat Vadell |
@bernatvadell |
Intricom Resources SL |
Γlvaro LΓ³pez |
@zifiul |
Intricom Resources SL |
Ian Ensenyat |
@densenyat |
Intricom Resources SL |
Detail
Currently typegraph-prisma
uses two ways of extending the default CRUD behaviour, one that its used only internaly by TypeGraphQL that the consumers of this library can't extend currently, and another that its decorator based, but in the current state only lets decorate the resolvers, so its not specific enough to have complete control of the CRUD resources, works for stuff like Authorized or not for this resource, but doesn't leave much space for let's say this field should be validated and if its not an email stop the user request with a validation error.
Proposed solution
Example of a posible solution that given this plugin system, a plugin called typegraph-prisma-plugin-class-validator
adds validation to the CRUD.
schema.prisma
generator typegraphql {
provider = "node ../src/cli/dev.ts"
output = "../prisma/generated/type-graphql"
plugins = ["typegraph-prisma-plugin-class-validator"]
}
model User {
id Int @id @default(autoincrement())
/// @Validator.IsEmail()
email String @unique
/// @Validator.MaxLength(30)
/// @TypeGraphQL.field(name: "firstName")
name String?
/// @Validator.Min(value: 18, message: "You must be atleast $constraint1 years old")
age Int
}
Generated code at models/User.ts
from previous schema.prisma
import * as TypeGraphQL from "type-graphql";
import GraphQLJSON from "graphql-type-json";
import { JsonValue, InputJsonValue } from "../../../client";
import * as ClassValidator from "class-validator";
@TypeGraphQL.InputType({
isAbstract: true,
description: undefined,
})
export class UserCreateInput {
@ClassValidator.IsEmail()
@TypeGraphQL.Field(_type => String, {
nullable: false,
description: undefined
})
email!: string;
name?: string | undefined;
@ClassValidator.Min(18, {message: "You must be atleast $constraint1 years old"})
@TypeGraphQL.Field(_type => TypeGraphQL.Int, {
nullable: false,
description: undefined
})
age!: number;
@ClassValidator.MaxLength(30)
@TypeGraphQL.Field(_type => String, {
nullable: true,
description: undefined
})
get firstName() {
return this.name;
}
set firstName(name: string | undefined) {
this.name = name;
}
}
schema.prisma
plugin usage
Because schema.prisma
gets modified by prisma instrospect
we need a way to add our stuff to it without lossing our changes. Prisma doesn't touch comments while doing introspection of the DB, so we can exploit this feature for this plugin system and at the same time behing completly transparent to what does prisma underneath.
Features and rules
Basic comment
//
Lets start with the basics,
Plain prisma comments would be leaved as it is, they would be comments that appear only in schema.prisma
.
From Prisma schema documentation:
// comment: This comment is for the reader's clarity and is not present in the abstract syntax tree (AST) of the schema file.
schema.prisma
// This is a basic plain comment
Abstract syntax tree (AST) comment
///
From Prisma schema documentation:
/// comment: These comments will show up in the abstract syntax tree (AST) of the schema file, either as descriptions to AST nodes or as free-floating comments. Tools can then use these comments to provide additional information.
Example of posible usage an expected output:
schema.prisma
/// This comment will appear in the generated code at the model class
model user {
/// This comment will appear in the generated code at the 'id' field
id Int @id @default(autoincrement())
}
@generated/models/User.ts
import * as TypeGraphQL from "type-graphql";
/* This comment will appear in the generated code at the model class */
@TypeGraphQL.ObjectType({
description: "This comment will appear in the generated code",
})
export class User {
/* This comment will appear in the generated code at the 'id' field */
@TypeGraphQL.Field(_type => TypeGraphQL.Int, {
nullable: false,
description: "This comment will appear in the generated code at the 'id' field",
})
id!: number;
}
AST comment attributes
For future posible compatiblity with Prisma as a plugin system with first-class citizen support, we could use the same concepts used by Prisma Attributes with some minor changes to work with the limitations of having to start them with an AST comment.
If something is not overriden by this spec, its expected to be implemented following the default Prisma attributes spec.
Namespaces
Plugins need to register a namespace for them to use, so all AST comment attributes would need to follow a schema like this:
Examples:
/// @Namespace.method
/// @TypeGraphQL.field(name: "firstName")
/// @Validator.IsEmail
/// @Validator.MaxLength(30)
/// @Validator.Min(value: 18, message: "You must be atleast $constraint1 years old")
Field Attributes
Prisma Attributes defines this as field attributes:
Field attributes are marked by an @ prefix placed at the end of the field definition. You can have as many field attributes as you want and they may also span multiple lines
Given the AST comments limitations, for our case they can appear in top of the field and at the end of the field.
Block attributes
Prisma Attributes defines this as block attributes:
Field attributes are marked by an @@ prefix placed anywhere inside the block. You can have as many block attributes as you want and they may also span multiple lines
Given the AST comments limitations, for our case they can appear only in top of the model block.
Motivations
Currently typegraph-prisma
uses two ways of extending the default CRUD behaviour, one that its used only internaly by TypeGraphQL that the consumers of this library can't extend currently, and another that its decorator based, but in the current state only lets decorate the resolvers, so its not specific enough to have complete control of the CRUD resources and implement plugins in top of it, works for stuff like Authorized or not for this resource, but doesn't leave much space for let's say this field should be validated and if its not an email stop the user request.
With special doc lines
Currently they are used in this features of the library:
Uses the schema.prisma
as the single source of truth to extend the CRUD behaviour, but in the current state this can only be extended by adding glue code inside the library, so if let's say a consumer of the library wants to add class-validator to his CRUD it would need to do a fork of the project and hack his way arround to extend the doc anotations to add stuff like:
model User {
id Int @id @default(autoincrement())
/// @Validator.IsEmail()
email String @unique
/// @Validator.MaxLength(length: 30)
name String?
/// @Validator.Min(value: 18, message: "You must be atleast $constraint1 years old")
age Int
}
Runtime added decorators
Implemented via:
Additional decorators for Prisma schema resolvers
Currently this is the only way for library consumers to extend the CRUD behaviour, in the current state it only lets consumers to add decorators to CRUD resolver methods.
Advantages and disadvantages
schema.prisma
extension system
Everything would be located in the same config file (schema.prisma
), no need to maintain multiple config files to configure the DB schema + CRUD behaviour, one with prisma + TypeGraphQL and other's with validation, auth etc, that would introduce one of the problems that prisma tries to eliminate at the ORM level, this proposal tries to solve all the burden for the 90% of the code that usually is for the CRUD's sake, and would let the consumer with only a master schema.prisma
to handle that 90% of code for them, and only implement the reimaining 10% for the bussiness specific logic without lossing control over the CRUD bussiness integration, but at the same time the CRUD would not be in the way of the bussiness custom logic.
This would let with a quick look at the schema.prisma
see everything related to the CRUD. And moving all CRUD related behaviour and configuration to the same step would remove the syncronization problem introduced by clasic ORM's (See Drawbacks of ORMs, point 2, by prisma), if multiple config files exist at multiple stages that would introduce the syncronization problem where your schema.prisma
changed, now you have to go to all your configs that work on top of that result to handle the schema changes downstream.
Code coverage & analysis
An implementation working on top of schema.prisma
could be also handled at some basic level from within the runtime capabilities of the runtime added decorators if functionality is added to add decorators for fields, but this way the posible plugins for this system would be limited to mostly decorators and would lose customization over the generated CRUD at a code level.
Having that 90% of the code be generated with all the required structure to handle the bussiness integration would open the option to get code coverage to all the CRUD + bussiness rules, this way bussiness problems like wanting to enforce that all the exposed CRUD have Auth guards, or that all fields have some form of validation could be resolved with current code coverage tools in the market.
Debugging & ejecting
Let's say you want to add a new decorator to some models, let's imagine that a plugin system working on top of runtime added decorators exists. You add the config options to the required files, start your project and... Its not working, wellcome to debugging madness, you have a config file with your wanted configuration, some code for your decorators but somewhere in the black box there had been a problem. You have a CRUD + TypeGraphQL generated code, a black box that adds your stuff in memory and then your app code, have fun going step by step with the debugger until you catch the problem between the generated code and your code.
Now, lets imagine the other case, you are working in your special decorator, you add the plugin to the list of plugins to be loaded by typegraphql-prisma
, add the corresponding doc lines to schema.prisma
and generate code... The generator finishes, you try your app with the new decorated CRUD, and its not working? Wellcome to debugging land! But now you have complete control over the generated code and even better, you can see exactly what is happening in the generated code, a quick look at the generated code would get a quick answer about why its not working, making your changes to the generated code and then implementing them back to your plugin, because at the end of the day its not more than code for your eyes to see and the plugin is just writing it for you in an automated, predictable way.
This also lets the user with the option to eject from the typegraphql-prisma
generator, if somehow it has a really weird bussiness case where that 90% of code would need to be manually modified at any point the user can stop using the generator, all the code is there for him to be modified as he wish, included the business rules.