Add filtering option to the graphql paginated query - reactjs

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.

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.

Hotchocolate Strawberryshake how to use a mutation?

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.. ?

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

AWS AppSync - Implement many to many connections using 1-M #connections and a joining #model

Following AWS documentation (https://aws-amplify.github.io/docs/cli-toolchain/graphql > Many-To-Many Connections), I try to understand the workaround example they provide for many to many connections (which seems not supported yet by Amplify).
The schema is:
type Post #model {
id: ID!
title: String!
editors: [PostEditor] #connection(name: "PostEditors")
}
# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
type PostEditor #model(queries: null) {
id: ID!
post: Post! #connection(name: "PostEditors")
editor: User! #connection(name: "UserEditors")
}
type User #model {
id: ID!
username: String!
posts: [PostEditor] #connection(name: "UserEditors")
}
Using AWS AppSync Console, so far I'm able to:
Create a user using this mutation:
mutation {
createUser(input:{
username: "theUserName"
}){
username
}
}
Create a post using this mutation:
mutation {
createPost(input: {
title: "second post"
}){
title
}
}
But I don't understand how to add multiple editors to a post? So far, I'm able to add editors to a post using PostEditor join, but in their example, there is this statement (which I don't understand very well), so I don't think this is the good approach:
# Create a join model and disable queries as you don't need them
# and can query through Post.editors and User.posts
So I guess that using this join model to perform mutation is not what I have to do. Nevertheless, to be able to create this relation between a post and an editor, I created a mutation (retrieving "postEditorPostId" and "postEditorEditorId" from both previous mutations):
mutation {
createPostEditor(input:{
postEditorPostId: "XXX-XXX-XXX"
postEditorEditorId: "YYY-YYY-YYY"
}){
post {
title
}
editor {
username
posts {
items {
post {
title
}
}
}
}
}
}
Do I need to perform this previous mutation everytime I add a new editor (so the mutation will remain the same but "postEditorEditorId" will change? it seems obviously not a scalable approach, if for example the UI allows an admin to add 50 or more new editors (so it will need 50 mutations).
Finally I can get the information I need using this query:
query{
getUser(id: "YYY-YYY-YYY"){
username
posts {
items {
post {
title
}
}
}
}
}
Is there a better way (I suppose) to add editors to a post?
edit:
Using a promise, I am able to add multiple editors to a post, but it involves to execute as mutation as mutations as there are users:
const users = [{id: "U1", username: "user1"}, {id: "U2", username: "user2"}];
const post = { id: "P1", title: "Post 1" };
/*
After creating two users and a post using the approriate mutations
Using the CreatePost join below to make user1 and user2 editor on Post 1
*/
function graphqlCreatePostEditor(editorID) {
return new Promise((resolve, reject) => {
resolve(
API.graphql(graphqlOperation(createPostEditor, {
input: {
postID: post.id,
}
}))
)
})
}
let promises = users.map(user=> {
return graphqlCreatePostEditor(user.id)
.then(e => {
console.log(e)
return e;
})
});
Promise.all(promises)
.then(results => {
console.log(results)
})
.catch(e => {
console.error(e);
})
Still looking if there is a way to pass an array in a sigle mutation.
For simplicity sake, I'm lets go with a User model and a Project model where a user can have many projects and belong to many projects.
Note: The creation of join table as I've described it here is for the Amplify JS API for React / React Native / JavaScript
User model
type User #model {
id: ID!
username: String!
projects: [UserProject] #connection(name: "UserProject")
}
Project model
type Project #model {
id: ID!
project_title: String!
users: [UserProject] #connection(name: "ProjectUser")
}
Join table
type UserProject #model {
id: ID!
user: User #connection(name: "UserProject")
project: Project #connection(name: "ProjectUser")
}
Creation of Join table
Prerequisite: Fetch both user.id and project.id however you want to do that.
const UserProjectDetails = {
userProjectUserId: user.id
userProjectProjectId: project.id
};
API.graphql({ query: mutations.createUserProject, variables: {input: UserProjectDetails}})
And there you have it.
This article on dev.to was also pretty straight to the point:
https://dev.to/norrischebl/modeling-relationships-join-table-graphql-aws-amplify-appsync-1n5f

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.

Resources