-
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
Extend Prisma Client #7161
Comments
Especially computed fields are also related to #5998, which would provide a very easy and flexible way to implement them. |
@tobiasdiez that's a good one. Added that to the Related list. Thanks! |
This might also be related: #6759 |
Might be! I could see a use-case for two Prisma Clients in one higher-level model. One with read access, one with write access. |
It seems to me that prisma/ent could deal with the usecase 1 and 2. Why don't you re-start from it? |
Related - #6349 |
Are there already plans to implement this soon? |
Seconded. I've been evaluating Node ORMs supporting MySQL for a couple weeks. I was turned on to Prisma because it seems to provide fantastic tooling orbiting around a central schema definition such as generating and managing migrations. What seems to be lacking is generating extendable model classes which encapsulate the data representation. Typically (my personal experience across various languages), you'd see ORM tooling generate base classes with accessors for each column. Instances of which represent rows in the database. Then, developers can extend those model classes with additional functionality. The use cases are many as you've already pointed out, but here is a list of ones I commonly face:
Fat controllers don't cut it because objects are often manipulated in multiple contexts: CLI, MVC controllers, REST API actions, etc. and you likely want to encapsulate coupled functionality right at the model level for DRY reasons. I briefly looked into documentation and extending the client to achieve equivalent results, but there isn't even any tangible code representing the "Active Record". It seems that dynamically creates objects based on schema rather than providing a base class per table. Can we at least get an estimate on some documentation which explains how to achieve this, and the pros/cons? Perhaps this would accomplish your first bullet:
I have limited knowledge of the inner workings, but can we hook into the client to tell it how to serialize/deserialize data per table? That way we could write our own classes and pass around instance of them as long as the Prisma client can understand how to handle them. We'd probably have to write some boilerplate (accessors mostly) for each, but that's a reasonable expectation and can be solved with tooling. Update: This is exactly it but Is this type of thing on the roadmap? |
Hey @brianreeve thanks for sharing your use cases. This issue is definitely still a priority for our team, but we're still a small team of engineers with lots of other highly-requested features too. I still think a guide or example would be a nice first step. Thanks for bumping this one up again. |
I'm not sure exactly what you want to see in an example, but for testing I would love to have a "seeding" system. Something like this:
Then I'd be able to use it like this:
And finally use it in tests like this:
|
Just to expand on what @Newbie012 wrote, it would be useful to be able to add your own opaque types/smart constructors. An example of how that might work has been suggested in #6349 (comment) |
@matthewmueller can we as a community help with the examples explaining the status quo. If yes, in which form do you prefer these, e.g comments here or as a external repository? |
Yes, that would be great! The way I'm thinking about this is a realistic, yet minimal example. Perhaps a signin form where you need to take multiple actions or responding to a github comment that sends out emails. Ideally framework agnostic, but a well-understood framework like Express would be fine too. Just looking to put together some patterns of higher-level models to see where we fall short and provide ideas on how to improve the Client to support these use cases. If you or anyone else wants to create an example repo of how it currently would work today and highlight the pains, we can collect these examples and work towards addressing pain points. |
We added some new documentation on how to build some of these use cases yourself:
We do plan to implement these features into the Prisma Client eventually, but this is meant to help you along in the meantime. If you'd like to chat about any of these topics or have any questions, feel free to book some time with me. |
I have been following this discussion for some time and I would like to contribute my thoughts. I've been trying to build a Prisma Middleware (using What I would love to see would be hooks based on models and operations that would also get called if the operation is nested. E.g. it would be cool if you could do prisma.$foo.user.onCreate((data, select, include, next) => {
// Do something, perhaps modify the data
const result = next(data, select, include);
// Possibly modify the result, e.g. adding computed fields
// select or include could also be used to check if a computed field needs to be computed
return result
}) As with the Prisma Middleware you could perform changes to the data that gets inserted, modify the result or you could even make additional calls to Prisma in the Middleware. The only difference is that you don't need to worry about covering every possible case of the query structure. Additionally, the typings for these hooks could be part of the Prisma client so it would be a lot more type safe than todays Some other things that would generally help would be:
Regarding the second point there should probably also be some predefined directives that make a field either client-only or database-only but I do think it would be best if you could also do this programmatically. This would also allow you to add fields to either all models programmatically or only some of them, e.g. based on the model directives I mentioned before. I have no idea if any of this is feasible at all. To my understanding, much of the Prisma client is not actually written in Typescript so this could potentially come at a performance cost. Also this is obviously something that would require major work so for now it will remain a fantasy for me. |
My team has been using Prisma for a year now. Our use case is complex and involves many of the use cases mentioned here and then some. In the JS/TS ecosystem there are multiple strong opinionated ORMs with plugins for just about everything. We specifically use Prisma for its simple API and strong types which lends it well to a functional paradigm. Documentation on how to compose the Prisma client with other specialized libraries for things such as validation is a great direction. There are some things that could improve the composability of the client. Outside that I'd prefer the core team to keep delivering features specific to managing and interacting with the database than make an ActiveModel style ORM that tries to do it all. |
Adding helper methods that compute additional fields works well to add one or two fields. But in a more complex project, you end up with a lot of helper methods, that devs have to remember, import separately, etc. It would be nice if one could create a custom model class and then say "Prisma please use this class as the model for users". Something like the following rewrite of the example from https://www.prisma.io/docs/concepts/components/prisma-client/computed-fields: import User as PrismaUser from prisma
// Somewhere add a config that prisma automatically wraps all methods normally returning `User` with this class
class User extends PrismaUser {
constructor(private user: PrismaUser) {
// So that methods/properties defined on `PrismaUser` work
super(user)
}
function computeFullName(): string {
return this.user.firstName + ' ' + this.user.lastName,
}
}
async function main() {
const user = await prisma.user.findUnique({ where: 1 })
user.computeFullName() // This now works, because `findUnique` returns a `User` and not a `PrismaUser`
user.firstName // Still works
} Currently one can achieve something like this using Alternatively, if prisma would return a class import { User } from "prisma"
declare module "prisma" {
interface User {
computeFullName(): string
}
}
User.prototype.computeFullName = function () {
return this.user.firstName + ' ' + this.user.lastName,
};
async function main() {
const user = await prisma.user.findUnique({ where: 1 })
user.computeFullName() // This now works, because we extended `User`
user.firstName // Still works
} |
@tobiasdiez import { User } from "@prisma/client"
function computeFullName(this: Pick<User, "firstName" | "lastName">) {
return this.firstName + ' ' + this.lastName,
};
async function main() {
const user = await prisma.user.findUnique({ where: { id: 1 } });
user!::computeFullName()
} |
I'm thinking we should use something like Firebase does when fetching data from their store. In Firsbase we use class Post {
constructor(readonly title: string, readonly author: string) {}
toString(): string {
return this.title + ', by ' + this.author;
}
}
const postConverter = {
toFirestore(post: Post): firebase.firestore.DocumentData {
return {title: post.title, author: post.author};
},
fromFirestore(
snapshot: firebase.firestore.QueryDocumentSnapshot,
options: firebase.firestore.SnapshotOptions
): Post {
const data = snapshot.data(options)!;
return new Post(data.title, data.author);
}
};
const postSnap = await firebase.firestore()
.collection('posts')
.withConverter(postConverter)
.doc().get();
const post = postSnap.data();
if (post !== undefined) {
post.title; // string
post.toString(); // Should be defined
post.someNonExistentProperty; // TS error
} One thing i found out difficult to build is dynamic typing, if we started including different entities in queries, we need a way to use the types more easily rather than building them out manually e.g. const uses = await prisma.user.findUnique({where : { id }, include: { posts: true, comments: true }});
/*
Prisma here knows that the user type is going to be User & { posts: Post[]; comments: Comment[] } but there is not an exported type by default so we have to build those/
*/
type UserWithCommentsAndPosts = User & { posts: Post[]; comments: Comment[] };
const findPostWithComment (user: UserWithCommentsAndPosts) => .... doc for FirebaseDataConverter |
I'm using the custom model approach to add "computed fields" to my prisma models. In a nutshell I'm "extending" the find & create methods that prisma provides, in order to inject my computed fields. This works, but my biggest complaint is that I was unable to reuse the method names - so I'm not actually extending I think it would be great if custom classes/methods could be passed to export default new PrismaClient({
user: {
findUnique: (user: User): User => doWhateverToUser(user)
}
}) or: export default new PrismaClient({
user: {
beforeFindUnique: (args: Prisma.UserFindUniqueArgs): Prisma.UserFindUniqueArgs => doWhateverToArgs(args)
afterFindUnique: (user: User): User => doWhateverToUser(user)
}
}) or if just for computed fields: export default new PrismaClient({
user: {
computedFields: (user: User): User => applyComputedFieldsToUser(user)
}
}) Current Use Example:Currently using this setup in a monorepo.
|
This functionality already exists: Agree that maybe it might be better to have this as part of PrismaClient but if it is it should just use ES6 Proxy because then it can be fully implemented without touching the internals and creating additional bloat const proxyify = <T extends object>(value: T, table?: string, operation?: string): T => {
return new Proxy(value, {
apply: function trap(obj, props, args) {
if (typeof obj !== 'function') return;
if (operation) {
console.log(args, table, operation);
// pre logic.
}
const returned = obj(...args);
if (returned instanceof Promise) {
/* post logic feel free to use finally */
return returned;
}
/* Can actually re-chain proxyify here if you need to. */
},
get: function trap<K extends keyof T & string>(obj: T, key: K) {
return proxyify(obj[key] as any /* 'any' forgive me jesus */, table || key, table ? key : undefined);
},
});
};
const client: PrismaClient = proxyify(new PrismaClient()); This is a simple example in our internal use we have an API we're written to easily allow pre/post logic by adding another parameter to proxify I.E
In my experience these pre/post validations start with good intentions, but eventually a use case comes along that needs to disable logic in the pre/post and that's when the architecture starts to cave in on itself. |
@ShanonJackson I haven't used ES6 Proxies yet, but it's an interesting idea that's worth looking into. Do you also get type-safety with proxies? |
Yeah type-safety around ES6 Proxies in Typescript cover almost all use cases, but there are some patterns left to be typed but I don't think this will run into those. |
Hey all, I've built a new Prisma Generator to emit custom models based on Prisma recommendations(as mentioned in the docs). Feel free to try it here: https://github.com/omar-dulaimi/prisma-custom-models-generator |
Hi, My opinion is that extending specific models is not that important, since business specific tasks should be handled one layer above Prisma. But you added it so hurray. However, extending the base functionality of the client with generic functions is of primary importance to me, for example to have a See my question on StackOverflow that explains the problem. |
has someone had any issues with $-methods while using ES6 proxies or even tried them? I'm getting errors like: I can't find any info on why this might happen. |
@random42 we will soon publish and expansion of our guide to show how to do that with client extensions. @JuanGalilea It is possible that we have a bug there, please create a new issue and provide us with a repro. Thanks. |
We're closing this issue as the points from the OP have been implemented in Client Extensions. If something is still missing for you, please open a feature request, a bug report, or a discussion. Thanks. |
Problem
Right now we don't provide guidance on how use the Prisma Client within a higher-level model or domain.
Use cases
1. Adding a computed field: The canonical example is getting a full name from a first and last name. We also have some use cases in: #3394 and #5998
2. Multi-step actions: Right now people put all their Prisma Client calls inside their GraphQL resolvers or RESTful controllers. This leads to "fat controllers" that are hard to test and lead to code duplication across controllers. The reason is because the Prisma client is often lower-level than your business domain and expressing your business logic requires multiple steps.
Some Examples:
From a top-level view of your application, you'd consider these to be a single action (e.g. team.signup and document.update), but they break down into multiple steps. We should have a way to turn multiple steps into single actions without limiting the rich capabilities of the Prisma Client API. #5258 #5258
3. Custom Validation: Validate fields before they hit the database. Using the example above, for a
team.signup(team_name, owner_email)
action, you can validate that the owner's email address is valid and the team name doesn't have any special characters before you go to the database. Lots more use cases in: #3528Existing Solutions
Prior Art
In Rails you have a difference between Active Record and the Active Record Query Interface. Prisma Client is an awesome Active Record Query Interface but Active Record is currently DIY. We should also have a story for some of the Active Record use cases described above.
Suggested Next Steps
What I'd like to avoid: Going too deep into implementing a solution before understanding how one would do this with today's Prisma Client in a hacky way perhaps.
Related
The text was updated successfully, but these errors were encountered: