Hotchocolate Strawberryshake how to use a mutation? - hotchocolate

There seem to be no examples at all on how to do a mutation by a graphql client using Strawberryshake (C#). Using Strawberryshake Version11.0.0-preview.138, Abstractions and Http v. 10.3.0-preview.12 and CodeGeneration.CSharp.Analyzers v. 0.0.30, I can run the query from the example just fine. Now I added a mutation, but I can't figure how to use it?
I got the following from the schema import (Alarm.graphql):
schema {
query: Query
mutation: Mutation
}
type Query {
alarms: [Alarm]
}
type Mutation {
addAlarm(input: AlarmInput): AlarmPayload
}
type AlarmPayload {
alarm: Alarm
}
type Alarm {
id: Int!
handle: Int!
messageId: Int!
message: String
}
input AlarmInput {
id: Int!
handle: Int!
messageId: Int!
message: String
}
The query using
query getAlarms {
alarms {
message
}
}
is just fine. For the mutation I added
mutation writeAlarm {
addAlarm {
alarm {
id
handle
messageId
message
}
}
}
Which successfully generates quite a lot of sensible looking classes on build. Now I create a connection to the server:
var serviceCollection = new ServiceCollection();
serviceCollection.AddHttpClient(
"ConnectorLogGraphQL",
c => c.BaseAddress = new Uri("http://localhost:5000/graphql"));
serviceCollection.AddAlarmClient();
var services = serviceCollection.BuildServiceProvider();
IAlarmClient client = services.GetRequiredService<IAlarmClient>();
The client is what I need to use. For the query, I can use:
var result = await client.GetAlarmsAsync();
similarly
var result = await client.WriteAlarmAsync();
compiles just fine, but of course it doesn't do what I'd like to. There's some WriteAlarmOperation defined, which I could pass as a parameter to WriteAlarmAsync, but this operation doesn't have fields for the input either. Something's missing.. ?

Related

How can I map data of multiple collections in snapshot?

I am not too confident working with Firestore and have trouble with more complex API calls to get data. Usually I use SQL backends in my apps.
For the section that I am working on, I would like to combine three collections to get an array of ToDos with the involved users and the category the current user labelled this ToDo with. Every involved person can label the ToDo like they prefer, which makes things a little more complicated. Broken down the collections are structured as follows.
todo: Firestore Database Document
{
title: string,
involved: string[], //user ids
involvedCategory: string[] //category ids mapped by index to involved
}
(I tried to have an array of objects here instead of the two arrays, but it seems I would not be able to query the array for the current user´s ID, like mentioned here, so this is a workaround)
category: Firestore Database Document
{
title: string,
color: string
}
user: Firebase Authentication User
{
uid: string,
displayName: string,
photoURL: string,
...
}
THE GOAL
An array of ToDo items like this:
{
id: string,
title: string,
involved: User[],
category?: {
title: string,
color: string
}
}
As I am working with TypeScript, I created an interface to use a converter with. My code looks like this so far:
import {
DocumentData,
FirestoreDataConverter,
WithFieldValue,
QueryDocumentSnapshot,
SnapshotOptions,
query,
collection,
where,
} from 'firebase/firestore'
import { store } from '../firebase'
import { useCollectionData } from 'react-firebase-hooks/firestore'
import { User } from 'firebase/auth'
import { useCategories } from './categories'
import { useAuth } from '../contexts/AuthContext'
interface ToDo {
id: string
title: string
involved: User[]
category?: {
title: string
color: string
}
}
const converter: FirestoreDataConverter<ToDo> = {
toFirestore(todo: WithFieldValue<ToDo>): DocumentData {
return {} //not implemented yet
},
fromFirestore(
snapshot: QueryDocumentSnapshot,
options: SnapshotOptions
): ToDo {
const data = snapshot.data(options)
return {
id: snapshot.id,
title: data.title,
category: undefined, //?
involved: [], //?
}
},
}
export function useToDos() {
const { currentUser } = useAuth()
const { categories } = useCategories() //needed in converter
const ref = query(
collection(store, 'habits'),
where('involved', 'array-contains', currentUser.uid)
).withConverter(converter)
const [data] = useCollectionData(ref)
return {
todos: data,
}
}
Is there any way I can do this? I have a Hook that returns all of the user´s categories, but I obviously can´t call that outside the
useToDos-Hook. And creating the const in the hook does not help, either, as it results in an infinite re-render.
I know this is a long one, but does anyone have tips how I could approach this? Thanks in advance ^^
UPDATE:
I had to make two small adjustments to #ErnestoC ´s solution in case anyone is doing something similar:
First, I changed the calls for currentUser.id to currentUser.uid.
Afterwards I got the very missleading Firestore Error: PERMISSION_DENIED: Missing or insufficient permissions, which made me experiment a lot with my security rules. But that is not where the error originated. Debugging the code line by line, I noticed the category objects resolved by the promise where not correct and had a weird path with multiple spaces at the beginning and the end of their ids. When I removed them before saving them in the promises array, it worked. Although I do not see where the spaces came from in the first place.
promises.push(
getDoc(
doc(
store,
'categories',
docSnap.data().involvedCategory[userCatIndex].replaceAll(' ', '')
)
)
)
The general approach, given that Firestore is a NoSQL database that does not support server-side JOINS, is to perform all the data combinations on the client side or in the backend with a Cloud Function.
For your scenario, one approach is to first query the ToDo documents by the array membership of the current user's ID in the involved array.
Afterwards, you fetch the corresponding category document the current user assigned to that ToDo (going by index mapping between the two arrays). Finally, you should be able to construct your ToDo objects with the data.
const toDoArray = [];
const promises = [];
//Querying the ToDo collection
const q = query(collection(firestoreDB, 'habits'), where('involved', 'array-contains', currentUser.id));
const querySnap = await getDocs(q);
querySnap.forEach((docSnap) => {
//Uses index mapping
const userCatIndex = docSnap.data().involved.indexOf(currentUser.id);
//For each matching ToDo, get the corresponding category from the categories collection
promises.push(getDoc(doc(firestoreDB, 'categories', docSnap.data().involvedCategory[userCatIndex])));
//Pushes object to ToDo class/interface
toDoArray.push(new ToDo(docSnap.id, docSnap.data().title, docSnap.data().involved))
});
//Resolves all promises of category documents, then adds the data to the existing ToDo objects.
await Promise.all(promises).then(categoryDocs => {
categoryDocs.forEach((userCategory, i) => {
toDoArray[i].category = userCategory.data();
});
});
console.log(toDoArray);
Using the FirestoreDataConverter interface would not be that different, as you would need to still perform an additional query for the category data, and then add the data to your custom objects. Let me know if this was helpful.

GraphQL: Writing Frontend Query with Optional Arguments

I am currently trying to write a GraphQL Query called recipe which can take a number of optional arguments based on a graphQL input called RecipeSearchInput, and uses the input to find a specific recipe matching the attributes passed.
I am struggling to write the frontend query to be able to be able to take the arguments as an object.
Here's my graphQl schema for graphql input RecipeSearchInput.
input RecipeSearchInput {
_id: ID
title: String
cookTime: Int
prepTime: Int
tools: [String!]
ingredients: [String!]
steps: [String!]
videoURL: String
tags: [String!]
country: String
}
And here's my query written in the frontend to access the my mongodb server through graphql:
// gql query that requests a recipe
export const findOne = obj => {
let requestBody = {
query: `
query {
recipe(recipeInput: ${obj}) {
_id
title
cookTime
prepTime
tools
ingredients
steps
videoURL
tags
country
}
}
`
};
return fetchEndpoint(requestBody);
};
When I wrote my frontend query with a simple object that I knew existed in my database:
const displayData = async () => {
const recipeData = await api.recipe.findOne({
title: "Greek Chicken Skewers"
});
console.log(recipeData);
};
This gives me the following error:
message: "Expected value of type "RecipeSearchInput", found [object, Object]."
The problem I'm seeing is that obj is not formatted properly (in String form) to be received by Graphql as an input. The Graphql query params look like this:
Here is what the working query looks like
What's the best approach for making a query that takes more than one argument? How should I package up the argument in the frontend to please GraphQL?
Thanks in advance, and please let me know if any of this was unclear!
Shawn

React Apollo updating client cache after mutation

I am trying to update my chache after succesfully executing a mutation. Here is my query and mutation:
export const Dojo_QUERY = gql`
query Dojo($id: Int!){
dojo(id: $id){
id,
name,
logoUrl,
location {
id,
city,
country
},
members{
id
},
disziplines{
id,
name
}
}
}`;
export const addDiszipline_MUTATION = gql`
mutation createDisziplin($input:DisziplineInput!,$dojoId:Int!){
createDisziplin(input:$input,dojoId:$dojoId){
disziplin{
name,
id
}
}
}`;
and my mutation call:
const [createDisziplin] = useMutation(Constants.addDiszipline_MUTATION,
{
update(cache, { data: { createDisziplin } }) {
console.log(cache)
const { disziplines } = cache.readQuery({ query: Constants.Dojo_QUERY,variables: {id}});
console.log(disziplines)
cache.writeQuery({
...some update logic (craches in line above)
});
}
}
);
when i execute this mutation i get the error
Invariant Violation: "Can't find field dojo({"id":1}) on object {
"dojo({\"id\":\"1\"})": {
"type": "id",
"generated": false,
"id": "DojoType:1",
"typename": "DojoType"
}
}."
In my client cache i can see
data{data{DojoType {...WITH ALL DATA INSIDE APPART FROM THE NEW DISZIPLINE}}
and
data{data{DisziplineType {THE NEW OBJECT}}
There seems to be a lot of confusion around the client cache around the web. Somehow none of the posed solutions helped, or made any sense to me. Any help would be greatly appreciated.
EDIT 1:
Maybe this can help?
ROOT_QUERY: {…}
"dojo({\"id\":\"1\"})": {…}​​​​​
generated: false​​​​​
id: "DojoType:1"​​​​​
type: "id"​​​​​
typename: "DojoType"​​​​​
<prototype>: Object { … }​​​​
<prototype>: Object { … }
Edit 2
I have taken Herku advice and started using fragment. however it still seems to not quite work.
My udated code:
const [createDisziplin] = useMutation(Constants.addDiszipline_MUTATION,
{
update(cache, { data: { createDisziplin } }) {
console.log(cache)
const { dojo } = cache.readFragment(
{ fragment: Constants.Diszilines_FRAGMENT,
id:"DojoType:"+id.toString()});
console.log(dojo)
}
}
);
with
export const Diszilines_FRAGMENT=gql`
fragment currentDojo on Dojo{
id,
name,
disziplines{
id,
name
}
}
`;
however the result from console.log(dojo) is still undefined.Any advice?
So I think your actual error is that you have to supply the ID as as a string: variables: {id: id.toString()}. You can see that these two lines are different:
dojo({\"id\":1})
dojo({\"id\":\"1\"})
But I would highly suggest to use readFragment instead of readQuery and update the dojo with the ID supplied. This should update the query as well and all other occurrences of the dojo in all your queries. You can find documentation on readFragment here.
Another trick is as well to simply return the whole dojo in the response of the mutation. I would say people should be less afraid of that and not do to much cache updates because cache updates are implicit behaviour of your API that is nowhere in your type system. That the new disziplin can be found in the disziplins field is now encoded in your frontend. Imagine you want to add another step here where new disziplins have to be approved first before they end up in there. If the mutation returns the whole dojo a simple backend change would do the job and your clients don't have to be aware of that behaviour.

With GraphQL + Apollo Client, how to return a totalCount after mutating?

I have the following:
const typeDefs = `
type Request {
id: ID!
email: String!
totalCount: Int
}
type Mutation {
createRequest(email: String!): Request!
}
`;
export default typeDefs;
Right now totalCount is returning null. Using GraphQL, what is the right way to return the TotalCount as the mutation's response. Should totalCount be included somehow in the Request model?
You've defined totalCount as a property of each Request object but totalCount represents the count of all Request objects.
The common practice to allow additional data in mutation response is to have it return a wrapper. For example:
type Mutation {
createRequest(email: String!): CreateRequestResponse!
}
type CreateRequestResponse {
request: Request!
totalCount: Int!
}
The CreateRequestResponse is a wrapper object that can include any arbitrary attributes that the clients might need in the response. totalCount here is just an example; you can add any attribute there.
With that, the definition of Request type would be:
type Request {
id: ID!
email: String!
}
which is ideal because it only contains attributes of a specific Request.

Add filtering option to the graphql paginated query

I am using this nice apollo-universal-starter-kit in one of my projects. I have a task to add a filtering option to this page to filter posts that have more than 2 comments.
The starter kit uses Apollo graphql-server as the back-end. The schema description for the posts looks like this:
# Post
type Post {
id: Int!
title: String!
content: String!
comments: [Comment]
}
# Comment
type Comment {
id: Int!
content: String!
}
# Edges for PostsQuery
type PostEdges {
node: Post
cursor: Int
}
# PageInfo for PostsQuery
type PostPageInfo {
endCursor: Int
hasNextPage: Boolean
}
# Posts relay-style pagination query
type PostsQuery {
totalCount: Int
edges: [PostEdges]
pageInfo: PostPageInfo
}
extend type Query {
# Posts pagination query
postsQuery(limit: Int, after: Int): PostsQuery
# Post
post(id: Int!): Post
}
postsQuery is used to generate a paginated result of the posts
Here is how postsQuery resolves (complete code here)
async postsQuery(obj, { limit, after }, context) {
let edgesArray = [];
let posts = await context.Post.getPostsPagination(limit, after);
posts.map(post => {
edgesArray.push({
cursor: post.id,
node: {
id: post.id,
title: post.title,
content: post.content,
}
});
});
let endCursor = edgesArray.length > 0 ? edgesArray[edgesArray.length - 1].cursor : 0;
let values = await Promise.all([context.Post.getTotal(), context.Post.getNextPageFlag(endCursor)]);
return {
totalCount: values[0].count,
edges: edgesArray,
pageInfo: {
endCursor: endCursor,
hasNextPage: values[1].count > 0
}
};
}
And, here is a graphql query which is used on the front-end with React post_list component (complete code for the component is here)
query getPosts($limit: Int!, $after: ID) {
postsQuery(limit: $limit, after: $after) {
totalCount
edges {
cursor
node {
... PostInfo
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
This was a long introduction :-), sorry
Question:
How can I add filtering option to the post_list component/page? I kind of understand the React side of the question, but I do not understand the graphql one. Should I add a new variable to the postsQuery(limit: $limit, after: $after) so it looks like postsQuery(limit: $limit, after: $after, numberOfComments: $numberOfComments)? And then somehow resolve it on the back-end? Or, I am on the wrong track and should think in the different direction? If so, can you point me to the right direction? :-)
Thank you in advance!
IMO I would definitely solve this on the backend. As far as whether or not it should be a new variable, i personally would say yes, and your Post.getPostsPagination could be updated to support filtering on comment count, ideally you want that as part of the DB request so that you know you are getting N number of the types of posts you want, otherwise it will open up the door for edgecases in your pagination.
The other option is a new query for CuratedPosts, or FilteredPosts, or whatever you want to call it that already knows to filter based on number of comments.

Resources