avoid calling a parent resolver if only nested resolver was called - node.js

lets say I have a simple query to get post's comments and it looks like this
post(id:"123") {
comments: {
id,
body
}
}
currently it the graph will call postResolver and then commentsResolver
but the call to postResolver is redundant since I only need to fetch all the comments by postId
I am using an implementation using nodeJs with typescript
i have a resolver such as this
const resolvers : Resolvers = {
Query: {
post: (parent, args, info) => { return fetchPost(args.id);}
},
Post: {
comments: (parent, args, info) => { return fetchComments(parent.id)}
}
}
basically in this example I don't need to fetch the post at all, but the resolver is still invoked, any way to elegantly avoid it ?
I'm looking of a generalized pattern and not this specific resolver situation, there are other nodes with same situation would like to know if there is anything common in this situation that was already solved ...
My solution so far is to remodel the graph like this
type Post (id: ID!){
postData: PostData,
comments: [Comment!]
}
type PostData {
id: ID! ...
}
type Comment{
id: ID! ....
}

Your original model is fine, you just need a different query that goes straight for the comments based on their postId:
getCommentsByPostId(postId: ID!): [Comment]
Then augment your query resolvers:
const resolvers : Resolvers = {
Query: {
post: (_, { id }) => { return fetchPost(id);},
getCommentsByPostId: (_, { postId }) => fetchComments(postId)
},
…

Related

GraphQL Resolver - Multiple Promises

I'm currently trying to call multiple Rest data sources to pull back a combined set of users in a single graph.
When running the query via Graphql Playground I'm getting null values which would point to the resolver function.
Looking in the debug console I can see that I'm getting data returned but I've got an additional layer of nesting includes which would explain why I'm not seeing the data in Graphql playground.
Resolver function looks like this:
latestProspects: async (_, __, { dataSources }) => {
try {
return Promise.all([
dataSources.site1Prospects.getLatestProspects(),
dataSources.site2Prospects.getLatestProspects()
])
} catch(error) {
console.log(error)
}
},
UPDATE
Thinking things through I realise that the result sets are held in a dimension per data source.
I've updated my code to merge the two result sets together, not sure I'm able to improve things further?
type Prospect {
date_created_gmt: String
first_name: String
last_name: String
email: String
billing: Address
meta_data: [Metadata]
}
latestProspects: async (_, __, { dataSources }) => {
try {
return await Promise.all([
dataSources.site1Prospects.getLatestProspects(),
dataSources.site2Prospects.getLatestProspects()
])
.then(result => {
const prospects = result[0]
return prospects.concat(result[1])
})
} catch(error) {
console.log(error)
}
},

Graphiql variables not being passed to server

I'm building an Apollo Server. I have one simple endpoint communicating with Mongo. There's a collection of announcements.
export const typeDefs = gql`
type Query {
announcements: [Announcement]
announcementsByAuthor(author: String!): [Announcement]
}
type Announcement {
_id: ID!
msg: String!
author: String!
title: String
}
`;
export const resolvers = {
Query: {
announcements: () => {
return new AnnouncementController().getAnnouncements();
},
announcementsByAuthor: (author: string) => {
console.log('RESOLVER: ', author);
return new AnnouncementController().getAnnouncementsByAuthor(author);
}
},
}
In my graphiql interface, the announcements query works correctly:
{
announcements {
msg
author
}
}
The announcementsByAuthor query does not seem to be accepting the string argument, either from a variable or when hardcoded into the query.
query($author: String!){
announcementsByAuthor(author: $author) {
msg
author
}
}
Variables:
{
"author":"Nate"
}
I've logged out from the resolver, and an empty string is being passed in, instead of the specified value for the author variable. I'm new to graphql and I'm hoping someone can enlighten me as to what I'm sure is a simple oversight.
Try this instead:
announcementsByAuthor: (doc, {author}) => {

Implementing pagination with Mongoose and graphql-yoga

I am experimenting with graphql and have created a simple server using graphql-yoga. My Mongoose product model queries my database and both resolvers return data as expected. So far it all works and I am very happy with how easy that was. However, I have one problem. I am trying to add a way to paginate the results from graphQL.
What did I try?
1) Adding a limit parameter to the Query type.
2) Accessing the parameter through args in the resolver
Expected behaviour
I can use the args.limit parameter in my resolver and use it to alter the Mongoose function
Actual behaviour
I can't read the arg object.
Full code below. How do I reach this goal?
import { GraphQLServer } from 'graphql-yoga'
import mongoose from "mongoose"
import {products} from "./models/products.js"
const connection = mongoose.connect('mongodb://myDB')
const prepare = (o) => {
o._id = o._id.toString()
return o
}
const typeDefs = `
type Product {
_id: String
name: String
description: String
main_image: String
images: [String]
}
type Query {
product(_id: String): Product
products(limit: Int): [Product]
}
`
const resolvers = {
Query: {
product: async (_id) => {
return (await products.findOne(_id))
},
products: async (args) => {
console.log(args.name)
return (await products.find({}).limit(args.limit))
},
},
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => console.log('Server is running on localhost:4000'))
The arguments for a field are the second parameter passed to the resolver; the first parameter is the value the parent field resolved to (or the root value in the case of queries/mutations). So your resolvers should look more like this:
product: (root, { _id }) => {
return products.findOne(_id)
}

Sequelize with GraphQl: How to update fields using mutation

I'm using a stack of koa2, sequelize and graphql. I wan't to change the state field of the users model using graphql mutation and return the changed object.
Currently my mutation looks like this:
mutation: new GraphQLObjectType({
name: 'Mutation',
fields: {
setState: {
type: userType,
args: {
input: {
type: userStateInputType
}
},
resolve: resolver(db.user, {
before: async (findOptions, {input}) => {
const {uuid, state} = input;
await db.user.update(
{state},
{where: {uuid}}
);
findOptions.where = {
uuid
};
return findOptions;
}
})
}
}
})
And that's the corresponding query:
mutation setstate{
setState(input: {uuid: "..UUID..", state: "STATE"}) {
uuid
state
}
}
It's working, but I'm pretty sure there are better solutions for this.
I would avoid trying to use graphql-sequelize's resolver helper for a mutation. Looking over the source for that library, it looks like it's really meant only for resolving queries and types.
I think a much cleaner approach would just to do something like this:
resolve: async (obj, { input: { uuid, state } }) => {
const user = await db.user.findById(uuid)
user.set('state', state)
return user.save()
}
I'm avoiding using update() here since that only returns the affected fields. If you ever decide to expand the fields returned by your mutation, this way you're returning the whole User Object and avoiding returning null for some fields.

How to check permissions and other conditions in GraphQL query?

How could I check if user has permission to see or query something? I have no idea how to do this.
In args? How would that even work?
In resolve()? See if user has permission and somehow
eliminate/change some of the args?
Example:
If user is "visitor", he can only see public posts, "admin" can see everything.
const userRole = 'admin'; // Let's say this could be "admin" or "visitor"
const Query = new GraphQLObjectType({
name: 'Query',
fields: () => {
return {
posts: {
type: new GraphQLList(Post),
args: {
id: {
type: GraphQLString
},
title: {
type: GraphQLString
},
content: {
type: GraphQLString
},
status: {
type: GraphQLInt // 0 means "private", 1 means "public"
},
},
// MongoDB / Mongoose magic happens here
resolve(root, args) {
return PostModel.find(args).exec()
}
}
}
}
})
Update - Mongoose model looks something like this:
import mongoose from 'mongoose'
const postSchema = new mongoose.Schema({
title: {
type: String
},
content: {
type: String
},
author: {
type: mongoose.Schema.Types.ObjectId, // From user model/collection
ref: 'User'
},
date: {
type: Date,
default: Date.now
},
status: {
type: Number,
default: 0 // 0 -> "private", 1 -> "public"
},
})
export default mongoose.model('Post', postSchema)
You can check a user's permission in the resolve function or in the model layer. Here are the steps you have to take:
Authenticate the user before executing the query. This is up to your server and usually happens outside of graphql, for example by looking at the cookie that was sent along with the request. See this Medium post for more details on how to do this using Passport.js.
Add the authenticated user object or user id to the context. In express-graphql you can do it via the context argument:
app.use('/graphql', (req, res) => {
graphqlHTTP({ schema: Schema, context: { user: req.user } })(req, res);
}
Use the context inside the resolve function like this:
resolve(parent, args, context){
if(!context.user.isAdmin){
args.isPublic = true;
}
return PostModel.find(args).exec();
}
You can do authorization checks directly in resolve functions, but if you have a model layer, I strongly recommend implementing it there by passing the user object to the model layer. That way your code will be more modular, easier to reuse and you don't have to worry about forgetting some checks in a resolver somewhere.
For more background on authorization, check out this post (also written by myself):
Auth in GraphQL - part 2
One approach that has helped us solve authorization at our company is to think about resolvers as a composition of middleware. The above example is great but it will become unruly at scale especially as your authorization mechanisms get more advanced.
An example of a resolver as a composition of middleware might look something like this:
type ResolverMiddlewareFn =
(fn: GraphQLFieldResolver) => GraphQLFieldResolver;
A ResolverMiddlewareFn is a function that takes a GraphQLFieldResolver and and returns a GraphQLFieldResolver.
To compose our resolver middleware functions we will use (you guessed it) the compose function! Here is an example of compose implemented in javascript, but you can also find compose functions in ramda and other functional libraries. Compose lets us combine simple functions to make more complicated functions.
Going back to the GraphQL permissions problem lets look at a simple example.
Say that we want to log the resolver, authorize the user, and then run the meat and potatoes. Compose lets us combine these three pieces such that we can easily test and re-use them across our application.
const traceResolve =
(fn: GraphQLFieldResolver) =>
async (obj: any, args: any, context: any, info: any) => {
const start = new Date().getTime();
const result = await fn(obj, args, context, info);
const end = new Date().getTime();
console.log(`Resolver took ${end - start} ms`);
return result;
};
const isAdminAuthorized =
(fn: GraphQLFieldResolver) =>
async (obj: any, args: any, context: any, info: any) => {
if (!context.user.isAdmin) {
throw new Error('User lacks admin authorization.');
}
return await fn(obj, args, context, info);
}
const getPost = (obj: any, args: any, context: any, info: any) => {
return PostModel.find(args).exec();
}
const getUser = (obj: any, args: any, context: any, info: any) => {
return UserModel.find(args).exec();
}
// You can then define field resolve functions like this:
postResolver: compose(traceResolve, isAdminAuthorized)(getPost)
// And then others like this:
userResolver: compose(traceResolve, isAdminAuthorized)(getUser)

Resources