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
[Proposal] Prisma Client Extensions #15074
Comments
This is great! Appreciate starting this. Few things:
|
This is really dope! |
Check my issue for alternative implementation #14793, I think it is more flexible and easier to use |
Hey @mishase thanks for sharing. I was not aware of your proposal. I'm glad to see that we had the same ideals for the API. I definitely agree that the API you showed is leaner. We had a similar candidate when we began our research. Here's why we did not adopt it:
const prisma = new PrismaClient().$extends({
$result: {
User: {
$needs: {
fullName: {
firstName: true,
lastName: true,
},
fullNameAge: {
age: true,
firstName: true,
lastName: true,
},
},
$fields: {
fullName(user) {
return `${user.firstName} ${user.lastName}`
},
fullNameAge(user) {
return `${this.fullName(user)} ${user.age}`
}
}
}
},
}) I guess it is also nice to represent the |
Okay, @millsp I got your idea, but I think there's something we should modify in it.
const prisma = new PrismaClient().$extends({
fields: {
user: {
fullName: {
select: {
firstName: true,
lastName: true,
},
compute(user) {
return `${user.firstName} ${user.lastName}`;
},
},
fullNameAge: {
select: {
age: true,
fullName: true,
},
compute(user) {
return `${user.fullName} ${user.age}`;
},
},
},
},
});
|
const extendsAB = prisma
.$extends(extensionA)
.$extends(extensionB) I like this. I just hope that if I will need to use a method/computed field from extensionA in the code of extensionB, will I be able to do it in typesafe manner? Will that be so @millsp ? |
To solve multiple fields on the same model, I would use an array for the model. Every defined property would then have its own needs and results, and one could depend on the other (as long as no circular reference is made...). const prisma = new PrismaClient().$extends({
$result: {
User: [
{
$title: 'fullName',
$needs: {
firstName: true,
lastName: true,
},
$field(user) {
return `${user.firstName} ${user.lastName}`;
},
},
{
$title: 'fullNameAge',
$needs: {
age: true,
fullName: true,
},
$field(user) {
return `${user.fullName} ${user.age}`;
},
},
],
},
}); |
I really like the proposal. It looks flexible enough to cover many use cases that are currently not possible to implement in a convenient manner. Nice job! Only the syntax for defining new fields seems a bit artificial: const prisma = new PrismaClient().$extends({
$result: {
User: (compute) => ({
fullName: compute({ firstName: true, lastName: true }, (user) => {
return `${user.firstName} ${user.lastName}`
}),
}),
},
}) I'm right in the assumption that the const client = new PrismaClient().extend({
result: {
User: computedFields({
fullName: {
needs: ['firstName', 'lastName'],
compute: (user) => `${user.firstName} ${user.lastName}`
}
})
}
}) A very rude first approximation of an implementation can be found at https://www.typescriptlang.org/play?ssl=27&ssc=3&pln=20&pc=1#code/MYGwhgzhAECqEFMBO0DeAoaXoAcCuARiAJbDQB2YAtggFzQQAuSx5A5tALzQDkP6AX3QAzPOWCNiAe3LRgUqvkYIAJgDFiCECogAeAAoA+ABSsl9API5JMvUYCU9fWiHpGATxwJoVm+TuGXGiY0ADaANL0TCzsALr0AMIKSqoaWioGADTQYOTuhoLo6KzKSMJgwN5JinjK6praugAq2QBqgRhY5AiqEAD89KEA1gjuUsLQTbEh8jXKA9Cm5OaT9lyBrYXooJAw+iwQVGAJJAjkjMHYcrbMeBJSSMb2qEJX+ESk0AgAHsrkKksVqg8IgkPR4MgBI5oPtiIdjqdzmgkAhGHgkLJGAALOFCVzyfwXUCaJHcboAdxhByOJxJjCeADofn8AZ1sCDkPRZil6ukIMY2VcsKIQCAAHLUOiXIUy7q9QY8Sg0HixTIhGVXbm1KXGDlINacQJ6hlKhDqoWvbBQwT2IA (which gives type safety for the implementation of the extension, but needs more ts magic to pass the type information back to the prisma client so that one can get the type of say |
I like the inherent meaning of |
Hi all, I'm just wondering: If I add methods to models like here (this is the example from the very first post in this issue): const prisma = new PrismaClient().$extends({
$model: {
User: {
async signUp(email: string) {
await client.user.create(...)
},
},
}
}) where does the const prisma = new PrismaClient().$extends({
$model: {
User: (client) => ({
async signUp(email: string) {
await client.user.create(...)
},
}),
}
}) ? |
Reminds me a lot of "the Sequelize way". I think this is a good chunk of effort, but for me I still would still prefer my own model class that uses dependency injection to separates concerns. const model = new UserModel(prisma.user)
await model.activate() My experience has been that is an easy pattern to unit test. |
Hey @aeddie-zapidhire, we aim to support that too via extensions. If you want you could get a class out of |
@millsp nice. Yeah, I'm not trying to extends Prisma. Prisma just lacks the typings to easily inject a model into a service pattern. |
I've been waiting for this. |
@chief-austinc the functions themselves (your @hrueger I definitely agree having reference to the client would be essential.. const prisma = new PrismaClient().$extends({
$model: {
$all: (client, modelDelegate) => ({
async softDelete(ids: number[]) {
return await modelDelegate.updateMany({
where: {
id: { in: ids }
},
data: {
deletedAt: new Date(),
}
})
},
}),
}
}) Obviously it'd never be perfect when implementing global functions like above, but having the ability to do something like that would be pretty powerful. |
Imo that sounds great 👍 const prisma = new PrismaClient().$extends({
$model: {
$all: (client, modelDelegate) => {
if (!modelDelegate instanceof LogItem) {
return {
async softDelete(ids: number[]) {
return await modelDelegate.updateMany({
where: {
id: { in: ids }
},
data: {
deletedAt: new Date(),
}
})
},
};
}
},
}
}) This would add |
This does require you to have every table the same columns |
Hey everyone! I wanted to let you know that we have officially shipped Prisma Client Extensions in our latest 4.7.0 release. Head to the release notes to learn how to get started and follow the relevant documentation links. Please let us know what you think at the preview feature feedback issue. And also, a very special thanks to everyone who commented in this issue and contributed to making this feature a better one ❤️ Thanks! |
Hi @millsp how dose this extends work with a class? This is in context of prisma in nestJS. import { OnApplicationShutdown } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
export class PrismaService extends PrismaClient implements OnApplicationShutdown {
constructor() {
super();
}
async onApplicationShutdown(): Promise<void> {
await this.$disconnect();
}
} Do I even use EDIT: Also, there is always mentioned
But in the end, my question is how do i start a callback free Transaction? surly not just with |
Hey @adrian-goe! I think the best for this one would be to publish an extension for everyone to enjoy it. What I did is a just ported code that I wrote a while ago and put it into an extension. What does it look like:
import { Prisma } from "@prisma/client"
type TxClient = {} // we don't have better types
const ROLLBACK = { [Symbol.for('prisma.client.extension.rollback')]: true }
async function $begin<T extends object>(this: T) {
let setTxClient: (txClient: TxClient) => void
let commit: () => void
let rollback: () => void
// a promise for getting the tx inner client
const txClient = new Promise<TxClient>((res) => {
setTxClient = (txClient) => res(txClient)
})
// a promise for controlling the transaction
const txPromise = new Promise((_res, _rej) => {
commit = () => _res(undefined)
rollback = () => _rej(ROLLBACK)
})
// opening a transaction to control externally
if ('$transaction' in this && typeof this.$transaction === 'function') {
const tx = this.$transaction((txClient: any) => {
setTxClient(txClient as TxClient)
return txPromise.catch((e) => {
if (e === ROLLBACK) return
throw e
})
})
return Object.assign(await txClient, {
$commit: async () => { commit(); await tx },
$rollback: async () => { rollback(); await tx }
} as T & { $commit: () => Promise<void>, $rollback: () => Promise<void> })
}
throw new Error('Transactions are not supported by this client')
}
export default Prisma.defineExtension({ client: { $begin } })
(Publishing is completely optional of course, you could also put it in a file in your local project) |
Hey all, thanks for the work you've been putting in on the Prisma client extensions and Prisma as a whole 🙏. I think it's awesome the direction you're heading in and the thought you're putting into these things. Extending a result with a computed field that
|
I'd like to argue that this proposal is a big mistake. You are taking on a lot of complexity in Prisma that doesn't need to be there and will slow down future development. All of the use-cases above can be solved at a higher level, and arguably they should. We went through the "fat models" phase with Rails years ago and collectively decided that it quickly got out of hand. This type of logic fits well into higher-level functional services (not necessarily "controllers"). Please don't take pressure from the community to solve all use-cases within Prisma, and don't take on this complexity. I'd encourage Prisma to focus on being an amazing abstraction to the data layer and not trying to be more. |
I have to agree with @mcrowe, it's much better for Prisma to do one thing and do it well, than to try and do everything anyone might ever need. Go down this path and you'll be competing with Nest.js before you notice. |
Hey @Jackman3005, thanks for your thorough proposal
No strong intention. We have discussed it but decided not to implement this for this first version. The consensus was that if you start selecting relations, we should treat the computed field as a relation too, otherwise one could generate too many queries too easily. In other words, if your computed field depends on a relation, it should be included or selected, and it won't be selected by default. Whether that is/can be efficient on the type-system is not known yet, and will drive this as we also want to maintain a good DX. We will get to it before GA. In the mean time, I think you could make compute |
Hey @mcrowe @Mahi Thanks for your feedback.
Our community has many needs that come from different angles and for many different reasons. There is a real pressure there and we want to help. So for us extensions is a way to make that possible. We don't want to fix every single use-case, but by providing an API that is generic enough, we can enable our community to solve these by themselves.
I agree with you that some of these use cases can be solved in Prisma itself, and do deserve a first-class feature. Extensions don't remove the need for first-class features, and we will continue working on shipping features and improvements. On the other hand, some valuable community use-cases also don't belong in the ORM and are better in a local extension and maybe even as an npm package (not within Prisma).
I agree, however this feature isn't designed to be opinionated, and it is for you to take it where you need to and integrate patterns as you wish.
Retrospectively, after implementing the feature, I don't think that it will slow down future development. Internally, extensions are a layer on top of JS and TS and are well separated. I think that by far, the most complex part was While our views on the topics obviously differ, I do agree with on some of your points and wanted thank you for your feedback. |
Questions1. Is is possible to use
|
There's a known issue: #16600 |
Hey @KATT 👋, nice to see you here!
|
Thank you for the info 👍🏻 I was able to fix our call stack issues using But now I'm getting some errors because it looks like all the |
Hey @mellson, thanks for the feedback. |
Thanks, I'm scrambling to remember which email I used to create my Slack account (it has been a while since I used Slack 😅) |
Thank you for the quick fix; 4.9.0-dev.70 fixes the |
This approach seems really cool at first glance but how does it fit in with other OOP style frameworks such as NestJS? Given the example above import { OnApplicationShutdown } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
export class PrismaService extends PrismaClient implements OnApplicationShutdown {
constructor() {
super();
}
async onApplicationShutdown(): Promise<void> {
await this.$disconnect();
}
} How do I // eslint-disable-next-line @typescript-eslint/no-explicit-any
type NoInfer<T> = [T][T extends any ? 0 : never]
export interface IPrismaCrudDataService<Model> {
prisma: PrismaService
create<CreateInput = 'please provide generic arg'>(args: NoInfer<CreateInput>): Promise<Model>
one<WhereUniqueInput>(where: NoInfer<WhereUniqueInput>): Promise<Model>
all<WhereInput = 'please provide generic arg'>(where: NoInfer<WhereInput>): Promise<Model[]>
some<Where, OrderBy, Cursor>(
where: NoInfer<Where>,
orderBy?: NoInfer<OrderBy>,
cursor?: NoInfer<Cursor>,
take?: number,
skip?: number
): Promise<Model[]>
update<UpdateInput = 'please provide generic arg'>(id: string, input: NoInfer<UpdateInput>): Promise<Model>
delete(id: string): Promise<Model>
}
export const PrismaCrudDataService = <Model>(model: keyof PrismaClient): Type<IPrismaCrudDataService<Model>> => {
@Injectable()
class CrudDataServiceHost {
@Inject(PrismaService) prisma: PrismaService
async create<CreateInput>(args: CreateInput): Promise<Model> {
// @ts-expect-error This is a bit too dynamic to figure out
return this.prisma[model].create({ data: args })
}
async one<WhereInput>(where: WhereInput): Promise<Model> {
// @ts-expect-error This is a bit too dynamic to figure out
return this.prisma[model].findFirst({ where })
}
async all<WhereInput>(where: WhereInput): Promise<Model[]> {
// @ts-expect-error This is a bit too dynamic to figure out
return this.prisma[model].findMany({ where })
}
async some<Where, OrderBy, Cursor>(
where: Where,
orderBy?: OrderBy,
cursor?: Cursor,
take?: number,
skip?: number
): Promise<Model[]> {
// @ts-expect-error This is a bit too dynamic to figure out
return this.prisma[model].findMany({
where,
orderBy,
cursor,
take,
skip,
})
}
async update<UpdateInput>(id: string, input: UpdateInput): Promise<Model> {
// @ts-expect-error This is a bit too dynamic to figure out
return this.prisma[model].update({ where: { id }, data: input })
}
async delete(id: string): Promise<Model> {
// @ts-expect-error This is a bit too dynamic to figure out
return this.prisma[model].delete({ id })
}
}
return mixin(CrudDataServiceHost)
} And it was used like this: @Injectable()
export class UserDataService extends PrismaCrudDataService<User>('user') {} One of the things you will notice is the hacky import { ModelTypes } from '@prisma/client'
export const PrismaCrudDataService = <ModelName extends keyof PrismaClient>(model: ModelName):
@Injectable()
class CrudDataServiceHost {
@Inject(PrismaService) prisma: PrismaService
async create(args: ModelTypes[ModelName]['Create']): Promise<ModelTypes[ModelName]['Model']> {
return this.prisma[model].create({ data: args })
}
}
} And I could use it like so @Injectable()
export class UserDataService extends PrismaCrudDataService('user') {} Also issue seems to be very similar and summed up for just the |
Base on the @kdawgwilk input that shows we need a type definition so we can just re-use it if some of us want to use a repository pattern or more OOP like approaches. Based on my implementation in #5273 (comment), I need to create some sort of a types that will contains the types that we need for anyone who want to use Prisma in a Repository Pattern or OOP like pattern. This kind of implementation is some sort of a band aid but could break if Prisma implementation changes on how the types are being produces. To fix this issue, I try to create a Prisma generator that will generate all the necessary types while the model is being created by generator. This package can be found in this github repository. But still this led to some kind of problem since we are not really sure about the return type that we have based on the So, to fix this kind of Issue I believe if we have a new type definition that allow a use case of |
Client extensions look powerful and could apply to advanced use cases. However it's adding a lot of complexity to an already complex code base. Moreover, this is the type of work that an API should do, not the ORM. What will inevitably happen is that business logic will get munged and spread between the API and ORM, making it harder to understand and maintain. The only way that could be avoided is if the entirety of the API could be implemented in the ORM. That isn't feasible even with client extensions, and attempting to do so would be in vain. |
We got a similar (identical?) issue where we use const animalWithOwner = Prisma.validator<Prisma.AnimalArgs>()({
include: {
owner: true,
},
}) And maybe a sidenote, but it would also be good to have a recommended solution for how to get the types with computed fields. For example from this extended client: new PrismaClient().$extends({
result: {
animal: {
computedState: {
needs: {},
compute(animal) {
return "computed"
},
},
},
},
}) I would like to avoid doing this manually: import { Animal as PrismaAnimal } from '@prisma/client'
export type Animal = PrismaAnimal & {
computedState: string
} |
Hey, thanks for all your feedback. Receiving your comments has been really valuable. We will be closing this issue, as we created it mostly to design the feature. That said, we understood that many of you are interested in further improvements, so we are now tracking these in the following issues:
If we missed anything, make sure to create an issue. Thanks! |
Client Extensions Proposal
Hey folks, we started designing this feature and we’re ready to share our proposal. Please let us know what you think!
Design
We aim to provide you with a type-safe way to extend your Prisma Client to support many new use-cases and also give a way to express your creativity. We plan to work on four extension layers which are
$result
,$model
,$client
,$use
.Let’s consider the following model:
Computed fields
We have this database model but we want to “augment” it at runtime. We want to add fields to our query results, have them computed at runtime, and let our own types flow through. To do this, we use
$result
to extend the results:We just extended our
User
model results with a new field calledfullName
. To do that, we defined our field withcompute
, where we expressed our field dependencies and computation logic.Result extensions will never over-fetch. It means that we only compute if we are able to:
Finally, you will be able to add fields to all your model results at once via a generic call using
$all
instead of a model name:Results are never computed ahead of time, but only on access for performance reasons.
Model methods
Extending the results is useful, but we would also love to store some custom logic on our models too… so that we can encapsulate repetitive logic, or business logic. To do this, we want to use the new
$model
extension capability:We extended our model
User
with asignUp
method and put the user creation and account logic away into asignUp
method.signUp
can now be called from anywhere via your model and via the extension:If you want to build more advanced model extensions, we will also provide an
$all
wildcard like before:We just implemented a brand new
softDelete
operation, we can now easily soft delete any of the models:Extending your queries
We want to perform queries on a specific subset of
User
in our database. In this case, we just want to work on the users that are above18
years old. For this, we have a$use
extension:$use
extensions allow you to modify the queries that come through in a type-safe manner. This is a type-safe alternative to middlewares. If you’re using TypeScript, you will benefit from end-to-end type safety here.Client methods
Models aren’t enough, maybe there’s a missing feature? Or maybe you need to solve something specific to your application? Whatever it is, we want to give you the possibility to experiment and build top-level client features.
For this example, we want to be able to start an interactive transaction without callbacks. To do this, we will use the
$client
extension layer:Now we can start an interactive transaction without needing the traditional callback:
Extension isolation
When you call
$extends
, you actually get a forked state of your client. This is powerful because you can customize your client with many extensions and independently. Let’s see what this means:Thanks to this forking mechanism, you can mix and match them as needed. That means that you can write as many flavors of extensions as you would like and for all your different use-cases, without any conflicts.
More extensibility
We are building Client Extensions with shareability in mind so that they can be shared as packages or code snippets. We hope that this feature will attract your curiosity and spark creativity 🚀.
Usage
The text was updated successfully, but these errors were encountered: