Mock Relay-style pagination with Apollo Server - javascript

I have a mock server using Apollo Server to return GraphQL responses. One of the queries is to get teams which has an array of metrics (see schema below):
const mocks = {
Query: () => ({
teams: (/*parent, args, context, info*/) => teamsFixture,
}),
};
const graphServer = new ApolloServer({ typeDefs: schema, mocks });
graphServer.applyMiddleware({ app });
And my query used to be (fields redacted):
teams {
bpxId
metrics {
timestamp
}
}
The real (java) server has changed this to allow me to query (Relay style) only the first item in the metrics array as it wasn't performant:
teams {
bpxId
metrics(first: 1) {
edges {
node {
timestamp
}
}
}
}
With the response in this shape:
metrics: {
edges: [
{
node: [Team]
__typename: "TeamMetricsConnectionEdge"
}
]
__typename: "TeamMetricsConnection"
}
I want to add this capability to my Apollo Server mocks but not sure how?
New schema (relevant bits):
type TeamMetrics {
timestamp: Int
# etc
}
type TeamMetricsConnection {
edges: [TeamMetricsConnectionEdge]
pageInfo: PageInfo
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String
endCursor: String
}
type Query {
teams(bpxId: Int): [Team]
}
type Team {
bpxId: Int!
metrics(first: Int! = 5, after: String): TeamMetricsConnection
}
How can I adjust my mock response to handle Relay pagination? Thanks

You can use graphql-relay to create mock resolver results. This could be an easy way to create pagination for static mock arrays. Use connectionFromArray to wrap an array of mocks to automatically create an object structure that fits the connection type. It is possible to access all the field arguments in the mock similarly how you would do it in a real resolver and pass them into the function.
I am not sure if this works when you call the function on a MockList though. Pagination is a bit tricky as well if the length of the results changes all the time (the pageInfo.hasNextPage might be mixed up and you might violate a lot of assumptions that Relay has according to the spec). So it might be fine to start with a simple static array if that satisfies your mocking needs.

Related

Graphql mutation return value [duplicate]

Hi I am trying to learn GraphQL language. I have below snippet of code.
// Welcome to Launchpad!
// Log in to edit and save pads, run queries in GraphiQL on the right.
// Click "Download" above to get a zip with a standalone Node.js server.
// See docs and examples at https://github.com/apollographql/awesome-launchpad
// graphql-tools combines a schema string with resolvers.
import { makeExecutableSchema } from 'graphql-tools';
// Construct a schema, using GraphQL schema language
const typeDefs = `
type User {
name: String!
age: Int!
}
type Query {
me: User
}
`;
const user = { name: 'Williams', age: 26};
// Provide resolver functions for your schema fields
const resolvers = {
Query: {
me: (root, args, context) => {
return user;
},
},
};
// Required: Export the GraphQL.js schema object as "schema"
export const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
// Optional: Export a function to get context from the request. It accepts two
// parameters - headers (lowercased http headers) and secrets (secrets defined
// in secrets section). It must return an object (or a promise resolving to it).
export function context(headers, secrets) {
return {
headers,
secrets,
};
};
// Optional: Export a root value to be passed during execution
// export const rootValue = {};
// Optional: Export a root function, that returns root to be passed
// during execution, accepting headers and secrets. It can return a
// promise. rootFunction takes precedence over rootValue.
// export function rootFunction(headers, secrets) {
// return {
// headers,
// secrets,
// };
// };
Request:
{
me
}
Response:
{
"errors": [
{
"message": "Field \"me\" of type \"User\" must have a selection of subfields. Did you mean \"me { ... }\"?",
"locations": [
{
"line": 4,
"column": 3
}
]
}
]
}
Does anyone know what I am doing wrong ? How to fix it ?
From the docs:
A GraphQL object type has a name and fields, but at some point those
fields have to resolve to some concrete data. That's where the scalar
types come in: they represent the leaves of the query.
GraphQL requires that you construct your queries in a way that only returns concrete data. Each field has to ultimately resolve to one or more scalars (or enums). That means you cannot just request a field that resolves to a type without also indicating which fields of that type you want to get back.
That's what the error message you received is telling you -- you requested a User type, but you didn't tell GraphQL at least one field to get back from that type.
To fix it, just change your request to include name like this:
{
me {
name
}
}
... or age. Or both. You cannot, however, request a specific type and expect GraphQL to provide all the fields for it -- you will always have to provide a selection (one or more) of fields for that type.

Send an enum to graphql API from react app

