How to sustain potentially infinite Graphql queries? - graphql

There's one thing I have never understood about Graphql. I think I'll paint by problem by giving some examples.
Let's say I have an Account type and a Chat type. An Account can have multiple Chats associated with it, but a Chat can only be associated with a single Account. The schema might look something like this:
type Account {
id: String!
username: String!
chats: [Chat!]!
}
type Chat {
id: String!
text: String!
account: Account!
}
Let's say I am exposing the following queries:
type Query {
account(id: String!): Account
accounts: [Account!]!
}
Now, I will query my schema:
accounts {
id
username
chats {
id
text
}
}
This is a pretty straigthforward query - I simply what to grab all the chats by all the accounts.
But what if I have a deeper query like this:
accounts {
id
username
chats {
id
text
account {
id
username
chats {
id
text
}
}
}
}
I know that in reality, querying this data would be ridiculous, but still, it should be possible, right?
Now let's switch our view from the consumer of the API to the actual developer of the API.
How is the graphql developer meant to sustain these, potentially, infinite queries? Does Graphql resolve these automatically (it doesn't seem to)? Or am I just missing something out?
Thanks for any help.

How is the graphql developer meant to sustain these, potentially, infinite queries?
Not possible in having "infinite" nested queries.
GraphQL schema should be acyclic - meaning the relationships between types should be unidirectional.
Just a rule that the schema should be based on the query patterns in the client application.
So having a nested account within the chat would simply mean the use case and sequence are not properly defined.

Related

Apollo GraphQL cache for common data in different queries?

I have a web based application with two graphql queries that have some data in common. The first query FullProject is more or less a very broad "lets pull all data that the client might need" and contains many nested resources. For this question the important thing is that it also pulls in loads of users:
query FullProject($id: ID!) {
projects(input: {filter: {id: $id}}) {
nodes {
id
name
relatedUsers {
id
name
}
# Many more
}
}
}
The second query is used to populate a list of users:
query NameUser($id: ID!) {
users(input: {filter: {id: $id}}) {
nodes {
id
name
}
}
}
When I check the GraphQL cache (using the Apollo Developer tools) after running FullProject I can see that the data has been properly normalized and I have entries like:
User:1
name:A
---
User:2
name:B
When I however run the NameUser query this always results in one new request for each user. After the first request for a user the cache properly kicks in, but this still means that I am ending up with possibly hundreds of queries for data that is technically already part of the cache (albeit via a different query). I was hoping that the Apollo Client would be able to leverage the cache even for different top-level queries. Am I doing something wrong or is my assumption incorrect?

Conceptual Question: Shared GraphQL schema for multiple endpoints (client/admin)

Context
I am using a NX Workspace to organize two different angular frontends (client & admin). To separate client and admin logic, two different NestJS backend services including GraphQL are used for client and admin.
As both services fetch data from a single MongoDB a single database library is used for both frontends.
Both backend services currently use a single GraphQL Schema generated through schema-first approach and a single database layer. In most cases the types and fields definition matches between client and admin, but in some cases a single service requires additional query arguments or fields.
For example, the admin service depends on the fields confirmed or banned of type User while they shouldn't be available through the client service.
Furthermore, e.g. the getUsers query should not be exposed through the client service.
type User {
_id: ID
name: String
email: String
confirmed: Boolean
banned: Boolean
}
type Query {
getUserById(userId: String): User
getUsers(): [User]
}
Question
Are there any best practices how to proceed with the GraphQL Schema(s) in such a case as the types are almost similar.
You can use schema directives to define authorization rules in a declarative manner directly in your Graphql schema.
A common approach would be to assign roles to a user and then use these roles to allow/block access to certain mutations or queries.
So for your example, I would imagine any request coming from the client would be made by a user with a role of client and any request coming from admin would have a user role of admin
So to build on your example of limiting the getUsers query to just admins we could add this directive to our schema:
type User {
_id: ID
name: String
email: String
confirmed: Boolean
banned: Boolean
}
type Query {
getUserById(userId: String): User
getUsers(): [User] #hasRole(roles: [admin])
}
You can read more about how to actually implement the custom directive hasRoles in the nestJs docs https://docs.nestjs.com/graphql/directives

Nested mutation GraphQL

I'm using AWS Appsync and Amplify.
A snippet of my GraphQL schema look like this:
type Recipe
#model
#auth(rules: [{allow: owner}])
{
id: ID!
title: String!
key: String!
courses: [Course!]!
}
type Course
#model
#auth(rules: [{allow: owner}])
{
id: ID!
name: String!
}
On amplify push it creates the DynamoDB tables Recipe and Course
After reading many tutorials I still don't get it how to add a recipe in GraphiQL.
How can i insert a new Recipe that has a reference to a course and avoid duplicates in the Courses table?
To create multiples Recipe referencing the same Course without duplicates in the Course table, you need to design a many-to-many relationship.
So far the relationship you have designed is not enough for AppSync to understand, you are missing #connection attributes. You can read this answer on github to have an explanation of how to design this many-to-many relation in AppSync
After designing the relation, you will use a mutation to insert data, and it's likely that AppSync will generate the mutation code for you (if not, use amplify codegen in the console). You will then be able to create data.
Since you use DynamoDB with multiple tables (default mode for amplify / AppSync), you will have to either :
Call multiple mutations in a row
Use a custom resolver, as described in this SO answer

How to query Apollo GraphQL server with a specific context?

I am writing an Apollo GraphQL API that returns product information from various brands. A simplified version of the schema looks like this:
type Query {
products: [Product]!
}
type Product {
name: String!
brand: String!
}
I want to be able to query products from a specific brand. Normally this would be simple to achieve by adding a brand argument to the Product object:
type Query {
products(brand: String!): [Product]!
}
However, I have multiple GraphQL clients in different apps and each is associated with a specific brand so it seems redundant to always pass the same brand argument in every query. I also have many other objects in my schema (orders, transactions, etc.) that are specific to a brand and would require a brand argument.
Furthermore, my resolvers need to query a different API depending on the brand so even objects in my schema such as User, which are conceptually unrelated to a brand, would potentially need a brand argument so that the resolver knows which API to fetch from.
Is there a way to set the brand context for each client and have this context received by the server? Or maybe there is a better way to achieve this brand separation?
I would probably make Brand be a first-class type in your GraphQL query. That doesn't save you from having to qualify many of the queries you describe by a specific brand, but it at least gives you a common place to start from. Then you'd wind up with an API somewhat like:
type Query {
brand(name: String!): Brand
allProducts: [Product!]!
}
type Brand {
name: String!
products: [Product!]!
# users: [User!]!
}
type Product {
name: String!
brand: Brand! # typical, but not important to your question
}
If the differences between kinds of brands are visible at the API layer, you also could consider using a GraphQL interface to describe the set of fields that all brands have, but actually return a more specific type from the resolver.
The way you describe your application, it could also make sense to run one copy of the service for each brand, each with a different GraphQL endpoint. That would let you straightforwardly parameterize the per-brand internal object configuration and make the "current brand" be process-global context. The big constraints here are that, at a GraphQL level, one brand's objects can never refer to another, and if you have a lot of brands, you need some good way to run a lot of servers.

GraphQL batching with logic

I have the following situation in GraphQL schema:
type User {
id: Float
name: String
cityId: Float
}
type City {
id: Float
country: String
}
On client I need information about User and City. But in order to load City I have to know its id, so I can't just batch these requests. Is it possible to make batch request with logic, so from client I make one request with two queries and maybe add some addition info that when the first request is done take id from it and then make another request.
After that both User and City go to client. It is so to say inner join, so I would like to have one request to load connected data.
I can't change the schema, but I can add libs and so on to client or server.
Thanks.
PS. sorry i have just noticed that you stated that you cannot change
the schema. I will leave it there for future reference, but it cannot
be applied on your problem probably.
i would suggest to rearrange your schema as follows. I do not have enough information if it would satisfy your needs. But i would suggest this.
type User {
id: Float
name: String
city: City
#for this city field there will be additional resolver binded to data loader
}
type City {
id: Float
country: String
}
This way the query will look like as follows
query getUsers {
users {
id
name
city {
id
country
}
}
}
There can be used UserConnection from Relay spec, but let's keep it simple for now.
On server side you will then need to implement two resolvers ... first is request for user list and then resolver for city field. Please note that resolvers have seperate context. In order to avoid N+1 requests problem and batch requests city requests to 1. It would be useful to implement data loader for cities to reduce the number of requests to database. The simple schema would be
User resolver, fetch users and return them in users resolver. CityId
is part of the payload for each use
Because city is second level of selection set you will receive each user in the first argument in the resolver function. You will use countryId to pass it to Countries data loader
Countries data loader will batch requests for each counry together. Data loader will transform the countryIds into country values and return them for each user.
GraphQL server will resolve the whole query and each city will be assigned to each user
This is the best approach that i know to deal with this and will also leads you to better architecture of your schema as you will leverage normalization of your appollo store and in my opinion it is easier to work with this format of the data on frontend as well. I hope that i did not miss something in your post and this information will be useful for you. The whole point is to just nest the country into the user, which leads to N+1 request problem and reduce the performance issue of N+1 problem with data loaders.

Resources