-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Description
Since creating this issue there are updates below:
- newest proposal Option brand the model name into data #5315 (comment)
Problem
Prisma Client does not introduce data classes/model classes, instead just returning/working with simple native JS data structures (plain objects, arrays, etc.).
This is a problem when the identity of data is needed. There are lots of cases where identity is needed. Here are two cases I am currently working with:
-
While implementing an Oso policy where we want to pattern match on a specific kind of resource.
-
While implementing polymorphism in GraphQL we want to use the Discriminant Model Field (DMF) Strategy.
Suggested solution
Ideally something as simple as this:
const prisma = new PrismaClient({
brandTypes: true | { fieldName: string, case: "lower" | "upper" | "pascal" }
})
I don't see how we can enable this by default without being backwards incompatible because there are no namespace guarantees. So alas I guess this would default to false.
We can use $type (or $kind) knowing that it will not break user code (except maybe in a very esoteric user case that we can accept breaking).
I think having this builtin and enabled by default makes sense for Prisma Client because model identity is a fundamental issue and it is currently impossible without resorting to duck-typing and offers no integration with TS discriminant union types.
When enabled via true
the default field used could be something like kind
or $kind
or type
or $type
.
Users would be able to overwrite this default with additional configuration, passing their desired field name (the second union member above).
An additional option may be the casing of the string. E.g. let the user decide between these:
user.kind === 'user'
user.kind === 'USER'
user.kind === 'User'
The problem I see with this is that it won't show up in the static typing. That is, with this setting enabled, the following should be statically safe:
const user = prisma.user.findUnique(...)
const a: 'user' = user.kind
const org = prisma.org.findUnique(...) // nested relation example
const b: 'org' = org.kind
const c: 'user'[] = org.members.map(user => user.kind)
// and so on
This is key because it is the only way to leverage TS discriminant union types.
The only way I can see Prisma Client being able to achieve this (without potentially major complexity via runtime reflection to generate typegen like Nexus) is for Prisma to add some new configuration at the generator level.
generator client {
provider = "prisma-client-js"
brandTypes = true
}
generator client {
provider = "prisma-client-js"
brandTypes {
fieldName = "kind"
case = "lower" | "upper" | "pascal"
}
}
Alternatives
It is currently possible I think to solve this by putting a field on every model that will simply be a constant of the model kind:
enum UserKind {
user
}
model User {
kind UserKind @default(user)
}
This is suboptimal because:
- Application level concern mixed with database level
- A constant is wasting space in the database, repeated for every row, the same value
- Error prone if that field is ever accidentally set by some operation
- Developer needs to remember to always select the
kind
field on any query. - Related to above point: Any automation/abstract might not and lead to integration issues.
I considered using middleware but this did not seem to work because:
-
Monkey patching
kind
fields would not be reflected in the TS types... (but maybe I can leverage TS interface declaration merging? But actually no, not easily, because Prisma model types are not globals. So I would need to create a new module of model types anyways) -
When there are nested relations I would need to traverse them and brand them too, which AFASICT is not possible because the model names of nested relations is not available in the middleware (nor is it clear anyways how it would be in a way that I could map to data during the traversal process...).