Nexus-plugin-prisma: custom crud resolvers

Created on 2 Dec 2019  路  9Comments  路  Source: graphql-nexus/nexus-plugin-prisma

Idea originally (?) raised here https://github.com/prisma-labs/nexus-prisma/issues/381#issuecomment-540093941. It has since been validated by no "wait we didn't think of X" moments, and multiple thumbs up from different users (both in GH and slack).

mutationType({
  definition(t) {
      t.crud.createOneUser({
        alias: 'signUp'
        resolver(parent, args, ctx, info) {
          // ...
        } 
      })
  },
})
efformodest impachigh typfeat

Most helpful comment

Actually, I quite love the idea of a framework named Feathersjs, it has a concept called hooks (not that React one 馃槃) to attach to the before and after stage of a request, so we can adding our own logic.

I think in most situations, the CRUD is still just a CRUD, you can attach your logic before and after a CRUD. Something like authentication before, remove some fields after

The argument is:

because you can always write your own resolver by using nexus, but for the generated t.crud.createOneUser, what is the point of writing your own if you can not reuse that implementation of t.crud.createOneUser

The API looks like this, instead of exposing a resolver, we add two properties before and after

mutationType({
  definition(t) {
      t.crud.createOneUser({
        alias: 'signUp'
        before: [isAdminUser],
        after:[removePassword]
      })
  },
})

The before and after are both an array of function, the engine will:

  1. go through the before array of function
  2. the result will be pass to t.crud.createOneUser
  3. after t.crud.createOneUser being resolved, its result will go through after
  4. then a response will be sent to the user

It should be easy to implement, and won't introduce any breaking change. And I am happy to submit the PR. If you guys agree on the idea.

Side topic:

Why I think it gonna work, is from my experiences of using feathersjs, it standardized the CRUD operation to a database model called service, it will generate a fully-fledged endpoint for you for an entity, and by adding these hooks, I never run into any situation that I can not do the things I want. Because a CRUD is just a CRUD, your custom business logic can always be added by the before and afterhooks. So you get the velocity from codegen, and still can customize to adapt your use case.

It is been mentioned in this PR, https://github.com/prisma-labs/nexus-prisma/issues/541

But the differences here is we make before and after an array, so we can compose the logic by adding function, and each hook function can be tested in an isolated manner.

All 9 comments

That's great!

And similarly, it might be also useful to add middleware like authorizeto t.model and t.crud:

t.crud.createOneUser({
        alias: 'signUp',
        authorize(parent, args, ctx, info) {
          // ...
        },
      })

or maybe a general middleware to add logics both before and after the default resolver:

t.crud.createOneUser({
        alias: 'signUp',
        async middleware(next, parent, args, ctx, info) {
          // do something
          await next()
          // do something else and return
        },
      })

Hey @beeplin we'd probably treat that as a separate feature for consideration.

@BjoernRave could you share how your use-case is resolved by this feature? Based on what you said in Slack, we're not sure it does.

Actually, I quite love the idea of a framework named Feathersjs, it has a concept called hooks (not that React one 馃槃) to attach to the before and after stage of a request, so we can adding our own logic.

I think in most situations, the CRUD is still just a CRUD, you can attach your logic before and after a CRUD. Something like authentication before, remove some fields after

The argument is:

because you can always write your own resolver by using nexus, but for the generated t.crud.createOneUser, what is the point of writing your own if you can not reuse that implementation of t.crud.createOneUser

The API looks like this, instead of exposing a resolver, we add two properties before and after

mutationType({
  definition(t) {
      t.crud.createOneUser({
        alias: 'signUp'
        before: [isAdminUser],
        after:[removePassword]
      })
  },
})

The before and after are both an array of function, the engine will:

  1. go through the before array of function
  2. the result will be pass to t.crud.createOneUser
  3. after t.crud.createOneUser being resolved, its result will go through after
  4. then a response will be sent to the user

It should be easy to implement, and won't introduce any breaking change. And I am happy to submit the PR. If you guys agree on the idea.

Side topic:

Why I think it gonna work, is from my experiences of using feathersjs, it standardized the CRUD operation to a database model called service, it will generate a fully-fledged endpoint for you for an entity, and by adding these hooks, I never run into any situation that I can not do the things I want. Because a CRUD is just a CRUD, your custom business logic can always be added by the before and afterhooks. So you get the velocity from codegen, and still can customize to adapt your use case.

It is been mentioned in this PR, https://github.com/prisma-labs/nexus-prisma/issues/541

But the differences here is we make before and after an array, so we can compose the logic by adding function, and each hook function can be tested in an isolated manner.

So, resolver middleware is already in flight with the new nexus plugins system. The degree to which it isn't enough to satisfy resolver auth requirements is not clear to me yet. I'm not discounting the ideas here. But I'd like to see very clear alignment with the underlying nexus middleware system.

I would love to be able to define custom resolvers too. In my case, when I call createOneCrossword, which accepts a list of words and their starting positions, I need the resolve function to generate cells and save them to database along with words.

