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) {
// ...
}
})
},
})
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
because you can always write your own resolver by using
nexus, but for the generatedt.crud.createOneUser, what is the point of writing your own if you can not reuse that implementation oft.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]
})
},
})
before and after are both an array of function, the engine will:before array of functiont.crud.createOneUsert.crud.createOneUser being resolved, its result will go through afterIt 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.
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
Most helpful comment
Actually, I quite love the idea of a framework named
Feathersjs, it has a concept calledhooks(not that React one 馃槃) to attach to thebeforeandafterstage 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
beforeandaftera CRUD. Something like authentication before, remove some fields afterThe argument is:
The API looks like this, instead of exposing a resolver, we add two properties
beforeandafterThe
beforeandafterare both an array of function, the engine will:beforearray of functiont.crud.createOneUsert.crud.createOneUserbeing resolved, its result will go throughafterIt 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 calledservice, 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 thebeforeandafterhooks. 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
beforeandafteran array, so we can compose the logic by adding function, and each hook function can be tested in an isolated manner.