GraphQL : Implementing windowed pagination for regular list - graphql

I'm trying to implement a windowed pagination using a "List". I don't need the cursor based solution with connections, because I need to show numbered pages to the user.
There are "User" and "Post" objects."User" has one-to-many relation to "Post".
Using graphql-js for schema,
here is my schema for userType and postType:
var userType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: globalIdField('User'),
posts: {
type: new GraphQLList(postType),
args: {
page:{
type: GraphQLInt,
defaultValue: 0
}
},
resolve: (_, args) => {
//code to return relevant result set
},
},
totalPosts:{
type: GraphQLInt,
resolve: () => {
//code to return total count
}
},
}),
interfaces: [nodeInterface],
});
var postType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: globalIdField('Post'),
name: {type: GraphQLString},
//other fields
}),
interfaces: [nodeInterface],
});
Please notice the "totalPosts" field in "userType". Since there is going to be other Lists for the user,with the same paging needs, I'm going to end up maintaining lot of "total{Type}" variables in the fragment. This can be solved if I can send the totalCount within the List result somehow.
https://github.com/facebook/graphql/issues/4 this issue talks about implementing a wrapper over the List to include the totalCount in the result set.
I tried creating a wrapper like this:
var postList = new GraphQLObjectType({
name: 'PostList',
fields:()=>({
count: {
type: GraphQLInt,
resolve: ()=>getPosts().length //this is total count
},
edges: {
type: new GraphQLList(postType),
resolve: () => {
return getPosts() ; // this is results for the page, though I don't know how to use 'page' argument here
},
}
}),
interfaces: [nodeInterface],
});
but how should I connect this to the userType's posts field? And how can I use a 'page' argument on this wrapper, like I have in original userType?

how should I connect this to the userType's posts field? And how can I use a 'page' argument on this wrapper, like I have in original userType?
One simple way to implement what you're trying to do is to define a dumb wrapper type postList like this:
var postList = new GraphQLObjectType({
name: 'PostList',
fields:()=>({
count: { type: GraphQLInt },
edges: { type: new GraphQLList(postType) }
// Consider renaming 'edges'. In your case, it's a list, not a
// connection. So, it can cause confusion in the long run.
}),
});
Then in the userType definition, add a field of that wrapper type and define its resolve function like below. As for argument page, just describe it while defining the field type posts.
posts: {
type: postList,
args: {
page:{
type: GraphQLInt,
defaultValue: 0
},
...otherArgs
},
resolve: async (_, {page, ...otherArgs}) => {
// Get posts for the given page number.
const posts = await db.getPosts(page);
// Prepare a server-side object, which corresponds to GraphQL
// object type postList.
const postListObj = {
count: posts.length,
edges: posts
};
// Consider renaming 'edges'. In your case, it's a list, not a
// connection. So, it can cause confusion in the long run.
},
},

Related

graphqljs resolver with multiple arguments

I am trying to figure out the best way to write a resolver that filters on multiple arguments. I have the following graphql type
const userQuery = new GraphQLObjectType({
name: 'Query',
fields: {
Users: {
type: new GraphQLList(User),
args: {
userId: { type: GraphQLString }
},
resolve: function (_, { UserId}) {
return new Promise((resolve, reject) => {
//Code to query the data store for the user with the given UserId
})
}
}
}
});
The User type has the following fields
Name
UserId
Type
Gender
Now if I want to introduce the ability to filter the user based on the name, then what is the best way to do it. The only way I can think of is to modify the resolver to include the additional args and then based on what is passed in send it to the database. For example
const userQuery = new GraphQLObjectType({
name: 'Query',
fields: {
Users: {
type: new GraphQLList(User),
args: {
userId: { type: GraphQLString }
},
resolve: function (_, { UserId, name}) {
return new Promise((resolve, reject) => {
//Check which argument is passed in and then run the query against the datastore
})
}
}
}
});
Isn't there a better way to do this? If I want the user to be able to filter on another attribute then it gets more complicated, and the resolve function is going to get huge and complicated.

I have confusion on relay and graphql resolve method

Apologies if this is a stupid question. this is the code for relay/graphql pagination that's confusing me:
const GraphQLTodo = new GraphQLObjectType({
name: 'Todo',
fields: {
id: globalIdField('Todo'),
text: {
type: GraphQLString,
resolve: (obj) => obj.text,
},
complete: {
type: GraphQLBoolean,
resolve: (obj) => obj.complete,
},
},
interfaces: [nodeInterface],
});
/* When pagination is needed, make a connection */
const {
connectionType: TodosConnection,
edgeType: GraphQLTodoEdge,
} = connectionDefinitions({
name: 'Todo',
nodeType: GraphQLTodo,
});
const GraphQLUser = new GraphQLObjectType({
name: 'User',
fields: {
id: globalIdField('User'),
todos: {
type: TodosConnection,
args: {
status: {
type: GraphQLString,
defaultValue: 'any',
},
...connectionArgs,
},
resolve: (obj, {status, ...args}) =>
connectionFromArray(getTodos(status), args),
},
totalCount: {
type: GraphQLInt,
resolve: () => getTodos().length,
},
completedCount: {
type: GraphQLInt,
resolve: () => getTodos('completed').length,
},
},
interfaces: [nodeInterface],
});
const Root = new GraphQLObjectType({
name: 'Root',
fields: {
viewer: {
type: GraphQLUser,
resolve: () => getViewer(),
},
node: nodeField,
},
});
You can see that on the GraphQLTodo field, it has text and complete fields with resolve function passed an obj parameter, how is obj passed there? is it from GraphQLUser resolve? I've read on docs that source(in this case obj) - The object resolved from the field on the parent type. is it not from the root query? how is obj here created?
The Connection
Here is where (some of) the magic happens:
const {
connectionType: TodosConnection,
edgeType: GraphQLTodoEdge,
} = connectionDefinitions({
name: 'Todo',
nodeType: GraphQLTodo,
});
You have now told GraphQL that a TodosConnection is going to be made up of GraphQLTodo nodes. Now, let's take a look at where the objects are actually fetched for the connection in your GraphQLUser object, which is on the todos field:
todos: {
type: TodosConnection,
args: {
status: {
type: GraphQLString,
defaultValue: 'any',
},
...connectionArgs,
},
resolve: (obj, {status, ...args}) =>
connectionFromArray(getTodos(status), args),
},
So where does the object come from? The key part here is the getTodos function, which is responsible for actually getting an array of the objects from your data source. Since this field is a TodosConnection and we've already specified in the connection definitions that the nodes are GraphQLTodos, GraphQL knows that the text and complete fields are resolved by getting (in this case) identically named fields on the objects that have been returned. In other words, the returned object is passed to the resolve method on each field.
Querying the Root
You have two fields exposed on Root: viewer and node. Ignoring node for a moment, you have just one way to actually query todos. Since viewer is of type GraphQLUser, and GraphQLUser has that todos field, they can be fetched only as a subfield of viewer, like this:
{
viewer {
todos(first: 10) {
edges {
# each node is a Todo item
node {
text
complete
}
}
}
}
}
Mystery of the Node
But what about that node field? Relay wants to be able to fetch any object using a top-level query, i.e. on your Root field, when given a unique globalId, which is just a base64 encoding of the type name and the id, so Todo:1 is encoded to VG9kbzox. This is set up in the nodeDefinitions (which you haven't included here, but probably have). In those definitions, the globalId is parsed back into the type (Todo) and id (1), and once again you then tell it how to fetch the correct object from your data source. It might look something like:
const { nodeInterface, nodeField } = nodeDefinitions(
(globalId) => {
const { type, id } = fromGlobalId(globalId);
if (type === 'Todo') {
return getTodo(id)
} else if (type === 'User') {
return getUser(id)
}
...
Because you're implementing the nodeInterface in both your GraphQLTodo and GraphQLUser types, Relay will be able query for either of them from the Root's node field.

Elegant and efficient way to resolve related data in GraphQL

What can be the best way to resolve the data in GraphQL
Here i have a SeekerType and JobType, JobsType is nested in SeekerType
A Seeker can apply to many Jobs. When Querying for a seeker, One can just query for seeker's data or as well as he can query for nested JobType and can get the jobstype data too.
But the Question is that If One doesn't Query for nested JobType
he won't get the Jobs data but mine Seeker resolver in viewerType would be fetching that data too.
So, while providing data to the seeker query how can i handle that, Either he can only want seeker details or may want the jobs details too.
Shall I use resolver of each nestedType and get the parent object, and fetch the relevant data using fields from parent Object???
The code below is just for illustration and clarification, the question is about the best way to resolve data
ViewerType.js
const Viewer = new GraphQLObjectType({
name: 'Viewer',
fields: () => ({
Seeker: {
type: SeekerConnection,
args: _.assign({
seekerId: { type: GraphQLID },
status: { type: GraphQLString },
shortlisted: { type: GraphQLInt },
}, connectionArgs),
resolve: (obj, args, auth, rootValue) => {
const filterArgs = getFilters(args) || {};
return connectionFromPromisedArray(getSeekers(filterArgs), args)
.then((data) => {
// getSeekers() provides all the data required for SeekerType fields and it's
JobsType fields
data.args = filterArgs;
return data;
}).catch(err => new Error(err));
},
},
}),
});
SeekerType.js
const SeekerType = new GraphQLObjectType({
name: 'SeekerType',
fields: () => ({
id: globalIdField('SeekerType', obj => obj._id),
userId: {
type: GraphQLID,
resolve: obj => obj._id,
},
email: { type: GraphQLString },
password: { type: GraphQLString },
firstName: { type: GraphQLString },
lastName: { type: GraphQLString },
imageLink: { type: GraphQLString },
education: { type: GraphQLString },
address: { type: GraphQLString },
jobs: {
type: new GraphQLList(JobType),
},
}),
interfaces: [nodeInterface],
});
getSeekers() provide complete data as graphql fields format with nested
jobs field data too
const getSeekers = filterArgs => new Promise((resolve, reject) => {
if (Object.keys(filterArgs).length === 0) {
Seeker.find(filterArgs, { password: 0 }, (err, d) => {
if (err) return reject(err);
return resolve(d);
});
} else {
async.parallel([
(callback) => {
filterArgs._id = filterArgs.seekerId;
delete filterArgs.seekerId;
Seeker.find(filterArgs).lean()
.exec((err, d) => {
if (err) return callback(err);
if (err === null && d === null) return callback(null);
callback(null, d);
});
},
(callback) => {
filterArgs.seekerId = filterArgs._id;
delete filterArgs._id;
Applicant.find(filterArgs).populate('jobId').lean()
.exec((err, resp) => {
if (err) return callback(err);
callback(null, resp);
});
},
], (err, data) => {
const cleanedData = {
userData: data[0],
userJobMap: data[1],
};
const result = _.reduce(cleanedData.userData, (p, c) => {
if (c.isSeeker) {
const job = _.filter(cleanedData.userJobMap,
v => _.isEqual(v.seekerId, c._id));
const arr = [];
_.forEach(job, (i) => {
arr.push(i.jobId);
});
const t = _.assign({}, c, { jobs: arr });
p.push(t);
return p;
}
return reject('Not a Seekr');
}, []);
if (err) reject(err);
resolve(result);
// result have both SeekerType data and nested type
JobType data too.
});
}
});
I gather this to be a question about how to prevent overfetching related data...I.e. How not to necessarily request jobs data when querying the seeker.
This might have several motivations for optimization and security.
Considerations:
If the consumer (e.g. Web app) is under your control, you can simply avoid requesting the jobs field when querying seeker. As you may know, this is one of the stated goals of graphql to only return what is needed over the wire to the consumer, to minimize network traffic and do things in one trip. On the backend I would imagine the graphql engine is smart enough not to overfetch jobs data either, if it's not requested.
If your concern is more of security or unintentional overfetching by consumer apps out of your control, for example, then you can solve that by creating seperate queries and limiting access to them even. E.g. One query for seeker and another for seekerWithJobsData.
Another technique to consider is graphql directives which offer an include switch that can be used to conditionally serve specific fields. One advantage of using this technique in your scenario might be to allow a convenient way to conditionally display multiple fields in multiple queries depending on the value of a single boolean e.g. JobSearchFlag=false. Read here for an overview of directives: http://graphql.org/learn/queries/
I am not sure I completely understand the question but it seems to me you're loading both seeker and job types info on one level. You should load both of them on demand.
On the seeker level, you only get the seeker information, and you can get the IDs of any records related to that seeker. For example, job types ids (if a seeker has many job types)
On the job type level, when used as a nested level for one seeker, you can use those ids to fetch the actual records. This would make the fetching of job types record on-demand when the query asks for it.
The ID to record fetching can be cached and batched with a library like dataloader

GraphQL pass args to sub resolve

I have a relationship between User and Post. This is how I query the User Posts.
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }) {
// I want to get here the args.someBooleanArg
return someLogicToGetUserPosts();
}
}
})
});
The main query is:
const queryType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(UserType),
args: {
id: {
type: GraphQLInt
},
someBooleanArg: {
type: GraphQLInt
}
},
resolve: (root, { id, someBooleanArg }, { db }) => {
return someLogicToGetUsers();
}
}
}
});
The problem is the args in the resolve function of the UserType posts is empty object, how do i pass the args from the main query to sub resolves functions?
When resolving the root query you can use object assign to attach the argument to the user object returned.
Then, on the user type, resolve the argument from the root value (first argument of resolve function).
Example:
const queryType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(UserType),
args: {
id: {
type: GraphQLInt
},
someBooleanArg: {
type: GraphQLInt
}
},
resolve: (root, { id, someBooleanArg }, { db }) => {
return Promise.resolve(someLogicToGetUsers()).then(v => {
return Object.assign({}, v, {
someBooleanArg
});
});
}
}
}
});
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }) {
console.log(parent.someBooleanArg);
return someLogicToGetUserPosts();
}
}
})
});
You can use the resolver fouth argument, info, to receive the desired variable - from Apollo docs:
Every resolver in a GraphQL.js schema accepts four positional arguments:
fieldName(obj, args, context, info)
{ result }
These arguments have
the following meanings and conventional names:
obj: The object that contains the result returned from the resolver on
the parent field, or, in the case of a top-level Query field, the
rootValue passed from the server configuration. This argument enables
the nested nature of GraphQL queries.
args: An object with the
arguments passed into the field in the query. For example, if the
field was called with author(name: "Ada"), the args object would be: {
"name": "Ada" }.
context: This is an object shared by all resolvers in
a particular query, and is used to contain per-request state,
including authentication information, dataloader instances, and
anything else that should be taken into account when resolving the
query. If you're using Apollo Server, read about how to set the
context in the setup documentation.
info: This argument should only be
used in advanced cases, but it contains information about the
execution state of the query, including the field name, path to the
field from the root, and more. It's only documented in the GraphQL.js
source code.
The info seems to be a very undocumented feature, but I'm using it now with no problems (at least until somebody decide to change it).
Here is the trick:
const UserType = new GraphQLObjectType({
name: 'User'
fields: () => ({
name: {
type: GraphQLString
},
posts: {
type: new GraphQLList(PostType),
resolve(parent, args , { db }, info) {
// I want to get here the args.someBooleanArg
console.log("BINGO!");
console.log(info.variableValues.someBooleanArg);
return someLogicToGetUserPosts();
}
}
})
});

How to authenticate in relay

what is the correct point to authenticate a user ?
going by the relay starter kit as an example.
this would seem like be the point to query (i have added the args id )
var queryType = new GraphQLObjectType({
name: 'Query',
fields: () => ({
node: nodeField,
// Add your own root fields here
viewer: {
args: {
id: {
type: GraphQLString
},
},
type: userType,
resolve: (_, args) => getViewer(args.id),
},
}),
});
then in the database do something like
getViewer: (id) => id === viewer.id ? viewer : null,
now its this point where it's falling apart, where would be the place to request the id be made from ? i would assume the route
export default class extends Relay.Route {
static queries = {
viewer: () => Relay.QL`
query {
viewer(id:"1")
}
`,
};
static routeName = 'AppHomeRoute';
}
this isn't working.
First you need to drop an auth middleware into your server (http://passportjs.org/ for instance).Then you have to pass the auth information to the graphql middleware (read about how to do it here https://github.com/graphql/express-graphql#advanced-options) and you can finally access that information using the 3rd argument to the resolve(parentValue, args, -->session) function. Here's what the actual auth endpoint could look like https://github.com/igorsvee/react-relay-example/blob/master/server/routes.js#L29-L51

Resources