I have a simple graphQl query life below
export function () {
const {data: usersData, loading, refetch, networkStatus } = useQuery(USERS_QUERY, {
context: { headers: { 'accept-language': locale}},
variables: props.variables,
fetchPolicy: 'network-only
});
return (usersData?.map(.....))
}
My query works fine. However, when there is a network issue or internet failure for example, the old data still remain despite the fact that the networkStatus changing to 2. Is there any way to make the data update when the network fail? Considering networkStatus already changed, why is data cached ? Any help would be appreciated.
Related
I have already looked at: Writing nested object to Apollo client cache
And I guess everywhere else on the internet.
So what I want to do is to write an object to the Apollo cache, to store it locally. This is my setup:
const cache = new InMemoryCache()
const client = new ApolloClient({
uri: '/hasura/v1/graphql',
cache,
credentials: 'include',
})
Then I write this to initialize the cache, taken from: https://www.apollographql.com/docs/react/data/local-state/#querying-local-state
cache.writeData({
data: {
todos: [],
visibilityFilter: 'SHOW_ALL',
networkStatus: {
__typename: 'NetworkStatus',
id: 1,
isConnected: false,
},
},
})
Now, when I want to query the cache using this query:
const lolquery = gql`{
visibilityFilter #client
}`
const result = cache.readQuery({ query: lolquery })
console.log(result)
I can get the visibilityFilter and todos, but when I try to query for networkStatus i.e.,
const lolquery = gql`{
networkStatus #client
}`
I get the following error:
Uncaught Invariant Violation: Invariant Violation: 10
Googling this error doesn't give me much of an answer.
Am I the only one experiencing this? Am I missing something really obvious?
networkStatus was initialized as an object, so you have to query it with a subselection of one or more fields.
{
networkStatus #client {
isConnected
}
}
I have a fairly simple node app using AWS AppSync. I am able to run queries and mutations successfully but I've recently found that if I run a query twice I get the same response - even when I know that the back-end data has changed. In this particular case the query is backed by a lambda and in digging into it I've discovered that the query doesn't seem to be sent out on the network because the lambda is not triggered each time the query runs - just the first time. If I use the console to simulate my query then everything runs fine. If I restart my app then the first time a query runs it works fine but successive queries again just return the same value each time.
Here are some part of my code:
client.query({
query: gql`
query GetAbc($cId: String!) {
getAbc(cId: $cId) {
id
name
cs
}
}`,
options: {
fetchPolicy: 'no-cache'
},
variables: {
cid: event.cid
}
})
.then((data) => {
// same data every time
})
Edit: trying other fetch policies like network-only makes no visible difference.
Here is how I set up the client, not super clean but it seems to work:
const makeAWSAppSyncClient = (credentials) => {
return Promise.resolve(
new AWSAppSyncClient({
url: 'lalala',
region: 'us-west-2',
auth: {
type: 'AWS_IAM',
credentials: () => {
return credentials
}
},
disableOffline: true
})
)
}
getRemoteCredentials()
.then((credentials) => {
return makeAWSAppSyncClient(credentials)
})
.then((client) => {
return client.hydrated()
})
.then((client) => {
// client is good to use
})
getRemoteCredentials is a method to turn an IoT authentication into normal IAM credentials which can be used with other AWS SDKs. This is working (because I wouldn't get as far as I do if not).
My issue seems very similar to this one GraphQL Query Runs Sucessfully One Time and Fails To Run Again using Apollo and AWS AppSync; I'm running in a node environment (rather than react) but it is essentially the same issue.
I don't think this is relevant but for completeness I should mention I have tried both with and without the setup code from the docs. This appears to make no difference (except annoying logging, see below) but here it is:
global.WebSocket = require('ws')
global.window = global.window || {
setTimeout: setTimeout,
clearTimeout: clearTimeout,
WebSocket: global.WebSocket,
ArrayBuffer: global.ArrayBuffer,
addEventListener: function () { },
navigator: { onLine: true }
}
global.localStorage = {
store: {},
getItem: function (key) {
return this.store[key]
},
setItem: function (key, value) {
this.store[key] = value
},
removeItem: function (key) {
delete this.store[key]
}
};
require('es6-promise').polyfill()
require('isomorphic-fetch')
This is taken from: https://docs.aws.amazon.com/appsync/latest/devguide/building-a-client-app-javascript.html
With this code and without offlineDisabled: true in the client setup I see this line spewed continuously on the console:
redux-persist asyncLocalStorage requires a global localStorage object.
Either use a different storage backend or if this is a universal redux
application you probably should conditionally persist like so:
https://gist.github.com/rt2zz/ac9eb396793f95ff3c3b
This makes no apparent difference to this issue however.
Update: my dependencies from package.json, I have upgraded these during testing so my yarn.lock contains more recent revisions than listed here. Nevertheless: https://gist.github.com/macbutch/a319a2a7059adc3f68b9f9627598a8ca
Update #2: I have also confirmed from CloudWatch logs that the query is only being run once; I have a mutation running regularly on a timer that is successfully invoked and visible in CloudWatch. That is working as I'd expect but the query is not.
Update #3: I have debugged in to the AppSync/Apollo code and can see that my fetchPolicy is being changed to 'cache-first' in this code in apollo-client/core/QueryManager.js (comments mine):
QueryManager.prototype.fetchQuery = function (queryId, options, fetchType, fetchMoreForQueryId) {
var _this = this;
// Next line changes options.fetchPolicy to 'cache-first'
var _a = options.variables, variables = _a === void 0 ? {} : _a, _b = options.metadata, metadata = _b === void 0 ? null : _b, _c = options.fetchPolicy, fetchPolicy = _c === void 0 ? 'cache-first' : _c;
var cache = this.dataStore.getCache();
var query = cache.transformDocument(options.query);
var storeResult;
var needToFetch = fetchPolicy === 'network-only' || fetchPolicy === 'no-cache';
// needToFetch is false (because fetchPolicy is 'cache-first')
if (fetchType !== FetchType.refetch &&
fetchPolicy !== 'network-only' &&
fetchPolicy !== 'no-cache') {
// so we come through this branch
var _d = this.dataStore.getCache().diff({
query: query,
variables: variables,
returnPartialData: true,
optimistic: false,
}), complete = _d.complete, result = _d.result;
// here complete is true, result is from the cache
needToFetch = !complete || fetchPolicy === 'cache-and-network';
// needToFetch is still false
storeResult = result;
}
// skipping some stuff
...
if (shouldFetch) { // shouldFetch is still false so this doesn't execute
var networkResult = this.fetchRequest({
requestId: requestId,
queryId: queryId,
document: query,
options: options,
fetchMoreForQueryId: fetchMoreForQueryId,
}
// resolve with data from cache
return Promise.resolve({ data: storeResult });
If I use my debugger to change the value of shouldFetch to true then at least I see a network request go out and my lambda executes. I guess I need to unpack what that line that is changing my fetchPolicy is doing.
OK I found the issue. Here's an abbreviated version of the code from my question:
client.query({
query: gql`...`,
options: {
fetchPolicy: 'no-cache'
},
variables: { ... }
})
It's a little bit easier to see what is wrong here. This is what it should be:
client.query({
query: gql`...`,
fetchPolicy: 'network-only'
variables: { ... }
})
Two issues in my original:
fetchPolicy: 'no-cache' does not seem to work here (I get an empty response)
putting the fetchPolicy in an options object is unnecessary
The graphql client specifies options differently and we were switching between the two.
Set the query fetch-policy to 'network-only' when running in an AWS Lambda function.
I recommend using the overrides for WebSocket, window, and localStorage since these objects don't really apply within a Lambda function. The setup I typically use for NodeJS apps in Lambda looks like the following.
'use strict';
// CONFIG
const AppSync = {
"graphqlEndpoint": "...",
"region": "...",
"authenticationType": "...",
// auth-specific keys
};
// POLYFILLS
global.WebSocket = require('ws');
global.window = global.window || {
setTimeout: setTimeout,
clearTimeout: clearTimeout,
WebSocket: global.WebSocket,
ArrayBuffer: global.ArrayBuffer,
addEventListener: function () { },
navigator: { onLine: true }
};
global.localStorage = {
store: {},
getItem: function (key) {
return this.store[key]
},
setItem: function (key, value) {
this.store[key] = value
},
removeItem: function (key) {
delete this.store[key]
}
};
require('es6-promise').polyfill();
require('isomorphic-fetch');
// Require AppSync module
const AUTH_TYPE = require('aws-appsync/lib/link/auth-link').AUTH_TYPE;
const AWSAppSyncClient = require('aws-appsync').default;
// INIT
// Set up AppSync client
const client = new AWSAppSyncClient({
url: AppSync.graphqlEndpoint,
region: AppSync.region,
auth: {
type: AppSync.authenticationType,
apiKey: AppSync.apiKey
}
});
There are two options to enable/disable caching with AppSyncClient/ApolloClient, for each query or/and on initializing the client.
Client Config:
client = new AWSAppSyncClient(
{
url: 'https://myurl/graphql',
region: 'my-aws-region',
auth: {
type: AUTH_TYPE.AWS_MY_AUTH_TYPE,
credentials: await getMyAWSCredentialsOrToken()
},
disableOffline: true
},
{
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'no-cache', // <-- HERE: check the apollo fetch policy options
errorPolicy: 'ignore'
},
query: {
fetchPolicy: 'no-cache',
errorPolicy: 'all'
}
}
}
);
Alternative: Query Option:
export default graphql(gql`query { ... }`, {
options: { fetchPolicy: 'cache-and-network' },
})(MyComponent);
Valid fetchPolicy values are:
cache-first: This is the default value where we always try reading data from your cache first. If all the data needed to fulfill your query is in the cache then that data will be returned. Apollo will only fetch from the network if a cached result is not available. This fetch policy aims to minimize the number of network requests sent when rendering your component.
cache-and-network: This fetch policy will have Apollo first trying to read data from your cache. If all the data needed to fulfill your query is in the cache then that data will be returned. However, regardless of whether or not the full data is in your cache this fetchPolicy will always execute query with the network interface unlike cache-first which will only execute your query if the query data is not in your cache. This fetch policy optimizes for users getting a quick response while also trying to keep cached data consistent with your server data at the cost of extra network requests.
network-only: This fetch policy will never return you initial data from the cache. Instead it will always make a request using your network interface to the server. This fetch policy optimizes for data consistency with the server, but at the cost of an instant response to the user when one is available.
cache-only: This fetch policy will never execute a query using your network interface. Instead it will always try reading from the cache. If the data for your query does not exist in the cache then an error will be thrown. This fetch policy allows you to only interact with data in your local client cache without making any network requests which keeps your component fast, but means your local data might not be consistent with what is on the server. If you are interested in only interacting with data in your Apollo Client cache also be sure to look at the readQuery() and readFragment() methods available to you on your ApolloClient instance.
no-cache: This fetch policy will never return your initial data from the cache. Instead it will always make a request using your network interface to the server. Unlike the network-only policy, it also will not write any data to the cache after the query completes.
Copied from: https://www.apollographql.com/docs/react/api/react-hoc/#graphql-options-for-queries
Using AWS, I have followed an example of a lambda function using the serverless framework. It is working as expected, but now I wonder what the best way of caching the response is.
My final version of this will consist of one or more json objects that will be retrieved on a regular basis.
The client side will call an api that will retrieve the already cached data.
So what AWS service should I implement to actually make the cache?
If it's static bit of JSON i'd simply import/return it from within the function rather than enable caching, but hey it's your API!
To answer your question you can use caching within API Gateway to do so, documentation can be found here
Update:
I'd actually misread the question so apologies for that, whilst that caching works what the Op is asking is where to store data retrieved - if your retrieving it from an external API you can just write it to s3 like so:
import AWS from 'aws-sdk'
export default (event, context, callback) => {
let s3Bucket = new AWS.S3({ params: { Bucket: 'YOUR_BUCKET_NAME' } })
let documentName = 'someName'
let fileExtension = 'json'
let s3data = {
Key: `${documentName}.${fileExtension}`,
Body: JSON.parse(event.body.someJsonObject),
}
s3Bucket.putObject(s3data, (err, s3result) => {
if (err) {
console.log(err)
callback(err, null)
} else {
console.log('S3 added', s3result)
callback(null, 'done')
}
})
}
Then you just need to read the object back in your serving endpoint.
Following the example provided in the docs I find the following message repeated many times in the logs:
redux-persist asyncLocalStorage requires a global localStorage object. Either use a different storage backend or if this is a universal redux application you probably should conditionally persist like so: https://gist.github.com/rt2zz/ac9eb396793f95ff3c3b
I can work around it by turning off offline support when creating the AppSync client, like this:
new AWSAppSyncClient({
url: 'https://...appsync-api.us-west-2.amazonaws.com/graphql',
region: 'us-west-2',
auth: {
type: 'AWS_IAM',
credentials: ...
},
disableOffline: true
})
... however I do want to use the offline store. I am using the setup config from the documentation like so:
global.WebSocket = require('ws');
global.window = global.window || {
setTimeout: setTimeout,
clearTimeout: clearTimeout,
WebSocket: global.WebSocket,
ArrayBuffer: global.ArrayBuffer,
addEventListener: function () { },
navigator: { onLine: true }
};
global.localStorage = {
store: {},
getItem: function (key) {
return this.store[key]
},
setItem: function (key, value) {
this.store[key] = value
},
removeItem: function (key) {
delete this.store[key]
}
};
require('es6-promise').polyfill();
require('isomorphic-fetch');
But it doesn't seem to work with redux-persist which is used a few layers deep in the AppSync client.
I have found a very simple way to resolve this issue. While this section is taken directly from the AWS docs it is not quite right:
global.localStorage = {
store: {},
...
};
By setting global.window.localStorage instead I am able to work around the issues:
global.window.localStorage = {
store: {},
...
};
Anyone else trying to use AppSync like this may want to know that node-localstorage also seems to work with this usage (after yarn add node-localstorage):
var LocalStorage = require('node-localstorage').LocalStorage
global.window.localStorage = new LocalStorage(<path for storage>)
Importantly, in this case, your queries are persisted to the file system and will be read if connectivity is lost. This could potentially work after restarting your application (but I've not tested this yet because you need an AWS credentials object to create the AppSync client).
The question is about the interaction of a mutation, optimistic response, and a watchQuery.
I have a mutation "myMutation" which has an "optimisticResponse" and an implemented "update" function.
Every time I do a mutation query the "update" function is called twice, the first time with optimistic response data and the second one with real data. All is Ok and all as described in the documentation.
Into my "update" function I modify "myQuery" cache data through using readQuery/writeQuery methods.
Every time I modify "myQuery" cache data a watchQuery (based on "myQuery") subscription is called. All is Ok and all as described in the documentation.
But the problem is that I cannot distinguish into my watchQuery whether I receive optimistic response data or real response data. It is crucial for me because the reaction must be different since valuable part of data can be provided by a server only.
I should show a GUI element with a special style when I receive an optimistic response and I should prohibit any interactions with it until I receive a real response.
Unfortunately, I can't solve this matter. At a glance, there is no difference between optimistic and real responses. I've googled a lot and haven't found a solution. The only idea I have is adding a special field to my GraphQL data which will show whether a response is received from a server or not. But it looks ugly and smells bad. I am sure, there must be a simple correct way to overcome the problem.
Maybe there is an easier way or there will be one in the future but here is what I know.
The data in optimisticResponse is only provided during the first call to update. That is where you can flag to your update function that it is dealing with optimistic data. You can put any data you want there. I put isOptimistic: true,.
To deal with the watchQuery issue, I recommend you make use of apollo-link-state to add a client-only field or fields to the areas of your data model where optimistic upserts should be known to the display. Don't include isOptimistic in your mutation query so you know it's from the server and not the optimistic response and force it to false if it's not true. See this example:
const SUBMIT_COMMENT_MUTATION = gql`
mutation submitComment($repoFullName: String!, $commentContent: String!) {
submitComment(repoFullName: $repoFullName, commentContent: $commentContent) {
postedBy {
login
html_url
}
createdAt
content
}
}
`;
const CommentsPageWithMutations = ({ currentUser }) => (
<Mutation mutation={SUBMIT_COMMENT_MUTATION}>
{(mutate) => (
<CommentsPage
submit={(repoFullName, commentContent) =>
mutate({
variables: { repoFullName, commentContent },
optimisticResponse: {
__typename: 'Mutation',
submitComment: {
__typename: 'Comment',
postedBy: currentUser,
createdAt: new Date(),
content: commentContent,
isOptimistic: true, // Only provided to update on the optimistic call
},
},
update: (proxy, { data: { submitComment } }) => {
// Make sure CommentAppQuery includes isOptimistic for each comment added by apollo-link-state
// submitComment.isOptimistic will be undefined here if it's from the server
const newComment = {
...submitComment,
isOptimistic: submitCommit.isOptimistic ? true : false,
};
// Read the data from our cache for this query.
const data = proxy.readQuery({ query: CommentAppQuery });
// Add our comment from the mutation to the end.
data.comments.push(newComment);
// Write our data back to the cache.
proxy.writeQuery({ query: CommentAppQuery, data });
},
})
}
/>
)}
</Mutation>
);
See https://www.apollographql.com/docs/link/links/state.html.
I couldn't get this to work on Apollo 3.X by only adding a property on the optimistic response, the property was getting stripped away. To get it to work I had to add a local variable to the query.
fragment object on Object {
id
isOptimistic #client
...
Once that is done, I was able to add the local-only flag to my optimistic response.
const optimisticResponse = {
object: {
id: "temp-id",
isOptimistic: true,
...
}
}