Problem with mongooseResolvers generics
Hi, I am working on custom guards around resolvers including findOne, findMany, etc. I have hit a couple of problems:
- The type of the arguments (
TArgs) aren't exported, and so I cannot enforce consistent typing (for args) on the guarding resolver. - The
findManyresolver declares its type asResolver<TSource = any, TContext = any, TDoc extends Document = any>. This is inconsistent with the return value (the resolver returnsTDoc[], notTDoc), and so type enforcement breaks again.
For context, here is my Guard class:
export type GuardType = 'ingress' | 'egress'
export interface GuardInput<TContext, TArgs, TReturn> {
context: TContext
args: TArgs
data?: TReturn
}
export interface GuardOutput<TArgs, TReturn> {
args?: TArgs
data?: TReturn | false // set to false to remove all data
}
export abstract class Guard<TContext, TArgs = any, TReturn = any> {
constructor(
public type: GuardType
) { }
abstract check(input: GuardInput<TContext, TArgs, TReturn>): Promise<GuardOutput<TArgs, TReturn> | void>
}
export function guardResolver<TSource, TContext, TArgs, TReturn>(
resolver: Resolver<TSource, TContext, TArgs, TReturn>,
...guards: Guard<TContext, TArgs, TReturn>[]
): Resolver<TSource, TContext, TArgs, TReturn> {
const guardedResolver = (<SchemaComposer<TContext>>schemaComposer).createResolver<TSource, TArgs>({
name: resolver.name,
type: resolver.getType(),
args: resolver.getArgs(),
resolve: async params => {
for (const guard of guards.filter(guard => guard.type == 'ingress')) {
const result = await guard.check({
args: params.args,
context: params.context
})
if (!result) continue
if (result.args) params.args = result.args
}
let data: TReturn | undefined = await resolver.resolve(params)
for (const guard of guards.filter(guard => guard.type == 'egress')) {
const result = await guard.check({
args: params.args,
context: params.context,
data
})
if (!result) continue
if (result.args) params.args = result.args
if (result.data !== null) { // data is being mutated
if (result.data === false) data = undefined
else data = result.data
}
}
return data
}
})
return guardedResolver
}
... and here is an example of how I use it:
export const chatQueries: ObjectTypeComposerFieldConfigMapDefinition<IChat, ResolverContext> = {
chatById: guardResolver(ChatTC.mongooseResolvers.findById(), new IsOwnChatGuard()),
chatMany: guardResolver(ChatTC.mongooseResolvers.findMany(), new ContainsOnlyOwnChatsGuard())
}
In the above example, at the moment, TS will raise an error for ContainsOnlyOwnChatsGuard because it expects IChat[] whereas findMany() declares that it will return IChat (despite the fact that it returns IChat[]). console.log of the output is below:
[
{ _id: 123456789, name: 'Chat' },
{ _id: 234567890, name: 'Chat' }
]
POST /graphql 200 147.473 ms - 964
I don't know the extent to which these problems exist in the library, any help is appreciated.
I think the more supported way of doing what you're trying to achieve is with resolve wrappers. I used resolver wrappers in my app to ensure that users can only read and write their own models, based on a userId field on every model. It took a while to get the types right, but my code is now type safe. Maybe you can do something similar and find a way to refactor your guardResolver to use beforeRecordMutate and beforeQuery.
Thanks for the suggestion, I was able to use resolve wrappers in my solution & it made it a lot cleaner 😃. However, the type error still exists. Here is my updated guard function:
export function guardResolver<TSource, TContext, TArgs, TReturn>(
resolver: Resolver<TSource, TContext, TArgs, TReturn>,
...guards: Guard<TContext, TArgs, TReturn>[]
): Resolver<TSource, TContext, TArgs, TReturn> {
const guardedResolver = resolver.wrapResolve(next => async params => {
// check ingress guards
for (const guard of guards.filter(guard => guard.type == 'ingress')) {
const result = await guard.check({
args: params.args,
context: params.context
})
if (!result) continue
if (result.args) params.args = result.args
}
// get the data
let data: TReturn | undefined = await next(params)
// check egress guards
for (const guard of guards.filter(guard => guard.type == 'egress')) {
const result = await guard.check({
args: params.args,
context: params.context,
data
})
if (!result) continue
if (result.args) params.args = result.args
if (result.data !== null) {
// data is being mutated
if (result.data === false) data = undefined
else data = result.data
}
}
return data
})
return guardedResolver
}
This line still raises type errors:
chatMany: guardResolver(ChatTC.mongooseResolvers.findMany(), new ContainsOnlyOwnChatsGuard())
because findMany() incorrectly declares its return type as TDoc instead of TDoc[]
and ContainsOnlyOwnChatsGuard expects the type IChat[] (which is consistent with the actual data returned by the findMany resolver)