question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Option brand the model name into data

See original GitHub issue

Since creating this issue there are updates below:


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:

  1. While implementing an Oso policy where we want to pattern match on a specific kind of resource.

  2. 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.~

https://github.com/prisma/prisma/issues/5315#issuecomment-769246358

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:

  1. Application level concern mixed with database level
  2. A constant is wasting space in the database, repeated for every row, the same value
  3. Error prone if that field is ever accidentally set by some operation
  4. Developer needs to remember to always select the kind field on any query.
  5. 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:

  1. 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)

  2. 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…).

Additional context

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:64
  • Comments:17 (6 by maintainers)

github_iconTop GitHub Comments

21reactions
jasonkuhrtcommented, Dec 13, 2021

This is an old issue, my current position/suggestion.

  1. __typename field on all Prisma models at all times by default. Prisma fields cannot start with _ so __typename will NEVER conflict with a userland property name.

  2. Support configuration within the Prisma Client class constructor.

      /**
    		* @example
    		*
    		* const prisma = new PrismaClient({
    		* 	typeBrand: { ... }
    		* })
    		*/
    		type TypeBrandSettings = {
    			/**
    			*
    			* @default "__typename"
    			*
    			* @example
    			*
    			* // default
    			*
    			* const foo = prisma.foo.findUnique({ where: { id }})
    			* foo.__typename // 'Foo'
    			* type __Typename = typeof foo.__typename // 'Foo'
    			*
    			* @example
    			*
    			* // custom
    			*
    			* const prisma = new PrismaClient({
    			* 	typeBrand: { propertyName: '_tag' }
    			* })
    			*
    			* const foo = prisma.foo.findUnique({ where: { id }})
    			*
    			* foo._tag // 'Foo'
    			* type _Tag = typeof foo._tag // 'Foo'
    			*/
    			propertyName?: string
    			/**
    			* @default false
    			*
    			* @example
    			*
    			* // custom
    			*
    			* const prisma = new PrismaClient({
    			* 	typeBrand: { enumerable: true }
    			* })
    			*
    			* const foo = prisma.foo.findUnique({ where: { id }})
    			* Object.entries(foo) // [["__typename", "Foo"], ...]
    			*
    			* @example
    			*
    			* // default
    			*
    			* const foo = prisma.foo.findUnique({ where: { id }})
    			* Object.entries(foo) // [...]
    			*/
    			enumerable?: boolean
    			/**
    			* @default true
    			*/
    			enabled? boolean
    		}
    
16reactions
stalniycommented, May 10, 2021

I need similar functionality in CASL. Usually, there is some property (e.g., __typename that describes object type) or class instance has reference to its constructor. CASL needs this information to be available on an object/resource it checks in order to detect its type, to understand what permission rules to apply to that object.

I also tried middleware approach which is kind of good for top level but as I’ve already understood it does not work on nested models.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Create a Data Model in Excel - Microsoft Support
A Data Model is a new approach for integrating data from multiple tables, effectively building a relational data source inside the Excel workbook....
Read more >
How to fill html "option" tag with data from database in Django?
I'm using SQLite. Here is my html template — click. And also: models.py from django.db import models ...
Read more >
The CREATE MODEL statement | BigQuery ML - Google Cloud
All model types are supported. INPUT_LABEL_COLS, The label column names in the training data. Linear & logistic regression, Boosted trees, Random Forest,
Read more >
Django model data types and fields list - GeeksforGeeks
A many-to-one relationship. Requires two positional arguments: the class to which the model is related and the on_delete option. ManyToManyField ...
Read more >
Django model data types
Django models can use several data types to enforce model data fit certain ... a Django model field to a list of values...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found