Another use case for having a custom resolve function is to be able to check if a mutation caller has a right to connect certain words to a new crossword. Otherwise, some words could be stolen from some other crosswords. I've never tried it but I assume that's how it works.

Is there a workaround today to doing AuthN/Z with t.crud.<model>? My guess is using graphql-shield?

I have a use case for custom resolvers. What I especially would also need then is a feature to extend the input arguments.

I'm using Postgres with PostGIS to implement a search which finds job offers in the near area specified by coordinates (latitude, longitude) and a radius.

I now implemented a very dirty hack to accomplish my area search.


The explanation of my hack

Excerpt from my schema.prisma:

model JobOffer {
  id                  String   @default(cuid()) @id
  locations           JobLocation[]          // m:n relation

  # many other fields
}

model JobLocation {
  id            String   @default(cuid()) @id
  /// Identifier composed by zip_lat_lng
  uniqueId      String   @unique

  jobOffer      JobOffer[]  // m:n relation

  zip           String
  city          String
  state         String?
  lat           Float?
  lng           Float?
}

My queryType for Query is implemented like this:

export const Query = queryType({
    definition(t) {
        // ...

        t.crud.jobOffers({
            filtering: {
                // many filtering fields
            },
            pagination: true,
        })

        // ...
    },
})

To have a way to pass additional arguments in my jobOffers query I added the field inArea to my JobOffer objectType:

const JobOfferInAreaCenterInput = inputObjectType({
    name: 'JobOfferInAreaCenterInput',
    description: 'Defines the center of the area search',
    definition(t): void {
        t.int('radius', {
            description: 'Radius in meters',
            required: true,
        })
        t.float('lat', {
            description: 'Latitude',
            required: true,
        })
        t.float('lng', {
            description: 'Longitude',
            required: true,
        })
    }
})

export const JobOffer = objectType({
    name: 'JobOffer',
    definition(t) {
        // many other fields 

        t.field('inArea', {
            type: 'Boolean',
            description: 'Dummy type for area search.\nThis is used ONLY for jobOffers query.\nYou MUST pass the center argument as a variable named $areaSearchCenter\n\nThis is a dirty hack.',
            args: {
                center: JobOfferInAreaCenterInput,
            },
            resolve: () => true
        })
    },
})

Now I wrote a nexus plugin (used in Nexus.makeSchema) which intercepts the jobOffers resolver:

import { plugin } from 'nexus'

interface Area {
    radius: number
    lat: number
    lng: number
}

export const jobOffersAreaSearchExtension = plugin({
    name: 'JobOffersAreaSearchExtension',
    onCreateFieldResolver(config) {
        if (config.fieldConfig.name !== 'jobOffers') {
            return
        }

        return async (root, args, ctx, info, next) => {
            const areaSearchCenter: Area | undefined = info.variableValues.areaSearchCenter

            if (!areaSearchCenter) {
                return next(root, args, ctx, info)
            }

            // TODO: pg can be replaced with prisma client raw queries when this issue is resolved:
            // https://github.com/prisma/migrate/issues/357
            const jobLocationsWithinArea = await ctx.pg.query(`
SELECT
    *
FROM
    prisma2_project."JobLocation"
WHERE
    ST_DWithin(ST_MakePoint(lng, lat)::geography, ST_MakePoint($1, $2)::geography, $3)
            `, [areaSearchCenter.lng, areaSearchCenter.lat, areaSearchCenter.radius])

            let jobLocationsWithinAreaIds: string[] = jobLocationsWithinArea.rows.map((location: any) => location.id)

            // locations are not specified as filterable for t.crud.jobOffers
            // if it was specified, we would have to merge the args
            args = {
                ...args,
                where: {
                    ...args.where,
                    locations: {
                        some: {
                            AND: {
                                id: {
                                    in: jobLocationsWithinAreaIds
                                }
                            }
                        },
                    }
                }
            }

            return next(root, args, ctx, info)
        }
    },
})

This makes it possible to query my jobOffers within a specified area:

query GetAllJobOffers($areaSearchCenter: JobOfferInAreaCenterInput) {
  jobOffers {
    inArea(center: $areaSearchCenter)

    id
    locations {
      id
      zip
      city
    }

    # many other fields
  }
}

# With area search:
# {
#   "areaSearchCenter": {
#     "radius": 5000,
#     "lng": 9.1919123,
#     "lat": 48.786453
#   }
# }
#
# Without area search:
# {
#   "areaSearchCenter": null
# }
#
# Alternatively "areaSearchCenter" can not be passed at all

Closed by #674

Was this page helpful?
0 / 5 - 0 ratings

Related issues

malekjaroslav picture malekjaroslav  路  5Comments

mateja176 picture mateja176  路  5Comments

jmadson picture jmadson  路  5Comments

jasonkuhrt picture jasonkuhrt  路  4Comments

adarnon picture adarnon  路  3Comments