Skip to content
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

Merged
merged 9 commits into from
Jun 24, 2017

Conversation

shadaj
Copy link
Contributor

@shadaj shadaj commented Jun 16, 2017

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 #1779

TODO:

  • If this PR is a new feature, reference an issue where a consensus about the design was reached (not necessary for small changes)
  • Make sure all of the significant new logic is covered by tests
  • Rebase your changes on master so that they can be merged easily
  • Make sure all tests and linter rules pass
  • Update CHANGELOG.md with your change
  • Add your name and email to the AUTHORS file (optional)

@apollo-cla
Copy link

@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/

@shadaj shadaj force-pushed the connection-directive-handling branch 2 times, most recently from e06e62b to 312ffaa Compare June 16, 2017 22:10
Copy link
Contributor

@helfer helfer left a 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! 👍

@@ -127,6 +127,7 @@ export default class ApolloClient implements DataProxy {
public queryManager: QueryManager;
public reducerConfig: ApolloReducerConfig;
public addTypename: boolean;
public removeConnectionDirective: boolean;
Copy link
Contributor

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.

@@ -135,6 +136,7 @@ export class QueryManager {
public ssrMode: boolean;

private addTypename: boolean;
private removeConnectionDirective: boolean;
Copy link
Contributor

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.

}

export function storeKeyNameFromFieldNameAndArgs(fieldName: string, args?: Object): string {
export type Directives = {
[fieldName: string]: {
Copy link
Contributor

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?

};

export function getStoreKeyName(fieldName: string, directives?: Directives, args?: Object): string {
if (directives && directives['connection'] && directives['connection']['key']) {
Copy link
Contributor

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.

@@ -1019,6 +1024,10 @@ export class QueryManager {
queryDoc = addTypenameToDocument(queryDoc);
}

if (this.removeConnectionDirective) {
Copy link
Contributor

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,
Copy link
Contributor

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', () => {
Copy link
Contributor

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);
});
});
Copy link
Contributor

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.

Copy link
Contributor

@helfer helfer Jun 19, 2017

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', () => {
Copy link
Contributor

@helfer helfer Jun 19, 2017

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', () => {
Copy link
Contributor

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.

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);
Copy link
Contributor

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
Copy link
Contributor

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?

@shadaj shadaj force-pushed the connection-directive-handling branch 2 times, most recently from af496bb to 64c3262 Compare June 21, 2017 17:22
Copy link
Contributor

@helfer helfer left a 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', () => {
Copy link
Contributor

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?

Copy link
Contributor

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', () => {
Copy link
Contributor

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', () => {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@shadaj shadaj force-pushed the connection-directive-handling branch from 669ef44 to 6d62776 Compare June 22, 2017 22:59
Copy link
Contributor

@helfer helfer left a 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', () => {
Copy link
Contributor

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?

return Promise.resolve()
.then(() => {
const qid = this.queryManager.generateQueryId();
let combinedOptions: any = null;

previous = this.currentResult().data;
Copy link
Contributor

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.

@@ -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();
Copy link
Contributor

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.

@shadaj shadaj force-pushed the connection-directive-handling branch from 5d54f8d to c1c6552 Compare June 23, 2017 22:04
@shadaj shadaj force-pushed the connection-directive-handling branch from c1c6552 to 3e136bd Compare June 23, 2017 22:05
@helfer helfer merged commit 94d8d20 into master Jun 24, 2017
@helfer
Copy link
Contributor

helfer commented Jun 24, 2017

Merged! 🎉
Nice work @shadaj 🙂

@scf4
Copy link

scf4 commented Jul 9, 2017

I'm all up-to-date but getting unknown directive when trying to use this. Am I missing something?

query thread($id: Int! $offset: Int $limit: Int){
  thread(id: $id) {
    ...
    posts(offset: $offset limit: $limit) @connection(key: "posts") {
      ...
    }
  }
}

@stubailo
Copy link
Contributor

stubailo commented Jul 9, 2017

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?

@juhaelee
Copy link

@stubailo im also getting an unknown directive, but only when im using persisted queries. here's my stack trace:

{ Error: GraphQL error: Unknown directive "connection".
    at new ApolloError (/Users/Projects/client/node_modules/apollo-client/src/errors/ApolloError.js:34:28)
    at /Users/Projects/client/node_modules/apollo-client/src/core/QueryManager.js:322:41
    at /Users/Projects/client/node_modules/apollo-client/src/core/QueryManager.js:816:25
    at Array.forEach (native)
    at /Users/Projects/client/node_modules/apollo-client/src/core/QueryManager.js:813:27
    at Array.forEach (native)
    at QueryManager.broadcastQueries (/Users/Projects/client/node_modules/apollo-client/src/core/QueryManager.js:810:42)
    at /Users/Projects/client/node_modules/apollo-client/src/core/QueryManager.js:745:31
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:169:7)
  graphQLErrors:
   [ { message: 'Unknown directive "connection".',
       locations: [Array] } ],
  networkError: null,
  message: 'GraphQL error: Unknown directive "connection".',
  extraInfo: undefined,
  queryErrors: [ [Circular] ] }

@stubailo
Copy link
Contributor

Oh, that's interesting! Looks like the persisted queries tool hasn't been updated to remove the @connection directive before the query is sent to the server. Can you file an issue there?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adjusting the store in order to squash pagination results via Relay's @connection directive.
6 participants