Using writeFragment to Update a Field Belonging to an Object? - graphql

I'm trying to get my first writeFragment working.
Here's the object shape:
resolutions {
_id
name
completed
goals {
_id
name
completed
}
}
I've just run a mutation on the client that successfully adds a new goal, and now I'm trying to get the client page to auto-update and show the new goal that was just added.
I've got readFragment working. It reads in the Resolution successfully. I'm reading in the Resolution, rather than the goals, because as a field belonging to resolution, the goals don't have an id of their own.
Here's my update function, showing readFragment and writeFragment:
<Mutation
mutation={CREATE_GOAL}
update={(cache, { data: { createGoal } }) => {
let resId = 'Resolution:' + resolutionId;
const theRes = cache.readFragment({
id: resId,
fragment: GET_FRAGMENT_GOAL,
});
theRes.goals = theRes.goals.concat([createGoal]); //<== THIS WORKS
cache.writeFragment({
id: resId,
fragment: SET_FRAGMENT_GOAL,
data: { __typename: 'Resolution', goals: theRes.goals },
});
}}
>
...and here's the gql for the fragments:
const GET_FRAGMENT_GOAL = gql`
fragment targetRes on resolutions {
name
completed
goals {
_id
name
completed
}
}
`;
const SET_FRAGMENT_GOAL = gql`
fragment targetGoal on resolutions {
__typename
goals
}
`;
Here's a console error I'm getting:
You are using the simple (heuristic) fragment matcher, but your queries contain union or interface types.
Apollo Client will not be able to able to accurately map fragments.To make this error go away, use the IntrospectionFragmentMatcher as described in the docs: http://dev.apollodata.com/react/initialization.html#fragment-matcher
I read up on IntrospectionFragmentMatcher and it looks like mega-overkill for my situation. It appears I'm doing something else wrong. Here's the other error I'm getting at the same time:
Uncaught (in promise) TypeError: Cannot read property 'data' of undefined
What's wrong with my call to writeFragment?

