-
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
Add imperative read method to ApolloClient #1280
Conversation
Usage for this function would look like: client.read({
selection: gql`{ viewer { name } }`,
}); When reading from the root, or: client.read({
selection: gql`{ name }`,
id: client.dataId(viewer),
}); When reading from a given id. An alternative API would be: client.read(gql`{ viewer { name } }`);
client.read(gql`{ name }`, client.dataId(viewer));
client.read(gql`{ name }`, 'User-5'); However, this would make it harder to pass in the The latter API is definitely more beautiful, so we have to ask if we want to make these compromises to get the more beautiful API. |
Awesome! I'll take a look at this first thing tomorrow morning before I start implementing the write method 🙂 |
Also, what’s the procedure for updating the documentation? 😊 We may want to update the docs with both |
@calebmer There's no fancy procedure. You just pick the place where you think it should go, and start writing. If necessary, you edit the config file as well. This can probably go into the core-docs at first, but once we have the |
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! Just a few nitpicks about the last test.
test/readFromStore.ts
Outdated
}; | ||
|
||
const store = { | ||
'ROOT_QUERY': assign({}, assign({}, omit(result, 'nestedObj', 'deepNestedObj')), { |
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 is pretty hard to read, why not just write out the store like in other tests?
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 was just a copy paste of this test:
apollo-client/test/readFromStore.ts
Lines 155 to 246 in 5e944c1
it('runs a nested query with multiple fragments', () => { | |
const result: any = { | |
id: 'abcd', | |
stringField: 'This is a string!', | |
numberField: 5, | |
nullField: null, | |
nestedObj: { | |
id: 'abcde', | |
stringField: 'This is a string too!', | |
numberField: 6, | |
nullField: null, | |
} as StoreObject, | |
deepNestedObj: { | |
stringField: 'This is a deep string', | |
numberField: 7, | |
nullField: null, | |
} as StoreObject, | |
nullObject: null, | |
__typename: 'Item', | |
}; | |
const store = { | |
'ROOT_QUERY': assign({}, assign({}, omit(result, 'nestedObj', 'deepNestedObj')), { | |
__typename: 'Query', | |
nestedObj: { | |
type: 'id', | |
id: 'abcde', | |
generated: false, | |
}, | |
}) as StoreObject, | |
abcde: assign({}, result.nestedObj, { | |
deepNestedObj: { | |
type: 'id', | |
id: 'abcdef', | |
generated: false, | |
}, | |
}) as StoreObject, | |
abcdef: result.deepNestedObj as StoreObject, | |
} as NormalizedCache; | |
const queryResult = readQueryFromStore({ | |
store, | |
query: gql` | |
{ | |
stringField, | |
numberField, | |
nullField, | |
... on Query { | |
nestedObj { | |
stringField | |
nullField | |
deepNestedObj { | |
stringField | |
nullField | |
} | |
} | |
} | |
... on Query { | |
nestedObj { | |
numberField | |
nullField | |
deepNestedObj { | |
numberField | |
nullField | |
} | |
} | |
} | |
... on Query { | |
nullObject | |
} | |
} | |
`, | |
}); | |
// The result of the query shouldn't contain __data_id fields | |
assert.deepEqual(queryResult, { | |
stringField: 'This is a string!', | |
numberField: 5, | |
nullField: null, | |
nestedObj: { | |
stringField: 'This is a string too!', | |
numberField: 6, | |
nullField: null, | |
deepNestedObj: { | |
stringField: 'This is a deep string', | |
numberField: 7, | |
nullField: null, | |
}, | |
}, | |
nullObject: null, | |
}); | |
}); |
I don’t like it either, but it’s consistent.
test/readFromStore.ts
Outdated
@@ -646,4 +646,91 @@ describe('reading from the store', () => { | |||
computedField: 'This is a string!5bit', | |||
}); | |||
}); | |||
|
|||
it('will read from an arbitrary root id', () => { | |||
const result: any = { |
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.
result
might not be the best name here. Something like storeObjects
maybe?
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.
Renamed it data
. I don’t like it either, but this test is basically just a copy paste of another test in this file 😉
`, | ||
}); | ||
|
||
assert.deepEqual(queryResult1, { |
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 would put this right after you run the query, its' a bit easier to read that way.
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.
Sure. At some point I had a reason for doing it this way, but I can’t remember anymore.
I think we should have reads that start with an ID use a fragment, because otherwise the query won't validate if you are using something like As a bonus, if you use a fragment and specify the type, that will validate that you are querying fields that exist. |
I agree that we should expect an operation definition when no id is provided. However, when an id is provided and we expect a fragment, we'll have to enforce a convention for which fragment is to be used as the first selection set (because the fragment may itself contain fragments). Is it clear enough if we just always take the first fragment? We could also allow only a single fragment, but I think that would be too restrictive. An alternative would be to introduce a special I think the |
@stubailo For the fragment case I expected people to do something like: client.read(gql`
{ ...userFragment }
fragment userFragment on User {
name
}
`) …or use template interpolation: client.read(gql`
{ ...userFragment }
${USER_FRAGMENT}
`) I was not thinking about the I’m not against passing fragments directly, my one concern though is that a user may pass multiple fragments. At that point do we also provide the user a
@helfer What would that look like? |
You mean a special root field that only Apollo knows about? That seems much less clean than using a fragment (we could enforce that you call that fragment I'm currently pretty convinced that we should never encourage people to write GraphQL query strings on the client that aren't valid with regards to their server-side schema. |
Note that this will break basically any tool that expects to look at the client code and find valid queries, such as the recently released persist-graphql tool, every single editor integration, etc. |
I think it is ideal if we can use fragments, but we’d need to add a |
Personally I think a |
I don’t like the idea of implicitly using the first fragment. It feels like a source of issues. I’m all for automatically using the fragment if there is only one, though 👍 |
Yeah I suppose if we just print a nice error message when |
In the future, I think the ideal solution is to refactor const fragment1 = gql.fragment`fragment on Foo { ... }`;
const fragment2 = gql.fragment`fragment on Bar { ...${fragment1} }`;
const query = gql.operation`query { ...${fragment2} }`;
const document = gql`mutation { ... } query { ... }`;
client.readFragment(fragment1, 'Foo-4'); …or with imports: import { fragment1 } from './MyQuery.graphql';
client.readFragment(fragment1, 'Foo-4'); For now, though, what I did instead was break |
If we decide we like this pattern, then we should probably use it to split |
@calebmer: why does For writing to the store, the story is a bit more complicated, because we have to do that through Redux actions, we can't do it directly. |
I’m perfectly comfortable with six different methods (or more!) than trying to fit those six methods awkwardly into two 😊. It’s good API design because it’s monomorphic (so more performant), easier to type for TypeScript (so errors get caught before runtime), more sensible to a user (they know exactly when to use a fragment and when to use a query), and simpler to implement (I tried doing it the other way, and the combinatorial explosion of invariants got pretty crazy). We could pack it into one Redux action for writing to the store, though. The user would get a nice API and we would massage that API into one consistent write action. |
That's not the case:
Is a perfectly valid GraphQL fragment! |
Yeah there are dozens of pages of conversations somewhere about why variables are global per query, and apparently this was an intentional decision in the spec. |
I remember reading through the issue about adding explicit local fragment variables, and I remember seeing global fragment variables as an option. I never thought to check how variables were actually handled in fragments and assumed the specification punted that decision to a later point in time instead of doing the implicit global thing. |
Renaming the branch unfortunately closed this issue. I’m also going to add the |
I told you it was pretty easy @helfer 😊
The major thing this adds is the ability to start at any id from
readQueryFromStore
. There are now tests for that functionality.I did not add tests for the method on
ApolloClient
, yet, because it does not look like there is a test file for theApolloClient
class at the moment. I assume we will want one though when we addwrite
as well.