Hey there,
As of now, most GraphQL adaptators aren't taking full advantage of one of GraphQL's biggest benefit: being able to query nested fields in one request, as deep as the schema allow us to.
Instead, they're more or less imitating the way REST works, by making X request for X reference needed (although some great optimizations were already done in that regard https://marmelab.com/blog/2016/10/18/using-redux-saga-to-deduplicate-and-group-actions.html)
I think there are ways we could improve that, and I suggest we use this place as a central thread to discuss about what could be done.
Include all scalar fields of linked types by default
As of now, we're only fetching ids from linked types, which forces us to use <ReferenceField /> on every linked type.
const linkedResource = introspectionResults.resources.find(
r => r.type.name === type.name
);
if (linkedResource) {
return { ...acc, [field.name]: { fields: { id: {} } } };
}
One quick-fix that could be done, is to automatically fetch all scalar fields of linked types:
const linkedResource = introspectionResults.resources.find(r => r.type.name === type.name);
if (linkedResource) {
// Fetch all linked resource scalar fields
const linkedScalarFields = linkedResource.type.fields
.filter(field => getFinalType(field.type).kind !== TypeKind.OBJECT)
.reduce((acc, field) => ({ ...acc, [field.name]: {} }), {});
return { ...acc, [field.name]: { fields: linkedScalarFields } };
}
This way, we're able to use <TextField source="linkedType.scalarField" />, which already cover lots of cases and greatly improve the amount of request made.
I think the few more bytes that those additional fields will take on every requests are more than worth the amount of request that it will save.
Make this overridable
After thinking about it, @fzaninotto, I don't think there's a need for a function to transform the introspectionResults. The introspection query already fetches all the existing types (and their fields) anyway.
If I'm not mistaking, overriding the behavior described above is actually already possible and that's what you've done here, by overriding the query that fetches the Command.
I think we could provide a simpler and more concise API to do that, for two reasons:
The query name and the params are not needed for what we're trying to achieve. The params will always have to be declared anyway, and it can become heavy when overriding GET_LIST actions (having filters, sorting and pagination as input).
Users have to be careful about the aliases (data and sometimes total)
If you agree with me, I see two ways of enhancing this:
const COMMAND_FIELDS = `{
id
reference
product { id name }
}`
graphqlify expects (more consistent, but heavier IMO)That's all for now, tell me what you guys think 馃檶
Hi @Weakky, thanks a lot for your feedback. The truth is, we don't have much hindsights on the graphql providers yet.
Instead, they're more or less imitating the way REST works, by making X request for X reference needed
Indeed. We opted for this in order to get a proof of concept for graphql providers. There is room for improvements!
Include all scalar fields of linked types by default
I don't think we should as it would defeat another benefit of graphql, only fetching what you need. We can't make that decision for you
Make this overridable
It is already. In fact, you can disable the introspection altogether and provide queries for each requests of each resources. One of our partners is actually doing it. Overly simplifying, they use a dataProvider such as:
import buildGraphQLProvider from 'ra-data-graphql'
const buildQuery = (raFetchType, resourceName, params) => {
switch (raFetchType) {
case GET_LIST: {
switch (resourceName) {
case 'Comments': {
return {
gql: gql`query Comments ($pagination: PaginationInput!, $sort: SortInput!, $filter: ContenuFilterInput){
data: comments (pagination: $pagination, sort: $sort, filter: $filter){
data {
id
body
}
total
}
}`,
variables: params,
parseResponse: response => response.data
}
}
}
}
}
};
buildGraphQLProvider({
client: apolloClient,
buildQuery,
introspection: false
});
The way I see it, you should use the introspection enabled dataProvider for prototyping, switching progressively and selectively to hand crafted queries when needed
I agree with @Weakky that overriding the entire query just to add a related resource might come in costly. We have to find something easier.
By the way, the override option doesn't seem to be documented.
What override option ?
Thanks for the fast response !
I don't think we should as it would defeat another benefit of graphql, only fetching what you need. We can't make that decision for you
I agree with you, but unless we find a way to generically detect which linked fields have to be fetched (although according to @fzaninotto, that'd be really hard because of react-admin's current implementation), we will have to make tradeoffs.
And I think it's a fair tradeoff to batch potentially perPage requests per <ReferenceField />. (25 resources with 2 <ReferenceField /> = 50 potential requests made), by adding a few more fields to every requests. I understand it's all about preference though here 馃槂
Invalid argument, you're batching all CRUD_GET_ONE_REFERENCE to one single GET_MANY using the trick linked above
The way I see it, you should use the introspection enabled dataProvider for prototyping, switching progressively and selectively to hand crafted queries when needed
Totally agree with you here. Detect the hot-spots, and override these queries by handcrafted one when needed. Hence my suggestion to simplify the current API to override the queries, so that it can be done in a more concise way !
Propositions:
override option which I totally forgot and is redundant with decorating the dataProvider to bypass its features when neededqueryBuilder to return any of the following:String: We'll pass it to the gql string literal. I think it's not so nice as our users will loose syntax highlighting provided by many IDE for calls to gql but we could support it. We should then passes the params as variables and provide a default parseResponse for themgql). Same as before, we should then passes the params as variables and provide a default parseResponse for themparams or parse the response, they may provide the same object as beforeWhat do you think ?
Apologies but I'm not sure to understand how we'd be able to decorate the dataProvider to override some queries here.
Let's assume we're talking about ra-data-graphcool. From a 'provider consumer's point of view', what would it look like if I wanted to override the same query as you did below, using the more concise option among the three that you're proposing ?
No problem, I should have taken more time to think about it in the first place :)
There are two ways I think:
override option and make it usable as I described. It would look like the example posted by @fzaninotto but simplified// in src/buildDataProvider.js
import buildApolloClient from 'ra-data-graphcool';
export default () =>
buildApolloClient({
clientOptions: {
uri: 'https://api.graph.cool/simple/v1/cj2kl5gbc8w7a0130p3n4eg78',
},
override: {
Command: {
GET_ONE: (params) => ({
query: gql`...`, // You could get syntax highlighting here depending on your IDE
// Optionally provide variables if you need to work on them for this specific resource/fetchType
variables: params,
// Optionally provide parseResponse if you need to work on it for this specific resource/fetchType
parseResponse: response => response.data
}),
// Or
GET_ONE: gql`...`, // You could get syntax highlighting here depending on your IDE
// Or
GET_ONE: `...`, // No syntax highlighting here as this is a simple string :(
},
},
});
buildQuery function from the dialect aware dataProviders such as ra-data-graphcool so that you can decorate it:// in src/buildDataProvider.js
import buildApolloClient, { buildQuery } from 'ra-data-graphcool';
const myBuildQuery = buildQuery => (aorFetchType, resource, params) => {
const builtQuery = buildQuery(aorFetchType, resource, params);
if (resource === 'Category' && aorFetchType === 'GET_ONE') {
return {
...builtQuery,
query: gql`...`, // Or a string directly
};
}
};
export default () =>
buildApolloClient({
clientOptions: {
uri: 'https://api.graph.cool/simple/v1/cj2kl5gbc8w7a0130p3n4eg78',
},
buildQuery: myBuildQuery(buildQuery),
});
I personally prefer the second option
Thanks for the examples, it's much clearer now. I prefer the second option as well. It'd allow us to mimic the override object anyway if we wanted to, and is much permissive regarding what consumers can do with it.
One thing though, unless I missed something, I don't get how the final query will be crafted in case we pass a simple string ?
What I mean is that on your example, assuming ra-data-graphql still passes the query as-is to apollo-client, it means that the apolloArgs and the args will be missing on the query ?
Besides, even if we add some kind of query processing in ra-data-graphql (which I think is not its job anyway), it will not be aware of how the query should be crafted.
The way I saw it, although I'd prefer decorating the dataProvider as well, was to pass the override option directly to ra-data-graphcool (for example), and let it process the overriden queries from within ra-data-graphcool. This way, it was just a matter of overriding the buildGqlQuery::buildFields() function while preserving how buildApolloArgs() and buildArgs() worked.
I may be wrong here though 馃
What I mean is that on your example, assuming
ra-data-graphqlstill passes thequeryas-is toapollo-client, it means that theapolloArgsand theargswill be missing on the query ?
No, you missed this part (I added comments to make it clearer):
return {
...builtQuery, // Merge the default builtQuery (variables and parseResponse)
query: gql`...`, // Or a string directly
};
It also means that ra-data-graphql will check for the query type and passes to the gql function if it's a string. I still think passing a string is not that great as you'll loose syntax highlighting in your IDE when not using the gql function
I think you missed what I meant as well, let me rephrase it 馃槢. I get that the only param overriden on your example will be the query, but my issue is precisely on the query.
Here's what a valid apollo-client query might look like:
query shops(
$where: ShopWhereInput
$orderBy: ShopOrderByInput
$skip: Int
$first: Int
) {
items: shops(where: $where, orderBy: $orderBy, skip: $skip, first: $first) {
id
name
address
}
total: shopsConnection(
where: $where
orderBy: $orderBy
skip: $skip
first: $first
) {
aggregate {
count
}
}
}
If we override the query param by something like this:
return {
...builtQuery,
query: `{ id name address }` //Same API as GraphQL Bindings
}
All the apolloArgs will be missing here. Even though we still pass the variables, apollo-client will have nothing to match those params ($where, $orderBy etc..).
Now, I totally get your point regarding the loose of syntax highlight.
My solution is a trade-off between the pain it is to have to always declare those apollo args and be careful about the aliases to match the parseResponse() function, and the loose the syntax highlight.
I think we were just not on the same line from the beginning.
If the goal of using a simple string is only to remove the use of gql, then there's no point, I agree with you.
Here's what you might be able to achieve using GraphQL Bindings):
// Retrieve `firstName` and `lastName` of a specific user
binding.query.user({ where: { id: 'user_id' } }, '{ firstName lastName }');
(Bindings either accept the query AST passed in all resolvers (known as the info param) OR such a string if you need to customize what fields you want to query)
This would produce the following GraphQL query:
{
user(where: { id: 'user_id' }) {
firstName
lastName
}
}
Think about it as a GraphQL Fragment. If you look closely to graphql-bindings implementation, this is actually how they're treating that param.
In the example above, { firstName lastName } is equivalent to fragment tmp on User { firstName lastName } (which is a valid GraphQL input)
In a nutshell, the 'simple string' option would be a way to only express the fields user wants to fetch, removing all the apollo-client boilerplate that we don't need anyway.
If you do agree with me now (and I'd understand if you didn't like it at all 馃槥), best-case scenario would be to allow all three options like you proposed before.
But then again, your original proposition would not allow to do what's shown above 馃憪
All the apolloArgs will be missing here. Even though we still pass the variables, apollo-client will have nothing to match those params ($where, $orderBy etc..).
Those are the standard parameters of the query matching the LIST fetch type for prisma ? If yes, the ra-data-prisma should have translated them already. If not, you could override the variables as we did for the query. Have I missed something ?
As much as I personally like the graphql bindings syntax, I fail to see what you'd like ra-data-graphql to support here. Could you describe the API of your dream for ra-data-graphql?
Those are the standard parameters of the query matching the LIST fetch type for prisma ?
Yes they're, but prisma isn't relevant here. I just wanted to highlight apolloArgs in general.
If yes, the ra-data-prisma should have translated them already. If not, you could override the variables as we did for the query. Have I missed something ?
Indeed, but on your proposition, we're overriding the whole query, meaning passing a simple GraphQL fragment as shown above won't work because the apolloArgs will be missing from the query. Even overriding the variables won't change anything, the only valid query that we could pass using your solution would be a full query, including the apolloArgs.
My original suggestion was about providing a more succint API to override queries, hence our disagreement here because decorating the provider would not provide any more concise API.
As much as I personally like the graphql bindings syntax, I fail to see what you'd like ra-data-graphql to support here. Could you describe the API of your dream for ra-data-graphql?
My initial statement is about GraphQL providers in general, and not specifically about ra-data-graphql, that's where I think we lost ourselves, apologies.
Although I understand ra-data-graphcool and ra-data-graphql-simple are only examples, I think it'd be great to improve their API as well to provide better starting point for future GraphQL providers.
override params from ra-data-graphql as you suggested, we can indeed override the queries by decorating the buildQuery function.query to be of type gql object, I don't think there's a need for allowing raw strings.Add a new overrideQueriesByFragment option to ra-data-graphcool and ra-data-graphql-simple, allowing to override queries using simple fragments, in the same way that the current override option works:
import buildApolloClient from 'ra-data-graphcool';
export default () =>
buildApolloClient({
clientOptions: {
uri: 'https://api.graph.cool/simple/v1/cj2kl5gbc8w7a0130p3n4eg78',
},
overrideQueriesByFragment: {
Command: {
// Not sure yet whether the params could be of any use here.
GET_ONE: (params) => '{ id firstName }',
// If not, this could be enough
GET_ONE: '{ id firstName }',
},
},
});
Below is what could be a straightforward implementation to convert such a fragment into something that graphlify understands:
(Optional) Inject the whole schema into the introspectionResults. We already have one use-case: being able to validate the fragments against the schema
import { validate } from 'graphql'
const document = parse('fragment tmp on User { firstName lastName }');
validate(schema, document);
graphql-js reference implementation is a treasure-chest when it comes to utils functions, there might be tons of other usecases that people could use thanks to the schema.
What do you think ?
I think I'm really lost now :laughing:
We at least agree on that :smile:, except that decorating the provider is actually not low level. As for the REST providers, this can be used to add features around the provider (like the addUploadFeature in the documentation) but that's not important
This is where I got lost. What is achieved by this overrideQueriesByFragment that cannot be achieved by decorating the buildQuery? Is your example correct?
Make sense, we could add the whole schema to the introspection results
I just roughly implemented the query overriding by fragments on a branch on ra-data-prisma for you to see how I'd implement what I'm suggesting https://github.com/Weakky/ra-data-prisma/pull/7 馃槂
Ok, I think I get what you're doing now. Basically, you want a way to override a query without having to write the query boilerplate with parameters, etc.
I think the overrideQueriesByFragment is cumbersome to use though. Taking your repository product as an example, I suggest doing something like the following:
// in src/buildDataProvider.js
import buildApolloClient, { buildQuery } from 'ra-data-graphcool';
const productGetListQuery = gql`
fragment product on Product {
id
name
description
brand {
id
name
}
category {
id
name
}
shop {
id
name
}
attributes {
id
value
}
}
`;
const myBuildQuery = buildQuery => (aorFetchType, resource, params) => {
if (resource === 'Category' && aorFetchType === 'GET_ONE') {
// buildQuery accepts an extra parameter which is the query/fragment to use as an override
// The rest of your logic still applies but you wont have to find the override
return buildQuery(aorFetchType, resource, params, productGetListQuery);
}
return buildQuery(aorFetchType, resource, params);
};
export default () =>
buildApolloClient({
clientOptions: {
uri: 'https://api.graph.cool/simple/v1/cj2kl5gbc8w7a0130p3n4eg78',
},
buildQuery: myBuildQuery(buildQuery),
});
Besides, I assume the only thing we have to do on our side is to include the full schema in the introspectionResults so that you can validate fragments, etc. Right?
We may then steal your code for our own providers :stuck_out_tongue:
Ok, I think I get what you're doing now
You should've seen my smile reading those sweet words 馃槀
I think the overrideQueriesByFragment is cumbersome to use though. Taking your repository product as an example, I suggest doing something like the following
This is fine for me. Your way of doing it is more consistent as you're planning to remove the override option. Everything will have to be done by decorating the provider, offering more freedom.
I'll be able to use my cumbersome data structure anyway to avoid those ugly if statements 馃槃
Besides, I assume the only thing we have to do on our side is to include the full schema in the introspectionResults so that you can validate fragments, etc. Right?
We may then steal your code for our own providers
Exactly ! Let me recap everything:
override option from ra-data-graphqlschema into the introspectionResults (other usecases might be found in the future)I might find some time to send a few PR's 馃帀
Closing this issue as the first part has been merged and will be available in 2.3.0
Most helpful comment
You should've seen my smile reading those sweet words 馃槀
This is fine for me. Your way of doing it is more consistent as you're planning to remove the
overrideoption. Everything will have to be done by decorating the provider, offering more freedom.I'll be able to use my cumbersome data structure anyway to avoid those ugly if statements 馃槃
Exactly ! Let me recap everything:
overrideoption fromra-data-graphqlschemainto theintrospectionResults(other usecases might be found in the future)I might find some time to send a few PR's 馃帀