I have an input (attached image) that I need to send to the graphql api from react application.
I am using below code to send this object and enum with init to graphql api
Reactjs Code
const RequestActionEnum = {
NEW: 'New',
UPDATE: 'Update',
ARCHIVE: 'Archive'
}
LocalCodeMutation({
variables: {
data: {
id: null,
city: values.jurisdiction,
country: values.country,
description: values.description,
edition: values.edition,
name: values.codeName,
note: 'test',
requestType: RequestActionEnum.NEW, // this is where i am sending enum value to api
state: values.state
}
}
});
Below code is where I am calling the mutation
const [LocalCodeMutation] = useMutation(LOCALCODE_MUTATION, {
refetchQueries: () => [
{ query: GET_LOCALCODES },
],
});
export const LOCALCODE_MUTATION = gql`
mutation LocalCodeMutation($data: LocalCodeRequestParamsInput) {
localCodeMutation(data: $data) {
ok
errors
localCodeInsertedId
}
}
`;
I am getting this error when I send to the API:
Error: GraphQL error: Variable $data got invalid value.
How can I send enum value to graphQL api from react component.
Could any one please suggest any ideas on this?
The enum values for RequestActionEnum are
NEW
UPDATE
ARCHIVE
If you were using this enum as a literal (not a variable), you would write it like this:
{
someField(someArgument: NEW)
}
Similarly, if you're using variables, you would use "NEW". Instead, you're using "New", which is not a valid enum value for this particular enum.
FWIW, if you actually read through to the end of the error, it would tell you as much as well.

Resolving relational document using Apollo Graphql Server

I have implementation of Post Comment models in Apollo graphql which schema is and I want to know which implementation is correct?
type Post {
id: ID!
title: String
image: File
imagePublicId: String
comments: [Comment] # we have type for Comment in another schema file
createdAt: String
updatedAt: String
}
extend type Query {
# Gets post by id
getPosts(authUserId: ID!, skip: Int, limit: Int): Post
}
and I have resolver which resolves Post type and resolves comment by help of populate function of mongoose as bellow:
const Query = {
getPosts: async (root, { authUserId, skip, limit }, { Post }) => {
const allPosts = await Post.find(query)
.populate({
path: 'comments',
options: { sort: { createdAt: 'desc' } },
populate: { path: 'author' },
})
.skip(skip)
.limit(limit)
.sort({ createdAt: 'desc' });
return allPosts
}
}
the second way possible way of implementing getPosts query in resolver is by not using populate function of mongoose and resolve it manually by writing a separate function for it:
const Query = {
getPosts: async (root, { authUserId, skip, limit }, { Post }) => {
const allPosts = await Post.find(query)
.skip(skip)
.limit(limit)
.sort({ createdAt: 'desc' });
return allPosts
}
Post: {
comments: (root, args, ctx, info) => {
return Comment.find({post: root._id}).exec()
}
}
}
It depends.
A resolver is only fired if its field is requested. So if the getPosts resolver fetches the posts without comments, and the comments resolver fetches the comments for each post, the comments will only be fetched if the comments field is included in the request. This can improve the performance of such requests by preventing overfetching on the backend.
On the other hand, by individually querying the comments for each post, you're drastically increasing the number of requests to your database (the n+1 problem). We can avoid this issue by fetching all the posts and all the comments in one query, but, again, we might not necessarily need the comments at all.
There's two options for resolving this dilemma:
Fetch the comments inside the comments resolver, but use dataloader to batch the database requests. This way you're making 2 database requests instead of n + 1 many.
Parse the GraphQLResolveInfo object passed as the fourth parameter to your resolver to determine whether the comments field was requested. This way, you can conditionally add the populate call only if the comments field was actually requested.

Add custom GraphQL resolvers and types into Prisma/Nexus schema

Using: TypeScript, Prisma, MySQL, GraphQLServer, ApolloClient, building schema this way:
const schema = makePrismaSchema({
// Provide all the GraphQL types we've implemented
types: [Query, Mutation, User, Post],...
And then:
const server = new GraphQLServer({
schema,
context: { prisma }
});
How to combine that with custom resolvers and types unrelated to the SQL?
(I would like to call some REST endpoint by the GQL as well)
While nexus was created to be used alongside prisma, it's really just a schema builder. You could easily use it to create a schema without even utilizing Prisma. For example:
export const User = prismaObjectType({
name: 'User',
definition(t) {
t.list.field('comments', {
type: 'Comment',
resolve(root, args, ctx) {
return getComments();
},
});
},
})
export const Comment = prismaObjectType({
name: 'Comment',
definition(t) {
t.string('body');
},
})
Here getComments can return an array of comment objects, or a Promise that resolves to one. For example, if you're calling some other API, you'll normally return a Promise with the results of the call. As shown above, the resolver exposes the parent value, the field's arguments and a context object -- you can use any of this information in determining how to resolve a particular field.

What type of Relay mutator configuration is appropriate for inserting a new record?

I'm diving into GraphQL and Relay. So far, everything has been relatively smooth and easy, for me, to comprehend. I've got a GraphQL schema going with Accounts and Teams. There is no relationships between the two, yet. I've got some Relay-specific GraphQL adjustments for connections, for both the accounts and teams. Here's an example query for those two connections ...
{
viewer {
teams {
edges {
node {
id,
name
}
}
}
accounts {
edges {
node {
id,
username
}
}
}
}
}
I've got a GraphQL mutation ready to go that creates a new account. Here's the GraphQL representation of that ...
type Mutation {
newAccount(input: NewAccountInput!): NewAccountPayload
}
input NewAccountInput {
username: String!
password: String!
clientMutationId: String!
}
type NewAccountPayload {
account: Account
clientMutationId: String!
}
type Account implements Node {
id: ID!
username: String!
date_created: String!
}
I'm now trying to create my client-side Relay mutation that uses this GraphQL mutation. I'm thoroughly confused as to how to do this correctly, though. I've followed examples and nothing I come up with even seems to run correctly. I tend to get errors relating to fragment composition.
If I were writing a Relay mutation that uses this GraphQL mutation, what would the appropriate mutator configuration be? Should I be using RANGE_ADD?
For your client side mutation, you can use something like this:
export default class AddAccountMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on Viewer {
id,
}
`,
};
getMutation() {
return Relay.QL`mutation{addAccount}`;
}
getVariables() {
return {
newAccount: this.props.newAccount,
};
}
getFatQuery() {
return Relay.QL`
fragment on AddAccountPayload {
accountEdge,
viewer {
accounts,
},
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'accounts',
edgeName: 'accountEdge',
rangeBehaviors: {
'': 'append',
},
}];
}
getOptimisticResponse() {
return {
accountEdge: {
node: {
userName: this.props.newAccount.userName,
},
},
viewer: {
id: this.props.viewer.id,
},
};
}
}
Then, in your GraphQL schema, you'll need to return the newly created edge, as well as the cursor:
var GraphQLAddAccountMutation = mutationWithClientMutationId({
name: 'AddAccount',
inputFields: {
newAccount: { type: new GraphQLNonNull(NewAccountInput) }
},
outputFields: {
accountEdge: {
type: GraphQLAccountEdge,
resolve: async ({localAccountId}) => {
var account = await getAccountById(localAccountId);
var accounts = await getAccounts();
return {
cursor: cursorForObjectInConnection(accounts, account)
node: account,
};
}
},
viewer: {
type: GraphQLViewer,
resolve: () => getViewer()
},
},
mutateAndGetPayload: async ({ newAccount }) => {
var localAccountId = await createAccount(newAccount);
return {localAccountId};
}
});
var {
connectionType: AccountsConnection,
edgeType: GraphQLAccountEdge,
} = connectionDefinitions({
name: 'Account',
nodeType: Account,
});
You'll need to substitute the getAccounts(), getAccountById() and createAccount method calls to whatever your server/back-end uses.
There may be a better way to calculate the cursor without having to do multiple server trips, but keep in mind the Relay helper cursorForObjectInConnection does not do any kind of deep comparison of objects, so in case you need to find the account by an id in the list, you may need to do a custom comparison:
function getCursor(dataList, item) {
for (const i of dataList) {
if (i._id.toString() === item._id.toString()) {
let cursor = cursorForObjectInConnection(dataList, i);
return cursor;
}
}
}
Finally, add the GraphQL mutation as 'addAccount' to your schema mutation fields, which is referenced by the client side mutation.
Right now, I'm following a roughly 5 step process to define mutations:
Define the input variables based on what portion of the graph you are targeting - in your case, it's a new account, so you just need the new data
Name the mutation based on #1 - for you, that's AddAccountMutation
Define the fat query based on what is affected by the mutation - for you, it's just the accounts connection on viewer, but in the future I'm sure your schema will become more complex
Define the mutation config based on how you can intersect the fat query with your local graph
Define the mutation fragments you need to satisfy the requirements of #1, #3 and #4
Generally speaking, step #4 is the one people find the most confusing. That's because it's confusing. It's hard to summarize in a Stack Overflow answer why I feel this is good advice but... I recommend you use FIELDS_CHANGE for all your mutations*. It's relatively easy to explain and reason about - just tell Relay how to look up the nodes corresponding to the top level fields in your mutation payload. Relay will then use the mutation config to build a "tracked query" representing everything you've requested so far, and intersect that with the "fat query" representing everything that could change. In your case, you want the intersected query to be something like viewer { accounts(first: 10) { edges { nodes { ... } } }, so that means you're going to want to make sure you've requested the existing accounts somewhere already. But you almost certainly have, and if you haven't... maybe you don't actually need to make any changes locally for this mutation!
Make sense?
EDIT: For clarity, here's what I mean for the fat query & configs.
getConfigs() {
return [
{
type: "FIELDS_CHANGE",
fieldIDs: {
viewer: this.props.viewer.id
}
}]
}
getFatQuery() {
return Relay.QL`
fragment on AddAccountMutation {
viewer {
accounts
}
}
`
}
*addendum: I currently believe there are only one or two reasons not to use FIELDS_CHANGE. The first is that you can't reliably say what fields are changing, so you want to just manipulate the graph directly. The second is because you decide you need the query performance optimizations afforded by the more specific variants of FIELDS_CHANGE like NODE_DELETE, RANGE_ADD, etc.

Categories

Resources