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.
Related
I'm maintaining a bot for Wikipedia using npm mwbot, but now I'm migrating to mwn. I'm experiencing some strange behaviour of mwn; that is, a POST request fails because of a 414 error.
Let's say you have some list of usernames from a wiki project and you want to check if any of the users is blocked from editing. This can be done by mediawiki action API list=blocks with the "bkusers" parameter specified with a pipe- (or "|"-) separated list of usernames, and bots can query 500 usernames with one API call. The following TypeScript code should do the job, but the relevant query fails because of 414. (Since it's a POST request, I have absolutely no idea why.)
import { mwn } from "mwn";
(async () => {
const mw: mwn = await mwn.init({
apiUrl: 'https://en.wikipedia.org/w/api.php',
username: 'YourBotUsername',
password: 'YourBotPassword',
userAgent: 'myCoolToolName 1.0 ([[link to bot user page or tool documentation]])'
});
interface DynamicObject {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
[key: string]: any
}
interface ApiResponseListLogeventsItem {
logid: number,
ns: number,
title: string,
pageid: number,
logpage: number,
params: DynamicObject,
type: string,
action: string,
user: string,
anon?: boolean,
timestamp: string,
comment: string
}
// eslint-disable-next-line #typescript-eslint/no-empty-interface
interface ApiResponseListLogevents extends Array<ApiResponseListLogeventsItem> {}
// Get 500 (5000 if you're a bot) usernames from the latest logs of account creation
const response = await mw.continuedQuery({
action: 'query',
list: 'logevents',
leprop: 'ids|title|timestamp',
letype: 'newusers',
lelimit: 'max',
}, 1).then(res => res).catch(err => console.log(err.info));
if (!response) return;
const resLgev: ApiResponseListLogevents = response.filter((obj: DynamicObject) => obj && obj.query && obj.query.logevents).map(obj => obj.query && obj.query.logevents).flat();
const users = resLgev.filter(obj => obj.title).map(obj => obj.title.replace(/^User:/, ''));
console.log(users.length);
// Check if any of the users is blocked
const r = await mw.request({ // => Returns 414 'URI Too Long' although this is a POST request
action: 'query',
list: 'blocks',
bklimit: 'max',
bkusers: users.slice(0, 500).join('|'), // Pipe-separated list of users
bkprop: 'user|timestamp|expiry|restrictions|flags',
formatversion: '2'
}, {method: 'POST'}).then(res => res).catch(err => console.log(err));
if (!r || !r.query || !r.query.blocks) return console.log('!');
console.log(r.query.blocks.length);
})();
The first half of the code isn't that important because it's just there to fetch some random usernames from the API. The problem is with mw.request(). An equivalent code with the mwbot framework works so this doesn't seem to be a server-side issue. I would appreciate any comments on what'd be wrong.
I don't know if this is relevant but the issue might come from axios, which mwn uses for HTTP requests. Yesterday I wrote a function to scrape a webpage, and at first used axios. But it constantly spit "Package subpath './lib/defaults' is not defined by 'exports'" error so I uninstalled it and am now using node:https to fetch a website's html. But as I just said above, I don't know if this is relevant in any way.
I have a similar to this question, however I don't see how it could be resolved using this other syntax currently in my Apollo project.
The issue is '0000-00-00' values coming from SQL db to my Apollo GraphQL server, thus throwing errors of invalid DateTime type. I'd like to write a resolver for the datetime field in question, like:
import gqlDate from 'graphql-iso-date';
const MyType = new gql.GraphQLObjectType({
name: 'Type',
description: 'This is a GraphQL type.',
fields: () => {
return {
datetime: {
type: gqlDate.GraphQLDateTime,
description: 'The datetime field of my type.',
resolve: (record) => {
return record === '0000-00-00 00:00:00' ? null : record;
}
}
};
}
});
However I cannot adjust this example to this other syntax my project is setup to:
import { gql } from 'apollo-server'
export const schema = gql`
extend type Query {
users(id: ID): [User!]
}
type User {
id: ID!
first_name: String!
middle_name: String
birthday: String!
}
import graphqlFields from 'graphql-fields'
export const usersResolver = {
Query: {
users: async (_, params, context, info) => {
const requestedColumns = Object.keys(graphqlFields(info))
return context.dataSources.db.getUsers(params, requestedColumns)
},
},
}
I played around with adding something like this to the resolver, but I'm not sure if it's the right thing to do and how exactly to implement it. Since the field resolves to Scalar, my guess is the below code won't be reached.
birthday: (parent, params, context, info) => { ... },
I also tried adding a separate query for the field, but am not sure how to connect it to the original users one.
My final resort is to just query the birthday as String and handle it on the front end, but that seems to be totally inappropriate compared to resolving it in GraphQL.
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.
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.
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.