-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Option brand the model name into data #5315
Comments
From slack, question/answer:
This feature would be useful for any language where a plain structure is used that is NOT an instance of a class. So Go would NOT need this because structs are instances whereas in Python this WOULD be useful because
By the way I'm not advocating that we should use classes 🙂 I think the simple data approach is great. Also, once/if branding lands it will have arguably easier identity usage than class instances with Why? Because thanks to TS static typing + Prisma generation will allow us to track |
I just realized that
We can use 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. |
Since opening this, a few more thoughts:
|
Have a look at #2505 (comment) I use the following on the client to generically lift db calls over the network. type model = Uncapitalize<keyof typeof Prisma.ModelName>;
const models = Object.keys(prisma.Prisma.ModelName).map(s => s[0].toLowerCase() + s.slice(1) as model);
type action = Exclude<Prisma.PrismaAction, 'createMany' | 'executeRaw' | 'queryRaw'>; // why are these not defined on PrismaClient[model]?
const actions = ['findMany', 'create', 'update', 'delete', 'findUnique', 'findFirst', 'updateMany', 'upsert', 'deleteMany', 'aggregate', 'count'] as const; // these are the actions defined on each model. TODO get from prisma? PrismaAction is just a type.
type dbm<M extends model> = Pick<PrismaClient[M], action>;
// dbm('model').action runs db.model.action on the server
const dbm = <M extends model> (model: M) : dbm<M> => {
const lift = <A extends action> (action: A) => ((args: {}) => rest('POST', `/db/${model}/${action}`, args)) as PrismaClient[M][A];
return Object.fromEntries(actions.map(s => [s, lift(s)]));
};
export const db = Object.fromEntries(models.map(s => [s, dbm(s)])) as { [M in model]: dbm<M> };
// db.foo.findMany().then(console.log); |
We find the extra table column to be the simplest solution right now. |
Just for the record: I played with the idea of using middlewares to manipulate the data returned by queries (add the model name as a property) and the data written to the database (remove it again) - add Code that no one should use and does not work for this problem anywayprisma.$use(async (params, next) => {
// remove __model before handing over to database
// TODO also remove in deeper input objects
if(params.args.data?.__model) {
delete params.args.data.__model
}
// execute actual query
let result = await next(params);
// add __model before returning to application
// TODO also apply to deeper returned objects
if (Array.isArray(result)) {
result.forEach(el => {
el.__model = params.model
});
} else {
result.__model = params.model
}
return result;
}) Bigger problem are of course the Typescript types: A Code that no one should use and does not work for this problem anyway let user1 = await prisma.user.create({
data: {
email: 'alice@prisma.io',
name: 'Alice',
posts: {
create: {
title: 'Watch the talks from Prisma Day 2019',
content: 'https://www.prisma.io/blog/z11sg6ipb3i1/',
published: true,
},
},
},
include: {
posts: true,
},
})
// copy paste from StackOverflow - no idea what this breaks!
let user_modified = user1 as typeof user1 & { [key: string]: string };
console.log(user_modified.__model) So summary: I wasted some time, learned something about middlewares, but this most probably does not help to solve this problem here in any way. |
I need similar functionality in CASL. Usually, there is some property (e.g., 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. |
I see only 3 ways on how this information can be added to objects:
|
…s` helper that generates all possible subjects from type object `addModelType` middleware was removed due to comments in prisma/prisma#5315
* feat(prisma): adds interpreter for model properties Relates to #161 * feat(prisma): implements Prisma interpreter and required `WhereInput` generic type for prisma query Removes interpreter that translates AST into prisma query as it seems to be redundant if we define rules using Prisma syntax Relates to #161 * feat(prisma): adds `accessibleBy` function that creates PrismaQuery out of Ability * feat(prisma): adds `PrismaAbility` * feat(prisma): adds model type setting middleware * chore: adds dx to prisma package * chore(dx): adds jest.chai config * test(prisma): adds tests for equals, lt/e, gt/e and in operators Also did small reorganization of code in lib * test(prisma): adds tests for PrismaQuery interpreter * fix(prisma): re-exports WhereInput so it can be used internally * style(prisma): fixes eslint * chore: fixes aurelia test run * test(prisma): adds tests for PrismaAbility and accessibleBy * chore: replaces env var for enabling coverage with option * test(prisma): adds tests for `addModelType` Prisma middleware * refactor(prisma): removes `addModelType` middleware and adds `Subjects` helper that generates all possible subjects from type object `addModelType` middleware was removed due to comments in prisma/prisma#5315 * docs(prisma): updates README to reflect actual package functionality
Any updates? |
This is an old issue, my current position/suggestion.
|
Would be great to have this part of the Prisma spec. |
Adding onto this. I am trying to implement authorization with CASL and need to discriminate type. I'm stuck on how to address my problem otherwise. CASL even mentions in its documentation how unfortunate it is that Prisma does not provide any type information (and it links to this issue): https://casl.js.org/v5/en/package/casl-prisma#note-on-subject-helper Prisma has been really fantastic otherwise so far, especially with https://prisma.typegraphql.com I really like the idea proposed by @jasonkuhrt. |
Likewise - this is a major pain point - I don't really want to have to have a database field just for my model's type, but it really seems like the only option at the moment. @jasonkuhrt's solution looks great. |
Still keeping an eye on this issue |
Is this being considered as part of Prisma's roadmap this year? |
I don't like the idea of putting that logic on the DB layer. I really hope Prisma team comes with a better solution. |
Any update on a timeline for working on this? I have to think anyone using Prisma to implement a GraphQL schema using best practices is going to run into this issue. The |
Did client extensions fix this issue by any chance? |
@sepehr500 IIRC from some recent discussion it potentially could/will. But until someone builds a working POC that others here can test against their use-cases we should maintain healthy skepticism about if this issue is fully resolved or not by it. |
Unfortunately in the meantime we've moved away from prisma so I can't comment on whether client extensions have resolved the issue. Good luck for those still waiting! |
@rosscooperman What did you end up moving to? |
We shifted our whole stack toward Rails w/ Gusto's Apollo federation library. |
I've somehow worked around this using the new client extensions. Fair warning: It's still a very suboptimal solution since it requires feeding the model names into it manually to build the extension (since const buildTypenameExtension = <T extends string>(model: T) => ({
__typename: {
needs: {},
compute: () => model,
},
});
const xprisma = prisma.$extends({
name: 'add-model-name',
result: {
user: buildTypenameExtension('User'),
post: buildTypenameExtension('Post'),
// and so on...
},
}); I've also tried something a little cleaner using Doesn't work, do not usefunction camelize(str) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (word, index) {
return index === 0 ? word.toLowerCase() : word.toUpperCase();
})
.replace(/\s+/g, '');
}
const xprisma = prisma.$extends({
name: 'add-model-name',
result: Object.fromEntries(
['User', 'Post', '...'].map((model) => [
camelize(model),
{
__typename: {
needs: {},
compute: () => model,
},
},
])
),
});
const user = await xprisma.user.findFirstOrThrow();
console.log(user.__typename, user.id); // works, but TypeScript doesn't see those fields This extension will create a property const user = await xprisma.user.findFirstOrThrow({
include: { posts: true },
});
console.log(user.posts[0].__typename); // Post In my case, I'm using it for CASL and feed this If anyone has an better idea to make this a little cleaner or even completely eliminate the need to explicitly specify all the models, that'd be great. If Prisma did this on its own would be even better though of course, so +1. |
Hey everyone 👋, here's some good news: I created a Prisma Client extension that adds branded types to your result types at runtime and on type-level, and it obviously handles nested data. In order to use this Prisma Client extension, you'll need to hook it in. So for example, in this sample project, this is how it would look: import { PrismaClient, Prisma } from "@prisma/client"
async function main() {
const prisma = new PrismaClient().$extends(brandExtension)
const result = await prisma.user.findFirst({ include: { accounts: true } })
console.log(result?.$kind) // user
console.log(result?.accounts[0].$kind) // account
} Here's the extension code for you to copy somewhere into your project: import { PrismaClient, Prisma } from "@prisma/client"
const brandExtension = Prisma.defineExtension((client) => {
type ModelKey = Exclude<keyof typeof client, `$${string}` | symbol>
type Result = { [K in ModelKey]: { $kind: { needs: {}, compute: () => K } } }
const result = {} as Result
const modelKeys = Object.keys(client).filter((key) => !key.startsWith("$")) as ModelKey[]
modelKeys.forEach((k) => { result[k] = { $kind: { needs: {}, compute: () => k as any } } })
return client.$extends({ result })
}) ℹ️ Prisma Client extensions are currently in preview, so feel free to give feedback. Also let us know if this extension worked for you, and feel free to improve it (or ask us to improve it). In the mean time, I hope it helps. |
Hi @millsp, thanks for that. I'm trying to use your solution together with jest-mock-extended, but I've gotten into an endless typing issues. Can you provide something that would help me to use this solution together with jest-mock-extended? |
Hey @boemekeld, I'd be happy to help you. Could you please open a github discussion or contact me via our public slack with all the details (so that we can move this conversation out of this issue). Thanks! |
This kinda works, I guess, but I would prefer to use the model name - |
Using TypeScript's const brandExtension = Prisma.defineExtension((client) => {
type ModelKey = Exclude<keyof typeof client, `$${string}` | symbol>;
type Result = {
[K in ModelKey]: {
$kind: {
needs: Record<string, never>;
compute: () => Capitalize<K>;
};
};
};
const result = {} as Result;
const modelKeys = Object.keys(client).filter((key) => !key.startsWith('$')) as ModelKey[];
modelKeys.forEach((k) => {
const capK = k.charAt(0).toUpperCase() + k.slice(1);
result[k] = {
$kind: { needs: {}, compute: () => capK as any },
};
});
return client.$extends({ result });
}); |
Since 4.16.0 2023-06-20, In my opinion (my Casl uses), this problem resolved. Problems:
Solution 2.1: An utility type that load the type from the extended prisma client.// ./prisma/extendedModelTypes.ts
import { prisma } from "@/db"
type Prisma = typeof prisma
type PrismaKeys = keyof Prisma
type ExtractStrings<T> = T extends string ? T : never;
type ExtractModelKeys<T extends string | symbol> = T extends `$${string}`
? never
: T;
type StringsKeys = ExtractStrings<PrismaKeys>
type ModelKeys = ExtractModelKeys<StringsKeys>
type CapitalizeKeys<T> = {
[K in keyof T as `${Capitalize<string & K>}`]: T[K]
};
type PrismaModelsUncapitalized = { [T in ModelKeys]: Awaited<ReturnType<typeof prisma[T]['findUniqueOrThrow']>> };
export type PrismaModels = CapitalizeKeys<PrismaModelsUncapitalized> // src/common/authorization/abilities.ts
import { PureAbility } from '@casl/ability';
import { PrismaQuery } from '@casl/prisma';
import { PrismaModels } from '@/db/extendedModelTypes.ts';
export type AppAbilities =
| ["create" | "read" | "update" | "delete" | "publish", "Article" | PrismaModels['Article']]
| ["create" | "read" | "update" | "delete" | "publish", "Post" | PrismaModels['Post']]
| ["create" | "read" | "update" | "delete" | "publish", "User" | PrismaModels['User']]
| ["read" | "update" | "delete" | "publish", "Comment" | PrismaModels['Comment']]
| ["read" | "update" | "delete" | "transfer_ownership", "Team" | PrismaModels['Team']]
| ["create", "Comment"]
| ["manage", "all"];
export type AppAbility = PureAbility<AppAbilities, PrismaQuery>; Check the types for your ownPaste that code in prisma playground here Solution 2.2: A custom generator that load prisma dmmf, read models and write a file like this:// ./prisma/extendedModelTypes.ts
import { prisma } from "../../../prisma/db"
export type User = Awaited<ReturnType<typeof prisma.user['findUniqueOrThrow']>>
export type Team = Awaited<ReturnType<typeof prisma.team['findUniqueOrThrow']>>
export type TeamMember = Awaited<ReturnType<typeof prisma.teamMember['findUniqueOrThrow']>>
export type Post = Awaited<ReturnType<typeof prisma.post['findUniqueOrThrow']>>
export type Article = Awaited<ReturnType<typeof prisma.article['findUniqueOrThrow']>>
export type Comment = Awaited<ReturnType<typeof prisma.comment['findUniqueOrThrow']>> I've written a generator for Pothos+Prisma. And there you can run a script "afterGenerate" over dmmf. See codeafterGenerate: (dmmf) => {
fs.writeFile(
path.join(__dirname, `../common/authorization/prismaExtesionTypes.ts`),
`import { prisma } from "../../../prisma/db"
${dmmf.datamodel.models.map(el => `export type ${el.name} = Awaited<ReturnType<typeof prisma.${firstToLower(el.name)}['findUniqueOrThrow']>>`).join('\n')}
`,
{},
(err) => {
console.log({ err });
}
);
} 2.3 Suggested "perfect" solution:A native option to extract types from extended client, maybe something like Pothos does here. |
@millsp @mfkrause I want to thank you both for making a solution here. I still had to manually make object types for my app to use, but this make it possible! eg import * as PrismaNamespace from '@prisma/client'
import { Prisma, PrismaClient } from '@prisma/client'
// brandExtension def here
export const prisma = new PrismaClient().$extends(brandExtension)
export type Prisma = typeof prisma
export type User = PrismaNamespace.User & { type: 'User' }
export type Team = PrismaNamespace.Team & { type: 'Team' }
export type TeamMembership = PrismaNamespace.TeamMembership & { type: 'TeamMembership' } |
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:
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:
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.#5315 (comment)
When enabled viatrue
the default field used could be something likekind
or$kind
ortype
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:
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:
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.
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:
This is suboptimal because:
kind
field on any query.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...).
Additional context
The text was updated successfully, but these errors were encountered: