Deep merging fragments - graphql

I'm using a GraphQL API (which I do not own) to access data. I make extensive use of fragments and one problem I've run into more than once is that fragments don't seem to deep-merge. Consider the following query:
fragment BasicInfo on Person {
id
name
address {
city
country
}
}
query ProfileCard($id: ID) {
personById(id: $id) {
...BasicInfo
id
age
address {
streetName
streetNumber
}
}
}
Here we run a basic query to get some information from a profile card including the person's age and some of their address (street name and number). Another component used by the profile card also wants some info which can be found in the BasicInfo fragment. This includes their name as well as their city and country.
Running this query returns an object that contains the following fields: id, name, age, address.streetName and address.streetNumber.
address.city and address.country are both missing - it appears that the query did not deep-merge the fragment in and only inserted it at a shallow level.
Is it possible to force my fragments to deep-merge? Is this even the expected behavior? Do I have to get in contact with the API owners to correct this?
I've had trouble finding documentation that says it should be one way or the other.

I have just run into a similar issue using #apollo/client, and funny enough it's also related to an address model. My second fragment seems to be completely disregarded and not merged. I wrote up a foobar code sample below:
type Request = {
id: string
stops: Array<Stop>
}
type Stop = {
id: string;
address: Address;
}
type Address = {
id: string;
address1: string;
name: string;
}
const ROOT_FRAGMENT = gql`
fragment foo_Request on Request {
id
stops {
...bar_Stop
...qux_Stop
}
${STOP_FRAGMENT_1}
${STOP_FRAGMENT_2}
}
`;
const STOP_FRAGMENT_1 = gql`
fragment bar_Stop on Stop {
id
address {
id
address1
}
}
}
`;
const STOP_FRAGMENT_2 = gql`
fragment qux_Stop on Stop {
id
address {
id
name
}
}
}
`;
/*
expected:
{
id: "request-1"
stops: [
{
id: "stop-1",
address: {
id: "address-1",
address1: "123 my way",
name: "Home",
},
},
],
}
actual:
{
id: "request-1"
stops: [
{
id: "stop-1",
address: {
id: "address-1",
address1: "123 my way",
},
},
],
}
*/

Try using alias instead. Something like
fragment BasicInfo on Person {
id
name
cityCountryAddress: address {
city
country
}
}

Related

GraphQL query how to pass variables

