Apollo: Refetch queries that have multiple variable permutations after mutation - apollo

Let's say I have a table that lists a bunch of Posts using a query like:
const PostsQuery = gql`
query posts($name: string) {
posts {
id
name
status
}
}
`;
const query = apolloClient.watchQuery({query: PostsQuery});
query.subscribe({
next: (posts) => console.log(posts) // [ {name: "Post 1", id: '1', status: 'pending' }, { name: "Paul's Post", id: '2', status: 'pending'} ]
});
Then later my user comes along and enters a value in a search field and calls this code:
query.setVariables({name: 'Paul'})
It fetches the filtered posts and logs it out fine.
// [ { name: "Paul's Post", id: '2', status: 'pending'} ]
Now, in my table there is a button that changes the status of a post from 'Pending' to 'Active'. The user clicks that and it calls code like:
const PostsMutation = gql`
mutation activatePost($id: ID!) {
activatePost(id: $id) {
ok
object {
id
name
status
}
}
}
`;
apolloClient.mutate({mutation: PostsMutation});
All is well with the mutation, but now I want to refetch the table data so it has the latest, so I make a change:
apolloClient.mutate({
mutation: PostsMutation,
refetchQueries: [{query: PostsQuery, variables: {name: 'Paul'}]
});
Hurray, it works!
// [ { name: "Paul's Post", id: '2', status: 'active'} ]
But... now my user clears the search query, expecting the results to update.
query.setVariables({});
// [ {name: "Post 1", id: '1', status: 'pending' }, { name: "Paul's Post", id: '2', status: 'pending'} ]
Oh no! Because the data was not refetched in our mutation with our "original" variables (meaning none), we are getting stale data!
So how do you handle a situation where you have a mutation that may affect a query that could have many permutations of variables?

I had a similar issue, I am using Apollo with Angular, so I am not sure if this method will work with React Client, but it should.
If you look closely at refetchQueries properties on the mutate method, you will see that the function can also return a string array of query names to refetch. By returning just the query name as a string, you do not need to worry about the variables. Be advised that this will refetch all the queries matching the name. So if you had a lot queries with different variables it could end up being a large request. But, in my case it is worth the trade off. If this is a problem, you could also get access to the queryManager through apolloClient.queryManager which you could use to do some more fine grained control of what to refetch. I didn't implement it, but it looked very possible. I found the solution below fits my needs fine.
In your code, what you need to do is:
apolloClient.mutate({
mutation: PostsMutation,
refetchQueries: (mutationResult) => ['PostQueries']
});
This will refetch any query with the name 'PostQueries'. Again, it is possible to only refetch a subset of them if you dig into the queryManager and do some filtering on the active watch queries. But, that is another exercise.

Related

Amplify AppSync: custom sorting and filtering with pagination

I'm trying to write a schema so that I can query models filtered by multiple keys, sorted by a custom key and paginated.
an example of my model:
type Article {
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
And an example of the query I would like to do is: retrieve all the Articles which are part of both a given category AND area, returned in descending order by publishOn in chunks of 10 items each (to implement pagination server-side, and have a lightweight UI).
The response should include also the nextToken attribute that can be used to load the "next" page of the filtered articles list.
I have multiple problems with what I can do with the automatically generated schema and can't find a way to implement manually a solution that works for all what I want to do. I try and make a list of what goes wrong:
Filtering
Let's say I want to query 10 articles that belong to the category "Holiday":
listArticles(filter: {category: {eq: "Holiday} }, limit: 10)
I won't get the first 10 articles that match that category, but instead, it seems that AppSync selects the first 10 items in the table, and then it filters these 10 items by the filter criteria.
In other words, it seems that the sequence in which filtering and sorting are applied is the opposite of what expected. Expected: firstly filter the table by the filter critaria, then return the first 10 items of the filtered result sets.
Sorting
I couldn't find a way to add sorting with AppSync, so I added searchable:
type Article (
#searchable
) {
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
Now if I sort by date, that key will be used as nextToken and brake the pagination. This is a known issue: https://github.com/aws-amplify/amplify-cli/issues/4434
Do you have any good tip on how to find a workaround to these bugs? I dag into the documentation and in couple of issue, but didn't come up with a solution that works well...
Thanks in advance,
Matteo
Filtering
You will need a Global Secondary Index in DynamoDB to achieve such a behaviour. You can create them with the #key annotation. I your case I would create a composite key consisting of the category for the partition key and area and publishOn as the sort key(s).
type Article
#model
#key(fields: ["id"])
#key(name: "byCategory", fields: ["category", "publishOn"])
#key(name: "byCategoryArea", fields: ["category", "area", "publishOn"])
{
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
Sorting
Sorting is done by the sortDirection property which is either DESC or ASC and can only be done on the sort key.
The #searchable directive enables elasticsearch on the table, which is a fulltext search engine and probably a bit pricy for small applications and wouldn't be required here unless you would want to query based on e.g. the article description text.
listArticles(filter: {category: {eq: "Holiday"} }, limit: 10, sortDirection: DESC)
Amplify AppSync: filtering with pagination
let allClubsList = async (sport) => {
try {
let clubsList;
let clubsInfoList = [];
let nextTokenInfo = null;
do{
let clubs = await client.query({
query: gql(clubBySportStatus),
variables: {
sport: sport,
eq: { status: "ACTIVE" },
},
limit: 100,
nextToken: nextTokenInfo,
fetchPolicy: "network-only",
});
clubsList = clubs.data.clubBySportStatus.items;
clubsList.forEach((item) => clubsInfoList.push(item));
nextTokenInfo = clubs.data.clubBySportStatus.nextToken;
} while (Boolean(nextTokenInfo));
if (clubsInfoList && clubsInfoList.length) {
return {
success: true,
data: clubsInfoList,
};
}
} catch (eX) {
console.error(`Error in allClubsList: ${JSON.stringify(eX)}`);
return {
success: false,
message: eX.message,
};
}
};

Update list items in dynamodb

My data structure in AWS DynamoDB looks like this:
{ key: 'roomNameOne',
value: {
attendees: ['A', 'B', 'C'] // this is a set,
wsConnections: [{ connectiondId: 'foo', domain: 'xyz.com' }, { connectiondId: 'bar', domain: 'xyz.com' }]
}
}
{ key: 'roomNameTwo',
value: {
attendees: ['X', 'Y', 'Z'],
wsConnections: [{ connectiondId: 'foo', domain: 'xyz.com' }, { connectiondId: 'bar', domain: 'xyz.com' }]
}
}
Now when I get a request that connectionId: foo is lost, I want to remove that entry from all the items.
So after DynamoDB update operation my list should look like this:
{ key: 'roomNameOne',
value: {
attendees: ['A', 'B', 'C'] // this is a set,
wsConnections: [{ connectiondId: 'bar', domain: 'xyz.com' }]
}
}
{ key: 'roomNameTwo',
value: {
attendees: ['X', 'Y', 'Z'],
wsConnections: [{ connectiondId: 'bar', domain: 'xyz.com' }]
}
}
Can you please help me with the query for update? The trick here is I don't know the room names, but while connection, I am aware of what all room names a connection is interested in.
Unfortunately, DynamoDB does not allow for this type of operation on a complex attribute (e.g. list of maps).
Modeling one-to-many relationships using complex attributes is a useful pattern. However, one of the drawbacks of this approach is that you won't be able to perform the types of operations you're describing.
If you have access patterns that require you to update wsConnections, you might consider modeling the relationship by making each entry of the wsConnections list it's own item in DynamoDB. For example
Storing your data in this way would make it easier for you to remove connections. For example, if you wanted to remove bar from your connections, you could perform the following operation
ddbClient.delete({
TableName: "YOUR_TABLE_NAME",
Key: {PK: "roomNameOne", SK: "wsConnection#bar"}
})
EDIT: If you don't have access to the PK, your only option is a scan operation.
ddbClient.scan({
"TableName": "YOUR TABLE NAME",
"FilterExpression": "contains(#key, :value)",
"ExpressionAttributeValues": {
":value": {
"S": "foo"
}
},
"ExpressionAttributeNames": {
"#key": "connections"
}
})
This will scan the entire database looking for items whose connections attribute contains "foo". This will let you fetch the list of items, which you can then update and persist back to DDB.
This approach is not ideal. The scan operation will search the entire database, which can be horribly inefficient. You'd also have to issue multiple requests to DDB; one to fetch and one to update. multiple roundtrips aren't the end of the world, but again, not ideal.
To unlock more flexible and efficient access patterns, it would be ideal to get the data out of the wsConnections list attribute. As long a the data is buried in a complex attribute, your options will be limited.

In Loopback 4, how to avoid update of few fields

In loopback framework, is there a way to avoid updates for few fields
Below code allows updates for all fields that is passed in the API request body.
async updateById(
#param.path.number('id') id: number,
#requestBody({
content: {
'application/json': {
schema: getModelSchemaRef(Todo, {partial: true}),
},
},
})
todo: Todo,
): Promise<void> {
await this.todoRepository.updateById(id, todo);
}
As far as I understand from your question, you want to update some part of the object in the database.
this.repo.updateById(id,objectYouWantToUpdate)
This is going to work perfectly, just send the data you want to update and not the whole object.
exclude key can help
schema: getModelSchemaRef(Todo, {partial: true, exclude: ['title']})

Can Ember-Data handle two models from one JSON payload?

I have JSON coming from the server which looks like:
data: {
user: {
address: {
id: "id",
city: "city",
street: "street",
.......
}
name: "name",
......
}
authentication-token: {
token: "token",
id: "id"
}
}
The idea is to store this two models (user, authentication-token) in ember store under the same names. When I gat the above mentioned response from a server, model user is saved successfully, but model authentication-token does not get saved to the store at all. When I log the data (in the adapter) before the data is passed to serializer I see that JSON has a structure which Ember-Data expects. I don't know whether the problem is that Ember-Data cannot handle two models in success at one time, and then save it to the corresponding models, or something else. Ideas?
Now it all makes sense to me. Of course, this was the problem in your last question. Anyway, ember-data's RESTAdapter can't handle this. If you're requesting a singular resource user it expects at most this user as a singular answer. Any other resource that may be "side-loaded" has to be an array. The requested singular user can either be a record under the user key or the first entry in an array unter the users key. Everything else (except meta data) has to be plural.
In case you're requesting a plural resource users, the main response must be under the users key, any sideloaded users that aren't part of the response prfixed with _ under the _users key.
For your example all this means that your json must be formatted like this:
data: {
user: {
address: {
id: "id",
city: "city",
street: "street",
.......
}
name: "name",
......
}
authentication-tokens: [{
token: "token",
id: "id"
}]
}
If you can't change your server, you'd have to configure the RESTAdapter to normalize the JSON data through normalize of the Serializer.

How to Model.fetch(<object>) when the returned data is a single object

I want to make an API call for searching that looks like this:
https://myapi.com/search/<query>/<token>
where query is the search term and token (optional) is an alphanumeric set of characters which identifies the position of my latest batch of results, which is used for infinite scrolling.
This call returns the following JSON response:
{
"meta": { ... },
"results" {
"token": "125fwegg3t32",
"content": [
{
"id": "125125122778",
"text": "Lorem ipsum...",
...
},
{
"id": "125125122778",
"text": "Dolor sit amet...",
...
},
...
]
}
}
content is an array of (embedded) items that I'm displaying as search results. My models look like this:
App.Content = Em.Model.extend({
id: Em.attr(),
text: Em.attr(),
...
});
App.Results = Em.Model.extend({
token: Em.attr(),
content: Em.hasMany('App.Content', {
key: 'content',
embedded: true
})
});
In order to make that API call, I figured I have to do something like this:
App.Results.reopenClass({
adapter: Em.RESTAdapter.create({
findQuery: function(klass, records, params) {
var self = this,
url = this.buildURL(klass) + '/' + params.query;
if (params.token) {
url += '/' + params.token;
}
return this.ajax(url).then(function(data) {
self.didFindQuery(klass, records, params, data);
return records;
});
}
}),
url: 'https://myapi.com/search',
});
then somewhere in my routes do this:
App.Results.fetch({query: 'query', token: '12kgkj398512j'}).then(function(data) {
// do something
return data;
})
but because the API returns a single object and Em.RESTAdapter.findQuery expects an array, an error occurs when Ember Model tries to materialize the data. So how do I do this properly? I'm using the latest build of Ember Model.
By the way, I'm aware that it would be much more convenient if the API was designed in a way so I can just call App.Content.fetch(<object>), which would return a similar JSON response, but I would then be able to set the collectionKey option to content and my data would be properly materialized.
You simply need to override your models load() method to adjust the payload hash to what Ember.Model wants. There are no serializers in Ember.Model. There is both a class level load for handling collections and an instance level load for loading the JSON specific to a single model. You want to override the instance level load method to wrap the content key value in an array if its not one already.
I have been using Ember.Mode quite heavily and enhanced it for a number of my use cases and submitted PR's for both fixes and enhancements. Those PRs have been sitting there for a while with no response from the maintainers. I have now moved to Ember.Data which has been 'rebooted' so to speak and having a lot better result with it now.
I would strongly suggest walking away from Ember.Model as it appears dead with the new pragmatic direction Ember Data has taken and because the project maintainer doesn't appear to have any interest in it anymore.