Graphql filter query result - javascript

I am new using graphql and I would like to improve some features at my API, one of the is get a better filter.
This API should return some recipe base on the ingredients that the user will inform in the respective APP, This is The resolver I am using:
module.exports = {
Recipe: {
async ingredients(recipe, _, { dataSources }) {
return await dataSources.IngredientService.getRecipeIngredients(recipe.id)
},
},
Query: {
recipe: async () => db('Recipe'),
ingredient: async () => db('Ingredient'),
recipeByIngredient:async () => db('Recipe'),
}}
the service
class ingredientService {
async getRecipeIngredients(recipe_id) {
const filteredRecipe = db('Ingredient')
.where({ recipe_id })
.join('Recipe', 'Recipe.id', '=', recipe_id)
.select('Recipe.*', 'Ingredient.*')
.whereIn('Ingredient.name', ["sal", "açucar"])
return await filteredRecipe
}
the query schema
type Query {
recipe(name:[String]): [Recipe]
ingredient:[Ingredients]
recipeByIngredient(ingredients:String):[Ingredients]
}
type Recipe {
id: Int
title: String!
author: String
link: String
category: String
subcategory:String
ingredients:[Ingredients]
}
type Ingredients{
id:Int
name:String
quantity:Float
measure:String
observation:String
}
The filter is working but I would like ot improve 2 things:
When I see the return no the graphql "playground", when there is no value to the ingredient (that is in on different table from recipes), then the ingredient value is "null" and I would like to not even return the recipe.
I could not make the filter work. I reated the query type "recipe(name:[String]): [Recipe]", for example, but I do not know how to filter it from there. It means, I would like to ingredients filter over my query, filtering the result as expected
quer:
recipe(name :["sal", "açucar", "farinha"]){
id
title
author
link
category
subcategory
ingredients{
name
quantity
measure
observation
}
}
but as you can see, the resolver is hardcode, how could I sent the filter to the resolver?
can anyone help me on it?
Thanks a lot.

In general, to handle filtering, I set create a Condition type, named based on context. Here, maybe you'd like to pass a type RecipeCondition, which defines fields to effectively filter or scope the recipes returned, for example, based on whether it has ingredients in your datastore. This assumes you may want to add additional conditions in future (otherwise, could just hardcode condition in your sql).
type RecipeCondition {
ingredientsRequired: Bool
// Add other condition fields here as needed
...
}
type Query {
recipe(name:[String], condition: RecipeCondition): [Recipe]
...
}
I would handle the filter at the top level where you initially fetch recipes with db service (as opposed to handling in ingredients subresolver). You can simply use this condition, accessible on the recipe resolver arg, and pass it to your db service func that initially fetches a recipes array. If the condition ingredientsRequired is true, filter with sql appropriately (will require join to ingredients table and whereIn condition -- if your passing an array of recipe names, this may need to be completed iteratively). In this way, a recipe with no ingredients will not even hit the ingredients subresolver (assuming that condition is desired).

Thank you all that tried to help, all these comments was very important to guide to the final answer.
I got one posible solution, that I would like to share and get your feedback, if posible.
On first, I improved my Query resolver
Query: {
recipe(obj, {name}, {dataSources}, info) {
if (name) {
return dataSources.IngredientService.getIngredientsByName(name)
} else {
return db('Recipe')
}
}
Second, I changed my Service to receive the filter
async getIngredientsByName(name) {
const filteredRecipe = db('Ingredient')
//.where({ recipe_id })
.join('Recipe', 'Recipe.id', '=', 'Ingredient.recipe_id')
.select('Recipe.*', 'Ingredient.name', 'Ingredient.quantity', 'Ingredient.measure','Ingredient.observation')
.whereIn('Ingredient.name', name)
return await filteredRecipe
now is everything working fine and making the filter as expected.
Once again, thanks a lot, all of you.

Related

Conditional where condition in GraphQL Query

i have the following query:
query MyExampleQuery($templateQuestionId: Int!, $assetId: Int) {
report_answer(
where: {
report_template_question: { id: { _eq: $templateQuestionId } },
asset_id: { _eq: $assetId } }
) {
id
}
}
and i want the asset_id condition of the where only apply conditionally. When $assetId is provided it should be a condition, otherwise it should not apply at all.
How can this be achieved with GraphQL?
Yes but not in the way you have described above.
You have:
query MyExampleQuery($templateQuestionId: Int!, $assetId: Int)
Which says that $templateQuestionId is required but $assetId is optional.
When you write your resolver for this query you can take the existence (or absence) of assetId into account when you execute an actual database query.
But the GraphQL language itself does not have the concept of a where clause. Some GraphQL APIs include a where in their input objects but it's not part of the language.

Does GQL support spreading / composing objects (eg. input args)?

I did not find anything about it in the Docs, but for me it seems to be a very common task, so is there a possibility/syntax or a workaround to achieve the following?
My GraphQL query looks something like this:
query customerPersons(
$pagination: PaginationInput!
$filter: FilterPersonInput
) {
persons(pagination: $pagination, filter: $filter) {
totalCount
data {
...PersonsData
}
}
}
With apollo and its codegen I get some helper React Hook function calls, which I can use like this:
const { data } = useCustomerPersonsQuery({
variables: {
pagination: {
skip: 0,
take: 20,
},
filter: {
query: 'peterchen',
userType: 'Customer'
}
});
Is it possible to move the userType: 'Customer' part into the static part of my query? If it would be JavaScript I would say it is assign or merge two objects with the spread operator.
Something like:
query customerPersons(
$pagination: PaginationInput!
$filter: FilterPersonInput
) {
persons(pagination: $pagination, filter: { ...$filter, userType: "Customer" }) {
totalCount
data {
...PersonsData
}
}
}
No, GraphQL does not support such syntax in query documents. The closest you could achieve would be creating separate variables for all the fields in FilterPersonInput other than userType.
The two solutions available are:
create a separate resolver in your backend that includes this filtering, providing a customers field in the schema next to the persons one. (If you're using unions/interface, this might be an opportunity to choose a more concrete result type as well).
create another React hook as a wrapper for the auto-generated one, and add the property to the variable value in there

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.

Cannot set value to object property in Typescipt. Cannot set property 'ismain' of undefined

bellow the class i initialize an object of type Photo, which photo is an interface with some attributes. And then i'm trying to use array filter to filter photos.
The filter return a copy of the photos array but i specify which photo do i want so i will have an array with 1 index inside. And then, i try to set a property inside the object to false, and the "Cannot set property 'ismain' of undefined in angular" occurs. Any ideas why?
Here is my code.
setMainPhoto(photo: Photo) {
this.userService
.setMainPhoto(this.authService.decodedToken.nameid, photo.id)
.subscribe(
() => {
// We use the array filter method to filter the photos apart from the main photo
// Filter returns a copy of the photos array. Filters out anything that it doesn`t match in the p
this.currentMain = this.photos.filter(p => p.ismain === true)[0];
this.currentMain.ismain = false;
photo.ismain = true;
this.alertify.success('Successfully set to profile picture');
},
error => {
this.alertify.error('Photo could not be set as profile picture');
}
);
}
In the above method the error occurs.
And below is my Photo interface.
export interface Photo {
id: number;
url: string;
dateadded: Date;
ismain: boolean;
description: string;
}
ERROR
UPDATE
PROBLEM SOLVED
The response from the server needs to match with the object attributes. So i had to change the property interface to match the JSON object coming back from the server.
Try this
setMainPhoto(photo: Photo) {
this.userService
.setMainPhoto(this.authService.decodedToken.nameid, photo.id)
.subscribe(
() => {
// We use the array filter method to filter the photos apart from the main photo
// Filter returns a copy of the photos array. Filters out anything that it doesn`t match in the p
this.currentMain = this.photos.filter(p => p.ismain === true)[0];
if (this.currentMain) {
this.currentMain.ismain = false;
photo.ismain = true;
this.alertify.success('Successfully set to profile picture'); }
},
error => {
this.alertify.error('Photo could not be set as profile picture');
}
);
}
If this works, then the filter does not return anything.
Interesting, I've had this come up before too. Typescript is quite strict with capitalization and if you don't match the json object exactly, it won't work, create a new property, etc.
I've found it helpful to take a look at the json return before building my typescript model, to make sure I'm matching the object being sent.

Dispatch Redux action after React Apollo query returns

I'm using React Apollo to query all records in my datastore so I can create choices within a search filter.
The important database model I'm using is Report.
A Report has doorType, doorWidth, glass and manufacturer fields.
Currently when the query responds, I'm passing allReports to multiple dumb components which go through the array and just get the unique items to make a selectable list, like so..
const uniqueItems = []
items.map(i => {
const current = i[itemType]
if (typeof current === 'object') {
if (uniqueItems.filter(o => o.id !== current.id)) {
return uniqueItems.push(current)
}
} else if (!uniqueItems.includes(current)) {
return uniqueItems.push(current)
}
return
})
Obviously this code isn't pretty and it's a bit overkill.
I'd like to dispatch an action when the query returns within my SidebarFilter components. Here is the query...
const withData = graphql(REPORT_FILTER_QUERY, {
options: ({ isPublished }) => ({
variables: { isPublished }
})
})
const mapStateToProps = ({
reportFilter: { isPublished }
// filterOptions: { doorWidths }
}) => ({
isAssessment
// doorWidths
})
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
resetFilter,
saveFilter,
setDoorWidths,
handleDoorWidthSelect
},
dispatch
)
export default compose(connect(mapStateToProps, mapDispatchToProps), withData)(
Filter
)
The Redux action setDoorWidths basically does the code above in the SidebarFilter component but it's kept in the store so I don't need to re-run the query should the user come back to the page.
It's very rare the data will update and the sidebar needs to change.
Hopefully there is a solution using the props argument to the graphql function. I feel like the data could be taken from ownProps and then an action could be dispatched here but the data could error or be loading, and that would break rendering.
Edit:
Query:
query ($isPublished: Boolean!){
allReports(filter:{
isPublished: $isPublished
}) {
id
oldId
dbrw
core
manufacturer {
id
name
}
doorWidth
doorType
glass
testBy
testDate
testId
isAssessment
file {
url
}
}
}
While this answer addresses the specific issue of the question, the more general question -- where to dispatch a Redux action based on the result of a query -- remains unclear. There does not, as yet, seem to be a best practice here.
It seems to me that, since Apollo already caches the query results in your store for you (or a separate store, if you didn't integrate them), it would be redundant to dispatch an action that would also just store the data in your store.
If I understood your question correctly, your intent is to filter the incoming data only once and then send the result down as a prop to the component's stateless children. You were on the right track with using the props property in the graphql HOC's config. Why not just do something like this:
const mapDataToProps = ({ data = {} }) => {
const items = data
const uniqueItems = []
// insert your logic for filtering the data here
return { uniqueItems } // or whatever you want the prop to be called
}
const withData = graphql(REPORT_FILTER_QUERY, {
options: ({ isPublished }) => ({
variables: { isPublished }
}),
props: mapDataToProps,
})
The above may need to be modified depending on what the structure of data actually looks like. data has some handy props on it that can let you check for whether the query is loading (data.loading) or has errors (data.error). The above example already guards against sending an undefined prop down to your children, but you could easily incorporate those properties into your logic if you so desired.

Categories

Resources