prisma / prisma

Next-generation ORM for Node.js & TypeScript | PostgreSQL, MySQL, MariaDB, SQL Server, SQLite, MongoDB and CockroachDB
https://www.prisma.io
Apache License 2.0
39.11k stars 1.53k forks source link

Prisma Model Hooks #3547

Open dilizarov opened 4 years ago

dilizarov commented 4 years ago

Problem

I've read up on issues where some sort of hook integration was requested, like beforeCreate, afterSave, etc. that are offered by ORMs like Sequelize and TypeORM.

If I'm not mistaken, Prisma opted instead to go the route of middlewares on the prisma client. While this is understandable, I still think there is room for improvement that might be able to leverage the existing middleware system.

My most common cases for hooks are validation and 3rd-party integration. Validation is fairly straightforward, so instead I'll focus on 3rd-party integrations.

In my case, when a User is created, I make sure to create a Stripe Customer first so that I can attach a stripeCustomerId to the User. This commonly goes in a beforeCreate hook.

In another instance, I locally have a representation of a Twilio Chat object. Whenever I update my TwilioChat model, I make sure that on a successful update that I also persist these changes to Twilio itself.

These are instances where it is natural to want hooks on the models.

I also strongly believe that if Prisma does not offer this out of the box, then someone will create a library using Prisma to add this functionality anyways.

Suggested solution

The following APIs are my suggestions:

// Assume there is a User model in the schema.prisma

// Atm, I imagine data would be the user object that will be persisted to the DB.
prisma.user.beforeCreate(async (data) => {
  const stripeId = await Stripe.createCustomer(...)
  data.stripeCustomerId = stripeId;
})

Under the hood, perhaps that becomes something like:

prisma.$use(async (params, next) => {
  if (params.model === "User" && params.action === "create") {
    await beforeCreateCB(params.data)
  }

  await next(params);
})

I think it would be possible to create hooks like this and Prisma could come baked with these hooks (the typical ones you find in an ORM like beforeCreate, beforeUpdate, afterCreate, afterUpdate, etc.

It would also be important that these hooks properly take into account transactions. If a transaction is rolled back, then an afterCreate hook should not be fired.

Alternatives

An alternative might be to write these all myself like I did above, but it is messy and doesn't give users confidence that it'll work just as well as a hook would (maybe a silly argument, but programmers feel confident when the system they trust provides nifty solutions like these).

Additional context

I think this would really help take Prisma to the next level

matthewmueller commented 4 years ago

Thanks @dilizarov for opening this issue. I definitely agree that as-is, the middleware API is a bit low-level.

I also agree that model hooks are a good idea. Another example is how Laravel Scout uses hooks in their Eloquent models to auto-update their search indexes in Algolia.

I could see us potentially standardizing this over time, but for now we'd like to see the community step in and build these higher-level implementations.

I say this because I've seen what a vibrant community has been built with the same middleware approach for Express and Koa. They offer the low-level API, but allow hundreds of authors to build and distribute middleware on top. Over time, certain patterns emerge and they can choose to codify that in the project itself. The introduction of ctx.state in Koa is a good example of this. There was a convention to stick middleware data in state, but not everyone followed it, so they made it official over time. I can see us taking a similar approach.

That's a long-winded way of saying, we definitely agree that it's a good idea to have a higher-level implementation. Just not from us right now 🙂

ctsstc commented 2 years ago

Coming from Active Record on Rails this would be lovely ( :

DonKoko commented 1 year ago

👍🏻

Yanis02015 commented 1 year ago

Good idea, is there anything new?

stasgm commented 9 months ago

Deprecated: Middleware is deprecated in version 4.16.0. They recommend using the Prisma Client extensions query component type as an alternative to middleware.

ItsJustRuby commented 5 months ago

@dilizarov Do Prisma Client extensions cover your needs? If yes, could you close this issue, if not, could you update and expand?

Thanks! ☺

zack403 commented 2 months ago

I am currently looking at implementing something like this, is this already available ?

drichardcarl commented 1 week ago

Problem

I've read up on issues where some sort of hook integration was requested, like beforeCreate, afterSave, etc. that are offered by ORMs like Sequelize and TypeORM.

If I'm not mistaken, Prisma opted instead to go the route of middlewares on the prisma client. While this is understandable, I still think there is room for improvement that might be able to leverage the existing middleware system.

My most common cases for hooks are validation and 3rd-party integration. Validation is fairly straightforward, so instead I'll focus on 3rd-party integrations.

In my case, when a User is created, I make sure to create a Stripe Customer first so that I can attach a stripeCustomerId to the User. This commonly goes in a beforeCreate hook.

In another instance, I locally have a representation of a Twilio Chat object. Whenever I update my TwilioChat model, I make sure that on a successful update that I also persist these changes to Twilio itself.

These are instances where it is natural to want hooks on the models.

I also strongly believe that if Prisma does not offer this out of the box, then someone will create a library using Prisma to add this functionality anyways.

Suggested solution

The following APIs are my suggestions:

// Assume there is a User model in the schema.prisma

// Atm, I imagine data would be the user object that will be persisted to the DB.
prisma.user.beforeCreate(async (data) => {
  const stripeId = await Stripe.createCustomer(...)
  data.stripeCustomerId = stripeId;
})

Under the hood, perhaps that becomes something like:

prisma.$use(async (params, next) => {
  if (params.model === "User" && params.action === "create") {
    await beforeCreateCB(params.data)
  }

  await next(params);
})

I think it would be possible to create hooks like this and Prisma could come baked with these hooks (the typical ones you find in an ORM like beforeCreate, beforeUpdate, afterCreate, afterUpdate, etc.

It would also be important that these hooks properly take into account transactions. If a transaction is rolled back, then an afterCreate hook should not be fired.

Alternatives

An alternative might be to write these all myself like I did above, but it is messy and doesn't give users confidence that it'll work just as well as a hook would (maybe a silly argument, but programmers feel confident when the system they trust provides nifty solutions like these).

Additional context

I think this would really help take Prisma to the next level

One issue I encountered while trying to get a workaround with this by using either middlewares or query extension is that the middleware or extension is not "fired" for a model if that model is created via the parent model, e.g. if user has profile then you try to create profile along with creating user using

await prisma.user.create({
  data: {
    ...,
    profile: { create: {...} },
  },
});

In that code, we can only get event for creating User but not for Profile. So our custom logic should check if we created a record for a model via its parent model.

I'm not sure if that's a bug or what but IMO that would simply the logic to implement something like "hooks" for Prisma, may it be official or just a local code implementation.