After quite a few hours of study, I learned a lot about fragments!
I got it working. Here are the updated fragment and query definitions:
import gql from "graphql-tag";
let resolutionQueryFragments = {
goalParts: gql`
fragment goalParts on Goal {
_id
name
completed
}
`,
};
resolutionQueryFragments.resolutionGoals = gql`
fragment resolutionGoals on Resolution {
goals{
_id
name
completed
}
}
`;
const GET_RESOLUTIONS = gql`
query Resolutions {
resolutions {
_id
name
completed
...resolutionGoals
}
user {
_id
}
}
${resolutionQueryFragments.resolutionGoals}
`;
const CREATE_RESOLUTION = gql`
mutation createResolution($name: String!) {
createResolution(name: $name) {
__typename
_id
name
...resolutionGoals
completed
}
}
${resolutionQueryFragments.resolutionGoals}
`;
const GET_RESOLUTIONS_FOR_MUTATION_COMPONENT = gql`
query Resolutions {
resolutions {
_id
name
completed
...resolutionGoals
}
}
${resolutionQueryFragments.resolutionGoals}
`;
const CREATE_GOAL = gql`
mutation createGoal($name: String!, $resolutionId: String!) {
createGoal(name: $name, resolutionId: $resolutionId) {
...goalParts
}
}
${resolutionQueryFragments.goalParts}
`;
export {resolutionQueryFragments, GET_RESOLUTIONS, GET_RESOLUTIONS_FOR_MUTATION_COMPONENT, CREATE_RESOLUTION, CREATE_GOAL}
...and here's the updated Mutation component:
import React, {Component} from "react";
import gql from "graphql-tag";
import {graphql} from "react-apollo";
import {Mutation} from "react-apollo";
import {withApollo} from "react-apollo";
import {resolutionQueryFragments, CREATE_GOAL} from '../../imports/api/resolutions/queries';
const GoalForm = ({resolutionId, client}) => {
let input;
return (
<Mutation
mutation={CREATE_GOAL}
update={(cache, {data: {createGoal}}) => {
let resId = 'Resolution:' + resolutionId;
let currentRes = cache.data.data[resId];
let theGoals = cache.readFragment({
id: resId,
fragment: resolutionQueryFragments.resolutionGoals
});
theGoals = theGoals.goals.concat([createGoal]);
cache.writeFragment({
id: resId,
fragment: resolutionQueryFragments.resolutionGoals,
data: {goals: theGoals}
});
}}
>
{(createGoal, {data}) => (
<div>
<form
onSubmit={e => {
e.preventDefault();
createGoal({
variables: {
name: input.value,
resolutionId: resolutionId
}
});
input.value = "";
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Submit</button>
</form>
</div>
)}
</Mutation>
)
;
};
export default withApollo(GoalForm);

Related

In Gatsby how do I pass a relative path file to a template that is also pulling a query?

In Gatsby I've coded a single post template:
singlePost.js:
import React from 'react'
import { graphql } from 'gatsby'
import { MDXRenderer } from 'gatsby-plugin-mdx'
import { H1 } from '../elements'
import { Container, Post, FeatureImage } from '../components'
const singlePost = ({ data }) => {
const featureImage = data.mdx.frontmatter.featureImg.childImageSharp.fixed
return (
<Container>
<FeatureImage fixed={featureImage} />
<Post>
<H1 margin=" 0 0 2rem 0">{data.mdx.frontmatter.title}</H1>
<MDXRenderer>{data.mdx.body}</MDXRenderer>
</Post>
</Container>
)
}
export const pageQuery = graphql`
query SinglePostQuery($id: String!) {
mdx(id: { eq: $id }) {
body
frontmatter {
date
excerpt
featureImg {
childImageSharp {
fixed(width: 1920) {
...GatsbyImageSharpFixed
}
}
}
title
slug
}
}
}
`
export default singlePost
In my gatsby-node.js I get the data from the slug:
data.allMdx.edges.map(edge => {
const slug = edge.node.frontmatter.slug
const id = edge.node.id
actions.createPage({
path: slug,
component: require.resolve(`./src/templates/singlePost.js`),
context: { id },
})
})
In the frontmatter of the markdown file there is a feature image:
---
title: Bacon Ipsum
slug: bacon
date: 2021-02-09
featureImg: nature.jpg
excerpt: Bacon ipsum dolor amet pastrami prosciutto meatball fatback, andouille drumstick shank burgdoggen brisket cow turkey.
---
if the post markdown file doesn't have an image I get an error of:
Cannot read property 'childImageSharp' of null
I can access the default image I've set with:
const defaultImage = useStaticQuery(graphql`
query {
default: file(relativePath: { eq: "default.jpg" }) {
publicURL
}
}
`)
but when I try to query for the content and the default with:
const defaultImage = useStaticQuery(graphql`
query SinglePostQuery($id: String!) {
mdx(id: { eq: $id }) {
body
frontmatter {
date
excerpt
featureImg {
childImageSharp {
fixed(width: 1920) {
...GatsbyImageSharpFixed
}
}
}
title
slug
}
}
default: file(relativePath: { eq: "default.jpg" }) {
publicURL
}
}
`)
I get an error of:
If you're not using a page query but a useStaticQuery / StaticQuery
you see this error because they currently don't support variables.
In Gatsby how can I query the slug but also have pass the default image to the Feature Image component?
As the error log points, useStaticQuery (because it's a type of static query) doesn't accept variables, hence the name.
In Gatsby how can I query the slug but also have pass the default
image to the Feature Image component?
You can use the same context where you attach the id to pass the image while using page queries.
data.allMdx.edges.map(edge => {
const slug = edge.node.frontmatter.slug
const id = edge.node.id
const imagePath = edge.node.featureImg || 'default.jpg'
actions.createPage({
path: slug,
component: require.resolve(`./src/templates/singlePost.js`),
context: {
id
imagePath
},
})
})
Note: You may need to query the image in your gatsby-node.js. Change the imagePath to another identifier that matches better your data structure if needed.
Then, in your template:
import React from 'react'
import { graphql } from 'gatsby'
import { MDXRenderer } from 'gatsby-plugin-mdx'
import { H1 } from '../elements'
import { Container, Post, FeatureImage } from '../components'
const singlePost = ({ data }) => {
const featureImage = data.mdx.frontmatter.featureImg.childImageSharp.fixed
return (
<Container>
<FeatureImage fixed={featureImage} />
<Post>
<H1 margin=" 0 0 2rem 0">{data.mdx.frontmatter.title}</H1>
<MDXRenderer>{data.mdx.body}</MDXRenderer>
</Post>
</Container>
)
}
export const pageQuery = graphql`
query SinglePostQuery($id: String!, $imagePath: String) {
mdx(id: { eq: $id }) {
body
frontmatter {
date
excerpt
featureImg {
childImageSharp {
fixed(width: 1920) {
...GatsbyImageSharpFixed
}
}
}
title
slug
}
}
default: file(relativePath: { eq: $imagePath }) {
publicURL
}
}
`
export default singlePost
Note: add the $imagePath as a nullable value (by removing the exclamation mark, !), since, as you said, not all the posts will have it.
Try removing the featureImg block if it stills breaking the query. Because of:
const imagePath = edge.node.featureImg || 'default.jpg'
Your imagePath variable will always contain the needed data,or your featureImg, or your default one. The key is to make separate queries.

Apollo client fragments not embedding data

This is the first time I've ventured into fragments and I can't see where I'm screwing up, but it definitely isn't working! In GraphiQL it's working fine:
query Tasks($taskIds: [String]!) {
tasks(taskIds: $taskIds) {
...taskDisplay
}
}
fragment taskDisplay on Task {
_id
name
description
status
children {
_id
}
}
Here's what's in my client app:
import { gql } from "#apollo/client";
export const TASK_FRAGMENT = gql`
fragment taskDisplay on Task {
_id
name
description
status
children {
_id
}
}
`;
export const TASKS = gql`
query Tasks($taskIds: [String]!) {
tasks(taskIds: $taskIds) {
...taskDisplay
}
}
${TASK_FRAGMENT}
`;
So, the server returns the data correct as I can see in the Network tab of Chrome, but the data received by the useQuery result is an empty object. What gives?
Using #apollo/client#3.2.0-beta.2 (I have downgraded to 3.1.0 with same results)
EDIT:
Adding more info. My code is about as simple as it could be using a hook. Here's what's happening:
import { useQuery, gql } from "#apollo/client";
import { TASK_FRAGMENT } from "../pages/task/queries";
const ROOT_TASK_QUERY = gql`
query Project($projectId: String!) {
rootTask(projectId: $projectId) {
...taskDisplay
}
}
${TASK_FRAGMENT}
`;
const useProject = ({ variables }) => {
return useQuery(ROOT_TASK_QUERY, {
variables,
});
};
export default useProject;
And this is just logging the query itself:
Your returned data is missing the __typename field

local state management for graphql vue apollo

I am trying to make local state management with vue apollo, but even after following the docs I am getting no result. There is no error in the console so I am not sure what is wrong.
Here is my setup:
// main.js file the initializing part
const client = new ApolloClient({
link: ApolloLink.from([
errorLink,
authMiddleware,
link,
]),
cache,
typeDefs,
resolvers,
connectToDevTools: true,
});
// resolvers file
import gql from 'graphql-tag';
import { todoItemsQuery } from './task.queries';
export const typeDefs = gql`
type Item {
id: ID!
text: String!
done: Boolean!
}
type Mutation {
changeItem(id: ID!): Boolean
deleteItem(id: ID!): Boolean
addItem(text: String!): Item
}
`;
export const resolvers = {
Mutation: {
checkItem: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: todoItemsQuery });
console.log('data res', data);
const currentItem = data.todoItems.find(item => item.id === id);
currentItem.done = !currentItem.done;
cache.writeQuery({ query: todoItemsQuery, data });
return currentItem.done;
},
},
};
//queries file
import gql from 'graphql-tag';
export const todoItemsQuery = gql`
{
todoItems #client {
id
text
done
}
}
`;
export const checkItemMutation = gql`
mutation($id: ID!) {
checkItem(id: $id) #client
}
`;
// component where I call it
apollo: {
todoItems: {
query: todoItemsQuery
}
},
checkItem(id) {
this.$apollo
.mutate({
mutation: checkItemMutation,
variables: { id }
})
.then(({ data }) => {
console.log("CACHE", data);
});
},
I get empty todoItems, no errors.
Please let me know what am I missing, I am not grasping some concept I think, and if there is a way to use vuex with apollo then I can do that too.
Foreword: I'm not an Apollo specialist, just starting to use it.
Just in case, these thoughts may help you. If they don't, well, I'm sorry.
In main.js: Apollo documentation provides a slightly different set up when using Apollo Boost (https://www.apollographql.com/docs/link/links/state/#with-apollo-boost). Just in case, this is how I have set up my implementation so far.
import VueApollo from 'vue-apollo'
import ApolloClient from 'apollo-boost';
import { InMemoryCache } from 'apollo-cache-inmemory';
const cache = new InMemoryCache();
Vue.use(VueApollo)
const apolloClient = new ApolloClient({
//...whatever you may already have,
clientState: {
// "defaults" is your initial state - if empty, I think it might error out when your app launches but is not hydrated yet.
defaults: {
todoItems: [],
}
cache,
typeDefs: {...yourTypeDefs},
resolvers: {...yourResolvers},
},
});
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
});
In your typeDefs: I can not see a Query Type for your todoItems:
type Query {
todoItemsQuery: [Item]
}
In your component: my own implementation was not working until I added an update method to the apollo request:
apollo: {
todoItems: {
query: todoItemsQuery,
update: data => data.todoItems
}
},

