Prisma-client-js: Ability to add hooks for CRUD query builder

Created on 27 Apr 2020  Â·  12Comments  Â·  Source: prisma/prisma-client-js

Problem

currently it's not possible to add hooks to alter queries (e.g. add where conditions). this feature is need for example to implement authorization in model level.

Solution

adding hooks for CRUD functions to add conditions would allow implementing privacy in model level.

Additional context

ent has privacy which allows to define permissions in data models. prisma should allow something similar.

kinfeature

Most helpful comment

We're also very interested in low level hooks for validation and authorization in Blitz apps.

I'd love something like this

const authorize = /* some auth logic */

prisma.addPreHook(({model, operation, args, ctx}) => {

  // model = the prisma model name. Ex: 'widget' 
  // operation = 'findOne' | 'findMany' | 'create' ....
  // args = entire input to the operation, like {data: {...}, include: {}}
  // ctx = set by the user on a per request basis

  // This hook returns a new `args`. So here the `authorize` hook can
  // filter out input arguments or add arguments. 
  // Example: add `where: {organizationId: user.organizationId}` to findMany

  // authorize() could also do `throw new AuthorizationError()` which would
  // surface up to the application code

  return authorize({model, user: ctx.user, operation, args})
})

function someHttpRequestHandler() {
  const user = getCurrentUser()

  const widget = await prisma.withCtx({user}).widget.create({data: {...}})
}

All 12 comments

Thanks a lot for raising this issue @sijad! Certainly something we've thought about before and something we want to look into. Do you have some API design suggestions in mind?

Related:

It would be great if this feature will be supported directly by the prisma client. At the moment we are working on a javascript proxy object to wrap the prisma client.

Our api looks likes this at the moment:

import { prisma } from './lib/prismaProxy';

// add before hook synchronous
prisma.addHook('beforeCreateUser', (args, ctx) => {
  // do something
  return true; // true is optional
});

// add after hook synchronous
prisma.addHook('afterCreateUser', (args, result, ctx) => {
  // do something
});

// add before hook asynchronous
prisma.addHook('beforeCreateUser', async (args, ctx) => {
  // do something
  return true;
});

// add before hook and stop prisma call
prisma.addHook('beforeCreateUser', (args, ctx) => {
  // do something
  return false // false indicates that this prisma call should be stopped an no other hooks should be called on this trigger.
});

We are considering to put this in a config file:

// hooks.config.js
export default {
  context: {
    prisma,
    req
  },
  hooks: {
    beforeCreateUser: [
      (args, ctx) => {},
      (args, ctx) => {},
      (args, ctx) => {}
    ],
    afterCreateUser: [
      (args, result, ctx) => {}
    ]
  }
}

So we have two trigger for each prisma crud-function: before... and after...

Our idea is to register each hook with the addHook function. It is possible to chain multiple hooks on the same trigger.
The args parameter consists of the args for the called prisma function. This can be edited over the object reference.
The result parameter is only available inside the after hooks and holds the result of the prisma call.
The ctx object consists in our case of the prisma client and the express req-object, to use them inside the hook function.

It should be possible to use a synchronous or an asynchronous hook function.

In order to have a "fire and forget" hook, it is possible to call an asynchronous function from inside the hook function without waiting for it.

I've been thinking about how it could be implemented into the official prisma client.

I saw that hooks is already a reserved word in the prisma client. So here is my proposal:

// set hooks for each prisma client separately

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

prisma.hooks.context = {
    // set prisma implicit
    req: getExpressReqObject() // or whatever middleware someone is using
}

prisma.hooks.user.before.findOne.add((args, ctx) => {
    console.log ('before hook triggered');
})

prisma.hooks.user.before.findOne.add((args, ctx) => {
    console.log ('cancel prisma request from before hook');
    return false;
})

prisma.hooks.user.after.findOne.add((args, result, ctx) => {
    console.log ('after hook triggered');
})

Other possible alternatives:

prisma.hooks.user.before.findOne.push // use an array instead of a function to add hooks
prisma.hooks.user.findOne.before.add // put "before or "after" after the crud operation
prisma.hooks.user.findOne.before.push // same with an array

Maybe there should also be a way to register global hooks that work for all prisma clients.
Or the solution above always registers the hooks globally.

import { globalPrismaHooks } from '@prisma/client';

globalPrismaHooks.user.before.findOne.add((args, ctx) => {
    console.log('global hook triggered');
})

By the way, I now have a working version (for my needs) as a javascript proxy object, that wraps the prisma client and adds the hooks. If someone is interested, just let me know.

We're also very interested in low level hooks for validation and authorization in Blitz apps.

I'd love something like this

const authorize = /* some auth logic */

prisma.addPreHook(({model, operation, args, ctx}) => {

  // model = the prisma model name. Ex: 'widget' 
  // operation = 'findOne' | 'findMany' | 'create' ....
  // args = entire input to the operation, like {data: {...}, include: {}}
  // ctx = set by the user on a per request basis

  // This hook returns a new `args`. So here the `authorize` hook can
  // filter out input arguments or add arguments. 
  // Example: add `where: {organizationId: user.organizationId}` to findMany

  // authorize() could also do `throw new AuthorizationError()` which would
  // surface up to the application code

  return authorize({model, user: ctx.user, operation, args})
})

function someHttpRequestHandler() {
  const user = getCurrentUser()

  const widget = await prisma.withCtx({user}).widget.create({data: {...}})
}

Authorization is not prisma responsibility imho, if you for example create a user in multiple places you probably should wrap that logic in a different function, like signUpUser or similar

@remorses absolutely — and this is exactly what you get with my suggestion. Prisma doesn't know anything about authn. Authn is handled entirely by user code.

Interested in this in relation to React Native. Currently retro-fitting WatermelonDB into a RN project, but seems rather old-school in terms of React thinking (no hooks!). It uses RxJS to notify components about CRUD database operations. Was thinking Prisma might be a great alternative to WatermelonDB since it also supports SQLite but with great TS support.

However, not sure how to achieve the whole observability and reactive component model unless Prisma offers some sort of events or hooks that could be used to notify when changes have been made, so relevant components can update with the new version after a CRUD operation has happened.

Now that we implemented Middlewares (cf. https://github.com/prisma/prisma-client-js/issues/770) I would consider closing this issue since it does address the use case mentioned in the original issue.
If there are more specific use cases which you don't feel are addressed properly, feel free to open a new one about this specific case.

Thank you!

Note: Middleware support is only available in the @dev version for now, but will be included in 2.3.0 which will be released next week.

One interesting idea of those hooks is applying them to only one model so it can properly type it. Currently the middlewares are applied to all so I would not say the use case is 100% covered. Maybe if we had model specific middleware or some helpers to discriminate the model (mostly needed to avoid the case where a model is renamed by the user forgets to update the middleware check).

The payload posted in https://github.com/prisma/prisma-client-js/issues/770#issuecomment-659955441 does not let you handle that? If so, best open a new issue so we can take this into consideration as a new feature.

Well we can do a comparison against the model name for sure and then cast the type of the input, but one of the point I was making is that if you rename the model one might forget to update the code of the middlewares. Where a hook on the model like it was proposed or helpers that compare against the model type would throw a typescript compilation error. Having the middleware for each model would just be a wrapper for checking its the right model and typing the input.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

FluorescentHallucinogen picture FluorescentHallucinogen  Â·  3Comments

divyenduz picture divyenduz  Â·  3Comments

williamluke4 picture williamluke4  Â·  3Comments

MichalLytek picture MichalLytek  Â·  3Comments

nikolasburk picture nikolasburk  Â·  3Comments