Hi all I have a query where I am trying to get messages from a user with a specific uuid or a role that matches the users role. I am unsure of how to properly use the _ilike or % in this instance. I've tried a myriad of combinations yet the role messages never get returned. My query as it sits now and the hook used in the component are below.
I appreciate any feedback.
Here is my query
query getUserMessages($userId: String!) {
messageReceivers(
where: { _or: [{ userId: { _eq: $userId } }, { message: { roles: { _ilike: "%" } } }] }
) {
messageId
userId
message {
id
audioLink
body
videoLink
user {
firstName
lastName
photo
title
specialty
profession
location
}
}
}
}
Using the lazyquery hook in component
const [getUserMessages, { error, called, loading, data }] = useGetUserMessagesLazyQuery()
const userRole = `%${user.role}%`
useEffect(() => {
getUserMessages({
variables: { userId: user?.id, message: { roles: { _ilike: userRole } } },
})
}, [user])
You are incorrectly passing userRole to the query. To fix it, apply userId's pattern to userRole.
In the query definition, add $userRole in the operation signature (You are currently hardcoding _ilike to % in the query, but you want set it dynamically as $userRole).
In the calling function, send the variables correctly variables: { userId: user?.id, userRole: userRole}.
The GraphQL Variable docs neatly describe how this fits together.
Thanks #fedonev! Though I didn't see your solution you were absolutely correct. I was able to work it out a little differently and I hope this helps someone who's run into the same issue.
By creating the variable $role in the query I was able to use the same syntax as was being used by the userId variable. If anyone has this issue please feel free to comment I will happily help if I can.
Query
query getUserMessages($userId: String!, $role: String = "%") {
messages(
where: {
_or: [{ roles: { _ilike: $role } }, { messageReceivers: { userId: { _eq: $userId } } }]
}
order_by: { createdAt: desc }
) {
createdAt
id
body
audioLink
videoLink
roles
}
Call from in component
useEffect(() => {
getUserMessages({
variables: { userId: user?.id, role: user?.role },
})
}, [user])

Is it possible to `readFragment` from apollo cache if it has no id?

I have a nested component in my app.
At the top of the page, I have a query like
const REPOSITORY_PAGE_QUERY = gql`
query RepositoryPageQuery($name: String!, $owner: String!) {
repository(name: $name, owner: $owner) {
...RepositoryDetailsFragment
}
}
${REPOSITORY_DETAILS_FRAGMENT}
`;
RepositoryDetailsFragment then includes
// list of branches
refs(first: 2, refPrefix: "refs/heads/") {
...BranchesFragment
}
and finally
fragment BranchesFragment on RefConnection {
totalCount
pageInfo {
...PageInfoFragment
}
edges {
node {
id
name
}
}
}
${PAGE_INFO_FRAGMENT}
Obviously, I am not happy, because I need to pass BranchesFragment info around 3 levels deep.
Instead, it would be great if I could read it from the cache directly in my BranchesList component.
I tried to use
client.cache.readFragment({
fragment: BRANCHES_FRAGMENT,
fragmentName: "BranchesFragment"
});
But the problem is that this fragment does not have any id. Is there any way to deal with it and get the fragment info?
Alright, I suddenly came to the solution. Maybe it could be useful for others.
Imagine we have a hierarchy of query -> fragments and components -> subcomponents like this:
RootPageComponent
query
query RepositoryPageQuery(
$name: String!
$owner: String!
$count: Int!
$branchSearchStr: String!
) {
repository(name: $name, owner: $owner) {
...RepositoryDetailsFragment
}
}
${REPOSITORY_DETAILS_FRAGMENT}
component returns the following
<RepositoryDetails repository={data.repository} />
RepositoryDetails
Has a fragment
fragment RepositoryDetailsFragment on Repository {
name
descriptionHTML
defaultBranchRef {
id
name
}
# the branches repository has
refs(first: $count, refPrefix: "refs/heads/", query: $branchSearchStr) {
...BranchesFragment
}
}
${BRANCHES_FRAGMENT}
and returns <BranchesList /> component.
So, instead of passing branch.info from RootPage to RepositoryDetails and then to BranchesList;
You can do the following in BranchesList
const client = useApolloClient();
client.cache.readFragment({
fragment: BRANCHES_FRAGMENT,
fragmentName: "BranchesFragment",
id: "RefConnection:{}" // note this {} - apollow cache adds it when no id is present for the object
})
IMPORTANT!
Make sure to also update type policy for the field and set keyArgs to []
So in this particular case:
RefConnection: {
keyFields: []
...
}
This will give the same result, but you won't have to pass props to nested components and instead can read from cache directly (just like one would do using redux)

Apollo - Updating cache when some fields in some results are missing

For the following query, in some objects in the results array, some of the requested fields might not be present in the response (for example photo or address), which causes the data of my useQuery to be undefined (without any error or warning).
people(xyz: { q: $q, offset: $offset, rows: $rows }) {
results {
uri <--- this is a field of type ID!
name
photo
address {
city
country
}
}
}
My fix is to specifically check if the field exists in the incoming data and provide a fallback value, i.e.: pass a type policy for Person to be {keyFields: false} and do this in the merge function:
newItem = {...item};
newItem.photo = item.photo ?? null;
newItem.address = item.address ?? {city: "", country: ""};
Is the reason for having to do this that there's no id field in the Person type (instead, uri is of type ID!)?
Can I handle this in a better way?
Found a better way on Apollo GraphQL's GitHub.
I'd still appreciate a solution where I don't have to go over each type's nullable field in turn, if there is one.
function nullable() {
// Create a generic field policy that allows any field to be null by default:
return {
read(existing = null) {
return existing;
},
};
}
new InMemoryCache({
typePolicies: {
Person: {
fields: {
photo: nullable(),
address: nullable(),
},
},
Address: { // If there's the case of either city or country missing
fields: {
city: nullable(),
country: nullable(),
}
}
},
})

How to nest qraphql resolvers so as to group them

I'm wrapping a series of REST APIs in GraphQL and I'm having trouble getting a resolver to work.
My typeDefs
const typeDefs = `
type Page {
id: String
path: String!
title: String!
heading: String
type: String
html: String
summary: String
}
type Site {
page(path: String!): Page
}
type Query {
site: Site
}`
and my resolvers
const resolvers = {
Query: {
site: {
page: async (_root, { path }) => getPage(path)
}
}
}
If I try this query
{
site {
page(path: "/services/research-request") {
id
path
title
html
type
summary
}
}
}
I get back
{
"data": {
"site": null
}
}
However if I don't nest the page within the site
const typeDefs = `
type Page {
id: String
path: String!
title: String!
heading: String
type: String
html: String
summary: String
}
type Query {
page(path: String!): Page
}`
and use resolvers
const resolvers = {
Query: {
page: async (_root, { path }) => getPage(path)
}
}
then I try this query
{
page(path: "/services/research-request") {
id
path
title
html
type
summary
}
}
I get back, correctly,
{
"data": {
"page": {
"id": "d290f1ee-6c54-4b01-90e6-d701748f0851",
"path": "/services/research-request",
"title": "Research | Requests",
"html": "<p>This is a really well put together Research Requests page<p>\n",
"type": "page",
"summary": "A short piece of summary text"
}
}
}
I'm going to be pulling in data from a number of APIs to populate my graph so wanted to group the various parts according to site vs user, etc but I'm missing something obvious when trying to nest the resolvers. What am I doing wrong?
No 'free' nesting in graphql ... nest > next level depth > next TYPE ... site resolver, site.page resolver ... but also own site id (for client cache), rebuild/adjust all clients code etc. ...
Namespacing should be better: sitePage, siteOptions, userProfile, userSettings ... use alias (or replace in files) for renaming in client.

Any reason I am getting back the query name in the GraphQL results?

Using the makeExecutableSchema with the following Query definition:
# Interface for simple presence in front-end.
type AccountType {
email: Email!
firstName: String!
lastName: String!
}
# The Root Query
type Query {
# Get's the account per ID or with an authToken.
getAccount(
email: Email
) : AccountType!
}
schema {
query: Query
}
And the following resolver:
export default {
Query: {
async getAccount(_, {email}, { authToken }) {
/**
* Authentication
*/
//const user = security.requireAuth(authToken)
/**
* Resolution
*/
const account = await accounts.find({email})
if (account.length !== 1) {
throw new GraphQLError('No account was found with the given email.', GraphQLError.codes.GRAPHQL_NOT_FOUND)
}
return account
}
}
}
When I query with:
query {
getAccount(email: "test#testing.com") {
firstName
lastName
}
}
I am getting the following result in GraphiQL:
{
"data": {
"getAccount": {
"firstName": "John",
"lastName": "Doe"
}
}
}
So, any reason I am getting this "getAccount" back in the result?
Because getAccount is not a query name. It's just a regular field on the root query type Query.
And having results on the exact same shape as the query is one of the core design principles of GraphQL:
Screenshot from http://graphql.org/ site
Query name in GraphQL goes after query keyword:
query myQueryName {
getAccount(email: "test#testing.com") {
firstName
lastName
}
}

Resources