orderBy: the results from update cache query

Working on a react apollo graphcool project
I've got my mutation update working, however I would like to filter the results, the results only filter on page refresh?
Looking at cache.writeQuery() the docs say get the query and concat to that so i guess thats why its not filtering. Is there anyway to query after?
Here the code from my CreatePost component
import React from 'react';
import gql from "graphql-tag";
import { Mutation } from "react-apollo";
const GET_POSTS = gql`
{
allPosts(orderBy: createdAt_DESC) {
id
title
content
}
}
`;
const CREATE_POST = gql`
mutation createPost($title: String!, $content: String!){
createPost(title: $title, content: $content){
id
title
content
}
}
`;
const udpateCache = (cache, { data: { createPost } }) => {
const { allPosts } = cache.readQuery({ query: GET_POSTS });
cache.writeQuery({
query: GET_POSTS,
data: { allPosts: allPosts.concat([createPost]) }
})
}
const CreatePost = () => {
//vars for the input refs
let titleInput
let contentInput
return (
<Mutation
mutation={CREATE_POST}
update={udpateCache}
>
{createPost => ( //##form and onSubmit ##// ) }
</Mutation>
)
}
export default CreatePost
When you do your writeQuery you also need to pass in any variables used, to make sure you receive the same information from the cache.
const udpateCache = (cache, { data: { createPost } }) => {
const { allPosts } = cache.readQuery({ query: GET_POSTS });
cache.writeQuery({
query: GET_POSTS,
data: { allPosts: allPosts.concat([createPost]) },
variables: { orderBy: /* input */ }
})
}

Missing field on cache.writeQuery in Mutation Component?

I'm studying GraphQL Mutation components. I'm doing a mutation that adds a resolution, i.e. a New Year's resolution. Here's the schema:
type Resolution {
_id: String!
name: String!
goals: [Goal]
completed: Boolean
}
type Query {
resolutions: [Resolution]
}
type Mutation {
createResolution(name: String!): {
Resolution
user: String
}
}
Here are the resolution resolvers:
import Resolutions from "./resolutions";
import Goals from "../goals/goals";
import { PubSub } from 'graphql-subscriptions';
export const pubsub = new PubSub();
export default {
Query: {
resolutions(obj, args, { userId }) {
return Resolutions.find({
userId
}).fetch();
}
},
Resolution: {
goals: resolution =>
Goals.find({
resolutionId: resolution._id
}).fetch(),
completed: resolution => {
const goals = Goals.find({
resolutionId: resolution._id
}).fetch();
if (goals.length === 0) return false;
const completedGoals = goals.filter(goal => goal.completed);
return goals.length === completedGoals.length;
}
},
Mutation: {
createResolution(obj, { name }, { userId }) {
if (userId) {
const resolutionId = Resolutions.insert({
name,
userId
});
return Resolutions.findOne(resolutionId);
}
throw new Error("Unauthortized");
}
},
};
Here's the user resolver:
export default {
Query: {
user(obj, args, { user }) {
return user || {};
}
},
User: {
email: user => user.emails[0].address
}
};
Here's the mutation component:
const ResolutionForm = () => {
let input;
let state = {
error: null
};
return (
<Mutation
mutation={CREATE_RESOLUTION}
update={(cache, {data: {createResolution}}) => {
const {resolutions} = cache.readQuery({query: GET_RESOLUTIONS});
cache.writeQuery({
query: GET_RESOLUTIONS,
data: {resolutions: resolutions.concat([createResolution])}
});
}}
>
{(createResolution, {data}) => (
<div>
<form
onSubmit={e => {
e.preventDefault();
createResolution({
variables: {
name: input.value
},
});
input.value = "";
}}
>
<input
ref={node => {
input = node;
}}
/>
<button type="submit">Submit</button>
</form>
</div>
)}
</Mutation>
);
};
Here's the query that loads all the resolutions when the app launches:
const GET_RESOLUTIONS = gql`
query Resolutions {
resolutions {
_id
name
completed
goals {
_id
name
completed
}
}
user {
_id
}
}
`;
That works fine, but when I run the mutation:
const CREATE_RESOLUTION = gql`
mutation createResolution($name: String!) {
createResolution(name: $name) {
__typename
_id
name
goals {
_id
name
completed
}
completed
}
}
`;
...I get a console log error saying:
Missing field user in {
"resolutions": [
{
"_id": "GKTNgbuiDgiZ4wAFZ",
"name": "testing 123",
.....
How do I get the field user into my mutation response?
The GET_RESOLUTIONS query used is originally from a parent component, App.js. It really contains two separate queries-- one for the resolution and one for the user. The CREATE_RESOLUTION Mutation query and resolver, don't return user data, and I don't yet know how to get them to do that.
But, the Mutation component doesn't need the user data. It only gets upset during the call to cache.writeQuery because GET_RESOLUTIONS is asking for user, and the Mutation resolver isn't returning user.
So the fix seems to be to have a special GET_RESOLUTIONS_FOR_MUTATION_COMPONENT query that doesn't ask for user in the first place:
const GET_RESOLUTIONS_FOR_MUTATION_COMPONENT = gql`
query Resolutions {
resolutions {
_id
name
completed
goals {
_id
name
completed
}
}
}
`;
[.....]
const {resolutions} = cache.readQuery({query: GET_RESOLUTIONS_FOR_MUTATION_COMPONENT});
[.....]
Using that there is no error message asking for user.

Resources