-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Specifically handle the connection directive when selecting store keys, fix #1779 #1801
Conversation
@shadaj: Thank you for submitting a pull request! Before we can merge it, you'll need to sign the Meteor Contributor Agreement here: https://contribute.meteor.com/ |
e06e62b
to
312ffaa
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@shadaj this is a great start! 👏
I've left a few comments in the code for things to change. I think the major ones are around creating an applyTransforms
function and pulling together the getStoreKeyName
functions into one and splitting out other stuff. Apart from that, it's just about adding more tests to make sure things work as intended.
For example, we should add tests to make sure that fetchMore
works as planned with the @connection
directive. A good start would be to copy some (or all) of the fetchMore
tests and rewrite them to use connections. It should work pretty much as is, but we should still make sure that it actually does.
Another thing we'll have to figure out is whether there's a way to use @connection
when we're not using fetchMore
. Right now I don't think there is, but if we add special connection types with custom resolvers in the future, I think there will be.
Really great work! 👍
src/ApolloClient.ts
Outdated
@@ -127,6 +127,7 @@ export default class ApolloClient implements DataProxy { | |||
public queryManager: QueryManager; | |||
public reducerConfig: ApolloReducerConfig; | |||
public addTypename: boolean; | |||
public removeConnectionDirective: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessary, we probably always want to remove the directive.
src/core/QueryManager.ts
Outdated
@@ -135,6 +136,7 @@ export class QueryManager { | |||
public ssrMode: boolean; | |||
|
|||
private addTypename: boolean; | |||
private removeConnectionDirective: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't think we need this.
src/data/storeUtils.ts
Outdated
} | ||
|
||
export function storeKeyNameFromFieldNameAndArgs(fieldName: string, args?: Object): string { | ||
export type Directives = { | ||
[fieldName: string]: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this supposed to be directiveName
?
src/data/storeUtils.ts
Outdated
}; | ||
|
||
export function getStoreKeyName(fieldName: string, directives?: Directives, args?: Object): string { | ||
if (directives && directives['connection'] && directives['connection']['key']) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to make debugging easier for people by throwing an error or printing a warning if we encounter a connection
directive that doesn't define the key
argument.
src/core/QueryManager.ts
Outdated
@@ -1019,6 +1024,10 @@ export class QueryManager { | |||
queryDoc = addTypenameToDocument(queryDoc); | |||
} | |||
|
|||
if (this.removeConnectionDirective) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure exactly where transformQueryDoucment
gets run from, but we'll have to make sure the directive doesn't get removed before it reaches the cache. It should also still be there when the query is read out of the cache. Only the server should be unaware of it.
test/client.ts
Outdated
|
||
const client = new ApolloClient({ | ||
networkInterface, | ||
addTypename: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think addTypename
is true by default, so you don't need to put it here.
test/client.ts
Outdated
}); | ||
}); | ||
|
||
it('should not remove the connection directive if there are no arguments', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test shouldn't exist because we should remove the directive anyway (as I've written in the code)
return client.query({ query }).then((actualResult) => { | ||
assert.deepEqual(actualResult.data, result); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be an end-to-end test as well that checks that the store contains the right objects after a query. The structure can be the same as for this test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a test to check if it prints a warning when @connection
is used without the key
argument. You can use withWarning
or withError
for that.
@@ -812,4 +812,43 @@ describe('reading from the store', () => { | |||
nullField: null, | |||
}); | |||
}); | |||
|
|||
it('properly handles the connection directive', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good test, but what concerns me a little bit now is that we're completely ignoring the arguments on the field and simply return the whole result. I think it's fine right now, but we might want to change it and start supporting different pagination types like skip/offset
, cursor
etc. which would then use custom resolvers. So the limit
connection would look for the skip and limit arguments, and extract only the relevant part from the array. In the example in this test, it would notice that it doesn't have two elements, so it would actually go and fetch those for the server (we may have to add an index to every field in the store so we can start fetching from the middle and don't have to start with offset: 0
).
@@ -1311,4 +1311,62 @@ describe('writing to the store', () => { | |||
}); | |||
}, /stringField(.|\n)*abcd/g); | |||
}); | |||
|
|||
it('properly handles the connection directive', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test is good, but it also shows that whenever we fetch anything that has a connection
directive on it, we'll end up overwriting what's already in the store. It may be okay as long as it only works with fetchMore
, but I'm pretty sure people will ask for different behaviors, so we may have to add custom write-to-store logic for the connection types. I don't think we'll implement it for the old Redux store, but it's definitely something great to keep in mind for the new store.
src/ApolloClient.ts
Outdated
this.disableNetworkFetches = ssrMode || ssrForceFetchDelay > 0; | ||
this.dataId = dataIdFromObject = dataIdFromObject || defaultDataIdFromObject; | ||
this.dataIdFromObject = this.dataId; | ||
this.fieldWithArgs = storeKeyNameFromFieldNameAndArgs; | ||
this.fieldWithArgs = (fieldName, args) => getStoreKeyName(fieldName, undefined, args); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe change this to have directives last, that way people can call it with directives.
CHANGELOG.md
Outdated
@@ -3,6 +3,8 @@ | |||
### vNEXT | |||
|
|||
- `batchInterval` now has a default value of 10 ms [PR #1793](https://github.com/apollographql/apollo-client/pull/1793) | |||
- the `@connection(key: ...)` directive can now be used to specify the key to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we call it something that doesn't restrict its use to only connections?
af496bb
to
64c3262
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! I think there are a few more tests to add, but the ones that are here are good 🙂
test/client.ts
Outdated
}); | ||
|
||
return client.query({ query }).then((actualResult) => { | ||
assert.deepEqual(actualResult.data, result); | ||
}); | ||
}); | ||
|
||
it('should not remove the connection directive if there are no arguments', () => { | ||
it('should run queries that include the connection directive even if there are no arguments', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear what run
means in this context. Does it mean sending them over the network? Does it mean executing them against the store? Or both?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test actually tests that the connection directive gets ignored by the store and removed by the network interface.
test/client.ts
Outdated
@@ -2637,6 +2634,57 @@ describe('client', () => { | |||
}); | |||
}); | |||
|
|||
it('should run queries that include the connection directive', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this could also be more descriptive. For example something like "runs a query with the connection directive and writes it to the store key defined in the directive" or something like that.
@@ -483,6 +503,44 @@ describe('network interface', () => { | |||
}); | |||
}); | |||
}); | |||
|
|||
describe('transforming queries', () => { | |||
it('should remove the @connection directive', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this test is good as it is, but another way to test this would be to mock fetch
and then check that the right params are passed to it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test indirectly checks this, since the existing mocked fetch
will only return the expected response if the @connection
was removed.
669ef44
to
6d62776
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately I think we'll have to solve this at a lower level. updateQuery
should get the real current result at the moment the fetchMore query has returned, along with the new fetchMore result. The fetchMore result doesn't need to be written to the store at all, so maybe that will help.
test/client.ts
Outdated
@@ -2634,7 +2634,7 @@ describe('client', () => { | |||
}); | |||
}); | |||
|
|||
it('should run queries that include the connection directive', () => { | |||
it('should run a query with the connection directive and write the result to the store key defined in the directive', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test description is super similar to the one on line 2558, how are they different?
src/core/ObservableQuery.ts
Outdated
return Promise.resolve() | ||
.then(() => { | ||
const qid = this.queryManager.generateQueryId(); | ||
let combinedOptions: any = null; | ||
|
||
previous = this.currentResult().data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this will actually work, because while the fetchMore
query is in flight, the current query's data might change. That change should still be reflected in the previousResult
. Instead of bypassing, you'll have to fix the value of previousResult
so it is actually the previous result and not the new one. It might be possible to do that by not writing query results to the store if it's a fetchMore query.
src/core/ObservableQuery.ts
Outdated
@@ -229,15 +229,13 @@ export class ObservableQuery<T> extends Observable<ApolloQueryResult<T>> { | |||
throw new Error('updateQuery option is required. This function defines how to update the query data with the new results.'); | |||
} | |||
|
|||
let previous: any; | |||
const previous = this.result(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this has the same semantics wrt when it runs, which means it still doesn't fix the race condition of an update while the query is in flight.
5d54f8d
to
c1c6552
Compare
c1c6552
to
3e136bd
Compare
Merged! 🎉 |
I'm all up-to-date but getting query thread($id: Int! $offset: Int $limit: Int){
thread(id: $id) {
...
posts(offset: $offset limit: $limit) @connection(key: "posts") {
...
}
}
} |
Can you open a new issue about this please @scf4? Also, can you post the full error with stack trace and some code as well? |
@stubailo im also getting an unknown directive, but only when im using persisted queries. here's my stack trace:
|
Oh, that's interesting! Looks like the persisted queries tool hasn't been updated to remove the |
This gives the client specific knowledge of the
@connection
directive so that the store key can be properly derived from the directive. This also updates the query transformer to remove the directive before sending requests to the server. This fixes #1779TODO: