Apollo mixes two different arrays of the same query seemingly at random - graphql

With a schema like
schema {
query: QueryRoot
}
scalar MyBigUint
type Order {
id: Int!
data: OrderCommons!
kind: OrderType!
}
type OrderBook {
bids(limit: Int): [Order!]!
asks(limit: Int): [Order!]!
}
type OrderCommons {
quantity: Int!
price: MyBigUint! // where it doesn't matter whether it's MyBigUint or a simple Int - the issue occurs anyways
}
enum OrderType {
BUY
SELL
}
type QueryRoot {
orderbook: OrderBook!
}
And a query query { orderbook { bids { data { price } }, asks { data { price } } } }
In a graphql playground of my graphql API (and on the network level of my Apollo app too) I receive a result like
{
"data": {
"orderbook": {
"bids": [
{
"data": {
"price": "127"
}
},
{
"data": {
"price": "74"
}
},
...
],
"asks": [
{
"data": {
"price": "181"
}
},
{
"data": {
"price": "187"
}
},
...
]
}
}
}
where, for the purpose of this question, the bids are ordered in descending order by price like ["127", "74", "73", "72"], etc, and asks are ordered in ascending order, accordingly.
However, in Apollo, after a query is done, I notice that one of the arrays gets seemingly random data.
For the purpose of the question, useQuery react hook is used, but the same happens when I query imperatively from a freshly initialized ApolloClient.
const { data, subscribeToMore, ...rest } = useQuery<OrderbookResponse>(GET_ORDERBOOK_QUERY);
console.log(data?.orderbook?.bids?.map(r => r.data.price));
console.log(data?.orderbook?.asks?.map(r => r.data.price));
Here, corrupted data of Bids gets printed i.e. ['304', '306', '298', '309', '277', '153', '117', '108', '87', '76'] (notice the order being wrong, at the least), whereas Asks data looks just fine. Inspecting the network, I find that Bids are not only properly ordered there, but also have different (correct, from DB) values!
Therefore, it seems something's getting corrupted on the way while Apollo delivers the data.
What could be the issue here I wonder, and where to start debugging such kind of an issue? There seem to be no warnings from Apollo either, it seems to just silently corrupt the data.
I'm clearly doing something wrong, but what?

The issue seems to stem from how Apollo caches data.
My Bids and Asks could have the same numeric IDs but share the same Order graphql type. Apollo rightfully assumes a Bid and an Ask with the same ID are the same things and the resulting data gets wrecked as a consequence.
An easy fix is to show Apollo that there's a complex key to the Order type on cache initialization:
cache: new InMemoryCache({
typePolicies: {
Order: {
keyFields: ['id', 'kind'],
}
}
})
This way it'll understand that the Order entities Ask and Bid with the same ID are different pieces of data indeed.
Note that the field kind should be also added to the query strings accordingly.

Related

Apollo InMemoryCache syntax

I’ve inherited a project that’s setting an inmemorycache with the following key field syntax. None of the examples showcase this particular signature (that I can find at least). All the fields I see in the examples use multiple fields and are placed in the key field attribute. Is this looking for any nested “myField” attributes? How is this expected in the graphql data? (Apollo client 3.2)
const cache = new InMemoryCache({
typePolicies: {
Query: {
/// query info
},
},
UserData: {
fields: {
fieldA: {
merge(existing = [], incoming = []) {
return incoming;
},
},
fieldB: {
merge(existing = [], incoming = []) {
return incoming;
},
},
},
keyFields: [["myField"]], // <-- What is this looking for?
},
},
});
This leads to an invariant violation error:
Uncaught Invariant Violation: Missing field 'myField' while extracting keyFields from {"id":"462a349...... (does not contain myField)
Your code seems fine when it comes to fields map. On the other hand, keyFields in a slightly different question. You could totally skip setting it.
The purpose of keyFields is to uniquely identify your record, so the cache would know how to update. Just like in the relational databases you have a primary key that consists of one or more columns that consider your record unique.
I believe this is well documented in Apollo's documentation, see this:
https://www.apollographql.com/docs/react/caching/cache-configuration/#customizing-cache-ids

How to use Shopify Graphql ProductVariantsBulkInput

I am trying to bulk update the price on multiple product variants at once. I'm just copying the "interactive" example from Shopify's page
All I did was copy and paste the code and put in my own id's. And of course it does not work.
It looks like this:
mutation productVariantsBulkUpdate($variants: [ProductVariantsBulkInput!]!, $productId: ID!) {
productVariantsBulkUpdate(variants: $variants, productId: $productId) {
product {
cursor
}
productVariants {
cursor
}
userErrors {
code
field
message
}
}
}
With Variables like this:
{
"variants": [
{
id: "gid://shopify/ProductVariant/39369514385591",
price: "50.00"
}
],
"productId": "gid://shopify/Product/6591908577463"
}
I'm getting this error:
Variables are invalid JSON: Unexpected token i in JSON at position 30.
It's OK for me. (with some quick tweaks)
I tweaked the request a little since the cursor is not present in the product/variant object, don't know why Shopify has not updated the example in their docs.
mutation productVariantsBulkUpdate($variants: [ProductVariantsBulkInput!]!, $productId: ID!) {
productVariantsBulkUpdate(variants: $variants, productId: $productId) {
product {
id
}
productVariants {
id
price
}
userErrors {
code
field
message
}
}
}
So try to fix the query and remove the cursor object and check if you are using the proper end-point since the bulk operation is available in the unstable version only if I'm not mistaken.
See the image below showing that the response is OK for me.

Unable to filter custom data in siteMetaData in Gatsby using GraphQL in GraphiQL

I've created a basic Gatsby site with the default starter. I'm now trying to add some custom data (the people array) to gatsby-config.json like so:
module.exports = {
siteMetadata: {
title: `Gatsby Default Starter`,
description: `XXX`,
author: `#gatsbyjs`,
people : [
{ id : 1234, name : "Bill Smith", sales : 143, birthdate : "2233-03-22" },
{ id : 5678, name : "Roger Miller", sales : 281, birthdate : "2230-01-06" }
]
},
plugins: [
`gatsby-plugin-react-helmet`,
{ resolve: `gatsby-source-filesystem`,
options: { name: `images`, path: `${__dirname}/src/images`, }
},
`gatsby-transformer-sharp`,
`gatsby-plugin-sharp`,
{ resolve: `gatsby-plugin-manifest`,
options: {
name: `gatsby-starter-default`, short_name: `starter`, start_url: `/`,
background_color: `#663399`, theme_color: `#663399`, display: `minimal-ui`,
icon: `src/images/gatsby-icon.png`
}
}
]
}
Then, in GraphiQL, what I'm trying to do is a query to get a list of people, but limit it to just those with sales above 200, that's my end goal. So first, I did a basic test:
{
site {
siteMetadata {
people {
name
}
}
}
}
That works, I get all people back. Then, I tried:
{
site {
siteMetadata {
people(sales: { gt: 200 }) {
name
}
}
}
}
That gets me an error "Unknown argument sales on field people of type SiteSiteMetadata". That kinda seems to be telling me that Sift underneath Gatsby doesn't have any notion of my custom fields in its schema, which would kind of make sense to me. So, as a test, I try this:
{
site {
siteMetadata(author: { eq: "none" }) {
author
title
}
}
}
My expectation is the query runs successfully but returns an empty result set since the author element's value isn't "none". But instead, I get the same basic error but now telling me "Unknown argument author on field siteMetadata of type Site" and now I'm confused because it seems like it should know about THOSE fields even if it doesn't know about arbitrary ones I add. Then again, maybe that query won't ever work because there's only a single siteMetaData object versus trying to query an array. I'm not sure.
So then I do some research and I see some reference to 'filter', so I try this now:
{
site {
siteMetadata(filter: { eq: "none" }) {
author
title
}
}
}
That gets me "Unknown argument filter on field siteMetadata of type Site."
And now I'm kind of out of ideas.
I did find one post that seemed to possibly imply that you can't query custom data elements like this at all, but some replies seem to imply you, in fact, can (and clearly that first test worked, so the data is found, I just can't get the filtering to work). Maybe I'm using the wrong syntax, but if so then I can't seem to find what the correct syntax looks like (and what's worse is that in the Gatsby docs, the ONE example that MIGHT provide me an answer is error'ing out in the playground and I can't see the code).
It seems like such a simple thing, but I'm at a loss. Any help would be greatly appreciated. Thanks!
EDIT: After I wrote this, I tried putting the data in a separate file that get loaded with the gatsby-transformer-json plugin and tried to query that. The data gets loaded, but I still can't filter the query. I can do:
{
testData {
people {
name
sales
}
}
}
...and that works, returns my data fine. But if I try:
{
testData {
people(sales:{gt:200}) {
name
sales
}
}
}
...or...
{
testData {
people(filter:{sales:{gt:200}}) {
name
sales
}
}
}
...I get the same types of errors. So, I think that at least proves this isn't an issue of querying it from siteMetaData specifically, but I still don't know how to make it do what I want.
For anyone who wants to reproduce this, just add the file data.json in the root of the project with this content:
{
"people" : [
{ "id" : 1234, "name" : "Bill Smith", "sales" : 143, "birthdate" : "2233-03-22" },
{ "id" : 5678, "name" : "Roger Miller", "sales" : 281, "birthdate" : "2230-01-06" }
]
}
Then, add this to the plugins array in gatsby-config.json:
{
resolve: `gatsby-transformer-json`,
options: { typeName: `testData` }
},
{
resolve: `gatsby-source-filesystem`,
options: { name: `data`, path: `${__dirname}/data.json` }
}
No other changes from the initially-generated project are needed. Then, just hop into GraphiQL and try to execute the queries above.
Or, to make things easier, I've created a codesandbox instance that demonstrates this:
https://codesandbox.io/s/gatsby-graphql-querying-json-issue-47km4
EDIT2: I had the thought that maybe this is an issue with GraphiQL itself. So, I created a simple component:
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
const Test = () => {
const testData = useStaticQuery(graphql`
query TestDateQuery {
testData(filter: {sales: {gte:200}}) {
people {
name
}
}
}
`)
console.log("testData", testData);
return (
<div />
)
}
export default Test
I then dropped that into my main Layout component. I get the same sort of error (about filter being an unknown argument) rather than seeing my data. If I remove the filter, I DO see ALL the data. So, again, I can't figure out why just filter isn't working, but that's what I've narrowed it down to.

Is it possible for vue-apollo to return different results from the Playground?

I have a GraphQL query called myAccounts which returns an array of accounts. When I go to the Playground and call the query:
{
accounts {
email
}
}
I get this result:
"data": {
"accounts": [
{
"email": "zach#email-one.com",
},
{
"email": "zach#email-two.com",
}
]
}
However, when I am in my Component, vue-apollo returns two items in the array, but seems to overwrite the second item with the first. Here is the query (in MyAccounts.gql):
query myAccounts {
accounts: myAccounts {
email
}
}
and here is the Apollo query in the component:
import MY_ACCOUNTS_QUERY from '~/apollo/queries/MyAccounts'
...
apollo: {
accounts: {
query: MY_ACCOUNTS_QUERY,
result(data) {
console.log(JSON.stringify(data))
}
}
}
and here is what vue-apollo logs out through the result:
{
"data":{
"accounts":[
{
"email":"zach#email-one.com",
"__typename":"Account"
},
{
"email":"zach#email-one.com",
"__typename":"Account"
}
]
},
"loading":false,
"networkStatus":7,
"stale":false
}
Expected behavior
I would expect the data returned in the Playground to be identical to what vue-apollo is fetching.
Versions
vue: 2.6.10
vue-apollo: #nuxtjs/apollo: 4.0.0-rc18
Additional context
I thought the result hook would be the best way to debug, but any other suggestions gladly welcomed. I assumed that this was a bug in our code, but I cannot figure out what could be causing the repetition (and mismatch).
Apollo normalizes its cache based on the __typename and the id (or _id) field. You need to include an id or _id field in your selection set alongside email. Failing to do so results in both objects being assigned the same key. If you don't have an id field to request, you'll need to provide a custom dataIdFromObject function as shown here.
From Guillaume Chau (https://github.com/Akryum):
This is because the Apollo Client cache can't compute a different ID
for the two items, so you endup with Account:undefined (or similar)
for both. Open the Apollo devtools and look at the myAccounts key in
the cache.
Learn more:
https://www.apollographql.com/docs/react/caching/cache-configuration/

GraphQL: Filtering, sorting and paging on nested entities from separate data sources?

I'm attempting to use graphql to tie together a number of rest endpoints, and I'm stuck on how to filter, sort and page the resulting data. Specifically, I need to filter and/or sort by nested values.
I cannot do the filtering on the rest endpoints in all cases because they are separate microservices with separate databases. (i.e. I could filter on title in the rest endpoint for articles, but not on author.name). Likewise with sorting. And without filtering and sorting, pagination cannot be done on the rest endpoints either.
To illustrate the problem, and as an attempt at a solution, I've come up with the following using formatResponse in apollo-server, but am wondering if there is a better way.
I've boiled down the solution to the most minimal set of files that i could think of:
data.js represents what would be returned by 2 fictional rest endpoints:
export const Authors = [{ id: 1, name: 'Sam' }, { id: 2, name: 'Pat' }];
export const Articles = [
{ id: 1, title: 'Aardvarks', author: 1 },
{ id: 2, title: 'Emus', author: 2 },
{ id: 3, title: 'Tapir', author: 1 },
]
the schema is defined as:
import _ from 'lodash';
import {
GraphQLSchema,
GraphQLObjectType,
GraphQLList,
GraphQLString,
GraphQLInt,
} from 'graphql';
import {
Articles,
Authors,
} from './data';
const AuthorType = new GraphQLObjectType({
name: 'Author',
fields: {
id: {
type: GraphQLInt,
},
name: {
type: GraphQLString,
}
}
});
const ArticleType = new GraphQLObjectType({
name: 'Article',
fields: {
id: {
type: GraphQLInt,
},
title: {
type: GraphQLString,
},
author: {
type: AuthorType,
resolve(article) {
return _.find(Authors, { id: article.author })
},
}
}
});
const RootType = new GraphQLObjectType({
name: 'Root',
fields: {
articles: {
type: new GraphQLList(ArticleType),
resolve() {
return Articles;
},
}
}
});
export default new GraphQLSchema({
query: RootType,
});
And the main index.js is:
import express from 'express';
import { apolloExpress, graphiqlExpress } from 'apollo-server';
var bodyParser = require('body-parser');
import _ from 'lodash';
import rql from 'rql/query';
import rqlJS from 'rql/js-array';
import schema from './schema';
const PORT = 8888;
var app = express();
function formatResponse(response, { variables }) {
let data = response.data.articles;
// Filter
if ({}.hasOwnProperty.call(variables, 'q')) {
// As an example, use a resource query lib like https://github.com/persvr/rql to do easy filtering
// in production this would have to be tightened up alot
data = rqlJS.query(rql.Query(variables.q), {}, data);
}
// Sort
if ({}.hasOwnProperty.call(variables, 'sort')) {
const sortKey = _.trimStart(variables.sort, '-');
data = _.sortBy(data, (element) => _.at(element, sortKey));
if (variables.sort.charAt(0) === '-') _.reverse(data);
}
// Pagination
if ({}.hasOwnProperty.call(variables, 'offset') && variables.offset > 0) {
data = _.slice(data, variables.offset);
}
if ({}.hasOwnProperty.call(variables, 'limit') && variables.limit > 0) {
data = _.slice(data, 0, variables.limit);
}
return _.assign({}, response, { data: { articles: data }});
}
app.use('/graphql', bodyParser.json(), apolloExpress((req) => {
return {
schema,
formatResponse,
};
}));
app.use('/graphiql', graphiqlExpress({
endpointURL: '/graphql',
}));
app.listen(
PORT,
() => console.log(`GraphQL Server running at http://localhost:${PORT}`)
);
For ease of reference, these files are available at this gist.
With this setup, I can send this query:
{
articles {
id
title
author {
id
name
}
}
}
Along with these variables (It seems like this is not the intended use for the variables, but it was the only way I could get the post processing parameters into the formatResponse function.):
{ "q": "author/name=Sam", "sort": "-id", "offset": 1, "limit": 1 }
and get this response, filtered to where Sam is the author, sorted by id descending, and getting getting the second page where the page size is 1.
{
"data": {
"articles": [
{
"id": 1,
"title": "Aardvarks",
"author": {
"id": 1,
"name": "Sam"
}
}
]
}
}
Or these variables:
{ "sort": "-author.name", "offset": 1 }
For this response, sorted by author name descending and getting all articles except the first.
{
"data": {
"articles": [
{
"id": 1,
"title": "Aardvarks",
"author": {
"id": 1,
"name": "Sam"
}
},
{
"id": 2,
"title": "Emus",
"author": {
"id": 2,
"name": "Pat"
}
}
]
}
}
So, as you can see, I am using the formatResponse function for post processing to do the filtering/paging/sorting. .
So, my questions are:
Is this a valid use case?
Is there a more canonical way to do filtering on deeply nested properties, along with sorting and paging?
Is this a valid use case? Is there a more canonical way to do filtering on deeply nested properties, along with sorting and paging?
Major part of original questing lies on segregating collections on different databases on separate microservices. In fact, it's nessasary to perform collection joining and subsequent filtering on some key, but it's directly impossible since there is no field in original collection to filter, sort or paginate.
Strightforward solution is perform full or filtered queries to original collections, and then perform joining and filtering result dataset on application server, e.g. by lodash, such at your solution. In is possible for small collections, but in general case causes large data transfer and unefficent sorting since there is no index structure - real RB-tree or SkipList, so with quadratic complexity it's not very good.
Dependent on resource volume on application server, special cache and index tables can be build there. If collection structure is fixed, some relations between collection entries and their fields can be reflected in special search table and update respectively on demain. It's like find & search index creation, but not it database, but on application server. Of cource, it will consume resources, but will be more fast than direct lodash-like sorting.
Also task can be solved from another side, if there is access to structure of original databases. Key is denormalization. In counter for classical relation approach, collections can have dublicate information for avioding further join operation. E.g., Articles collection can have some information from Authors collection, which is nessasary to perform filtering, sorting and pagination in further operations.

Resources