-
Notifications
You must be signed in to change notification settings - Fork 33
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
Feature request: Allow user-specified queries #132
Comments
Is this a practical observation from a project you've build with the Codegen? It's true that Ariadne Codegen limits your client only to queries you have pre-defined for it, but its a trade off we've did based on prior experience in consuming GraphQL APIs from Python services, where most often than not following were true:
You can call this approach reductio at RESTum but observation that gains from GraphQL's flexibility are limited when using it in service 2 service communication are nothing new. I've did first multi-service project that used GraphQL before Ariadne even existed (in first half of 2018) and we've quickly ended with set of functions that did few pre-defined queries because that was actually better developer experience than specifying the fields every time I've wanted to pull the data from other API. This is sort of thing you see with ORM's. You can pull a subset of columns from a table, but most of time everything is better. We could probably support generic queries like this (an escape hatch like how it is in ORMs) but I am worried this is a lot of extra work that developers would eventually walk away from as they discover that their business logic is easier to write with a finite and limited number of models. |
We haven't actually built anything with Codegen yet, we are evaluating it as a possible solution, alongside alternatives like https://github.com/graphql-python/gql and https://github.com/profusion/sgqlc However, our particular scenario is that we have a backend server utilizing GraphQL, and we have many different users accessing this API, for a variety of different purposes (populating UI's, generating reports, fetching data to sync into other systems). We don't have control over the specific details of the queries they could be executing.
The only use case in which I could envision a friction-less workflow for responding to changing query needs is the scenario where the backend developers and frontend developers are the same people -- that is, the developers using the Client library and the developers building the GraphQL schema are either one team or collaborate extremely closely. So maybe this works in service-to-service communication, like you describe. But if you are simply wanting to provide a Pythonic client library that wraps the HTTP mechanics of communicating with a GraphQL server in a generic and extensible way, ariadne-codegen seems inadequate. The design changes I was suggesting were aimed at making this library cater to a wider range of scenarios. In the end, we may end up having to go with a library like https://github.com/graphql-python/gql since this does allow dynamic construction of queries, but we will be sacrificing the ability to have full mypy typechecking, which I would consider to be a great loss. |
@dkbarn In our experience using |
API proposal: result = await client.query(
Query.user(id="123").fields(
User.id,
User.email,
User.group.fields(UserGroup.id),
User.reviews(category="123"),
),
Query.user(id="fsadsa").alias("wohoo").fields(
User.id,
User.email,
User.group.fields(UserGroup.id),
User.reviews(category="123"),
),
Query.search(query="test").on(
"User",
User.id,
User.email,
).on(
"Thread",
Thread.id,
Thread.title,
),
operation_name="HelloWorld"
)
Just a caveat tho: those generated types would be used only as safety net for query building. I don't think we would be able to generate types for |
Yes this looks like what we're after! Regarding your caveat about return types, would the type annotation have to be "Any" then? Also, I'm not sure I completely understand this bit:
What is "search" and "on" in this context? What does that translate to in GraphQL? |
I think so, but I am not familiar with what Mypy plugins would make possible. Above would produce the following GraphQL query: query HelloWorld {
user(id: "123") {
id
email
group {
id
}
reviews(category: "123")
}
wohoo: user(id: "123") {
id
email
group {
id
}
reviews(category: "123")
}
search(query: "test") {
.. on User {
id
email
}
.. on Thread {
id
title
}
}
} |
In your example code above, would the Assuming that the class User(pydantic.BaseModel):
id: int
email: str
Query.user(id="123").fields(
User.id,
User.email,
)
# this would throw
# AttributeError: type object 'User' has no attribute 'id' |
I doubt those ORM classes would be pydantic models, or that they would return anything else than lists of dicts.
Even if we went with classes, those would be separate class UserData(ariadne_codegen.orm):
id: str | None = None
username: str | None = None
group: UserGroupData | None = None |
...but seeing how we already require Pydantic, I don't think there would be an issue with having those data types as it's models instances as you've originally proposed. My only worry here is that the code will have to be littered with |
Bumping this as we use Strawberry for covering our graphql needs but we are trying to move away from OpenAPI Generator and want to use something that can generate async graphql clients instead. We've hit a roadblock here as we have a lot of queries that we couldn't possibly manually define into a |
@nevoodoo I'm working on a solution and will have it ready soon. |
@DamianCzajkowski we're eager to see what sort of solution you've come up with! Roughly when do you think there might be a pull request to look at? |
@dkbarn For now it looks like this: async with SaleorClient(saleor_url=saleor_url, api_key=None) as saleor_client:
query_str = [
Query.products(first=1, channel="default-channel").fields(
ProductCountableConnectionFields.edges().fields(
ProductCountableEdgeFields.node().fields(
ProductFields.id,
ProductFields.slug,
),
ProductCountableEdgeFields.cursor,
),
),
Query.products(first=10, channel="default-channel")
.alias("my_products")
.fields(
ProductCountableConnectionFields.edges().fields(
ProductCountableEdgeFields.node().fields(
ProductFields.id,
)
),
),
]
result = await saleor_client.query(
*query_str,
operation_name="getProducts",
) Right now I'm struggling with response types and typing on them. I'm testing my solution on really complicated schema from Saleor and I see performance issues with Pydantic models, import takes about 1.8 seconds for 81 models in one file (they are all dependent of themselves). I'm thinking of using dataclasses (0.057s import) but it doesn't have fields validation. I was thinking to use manually generated field validation (as simple as possible). What do you think guys? what will be the best solution for you right now, to have typed responses? or should I abandon this idea in first version of query builder? |
Hi, I've published experimental version of custom query builder in ariadne-codegen. It is a pre-release from dev branch. |
I'd like to do some testing of this. I installed the dev version but the client I generated looks the same as the one generated with stable version of It also seems all of the stuff is in the |
@jukiewiczm |
I've done some testing and here's my input. First of all, great piece of work! I actually believe this feature will actually become the most exciting one in the project.
That's the schema I'm given, so I refrained from assessing whether this is a bad schema example, I guess stuff like this will appear and should work though. Here are the adjustments def _convert_value(
self, value: Any
) -> Union[StringValueNode, IntValueNode, FloatValueNode, BooleanValueNode, ObjectValueNode]:
if isinstance(value, str):
return StringValueNode(value=value)
if isinstance(value, int):
return IntValueNode(value=str(value))
if isinstance(value, float):
return FloatValueNode(value=str(value))
if isinstance(value, bool):
return BooleanValueNode(value=value)
if isinstance(value, BaseModel):
fields = [
ObjectFieldNode(name=NameNode(value=field_info.alias or field_name), value=self._convert_value(field_value))
for field_name, field_info in value.model_fields.items()
if (field_value := getattr(value, field_name)) is not None
]
return ObjectValueNode(fields=fields)
if isinstance(value, UUID): # Guess we'd need something more sophisticated for all custom scalar types
return StringValueNode(value=str(value))
raise TypeError(f"Unsupported argument type: {type(value)}") Regarding response types you mentioned in the other comment, do you mean the type for For the time being, I guess something like that could work (simplified example):
and used like this
|
Hi @jukiewiczm, |
Unfortunately I can't do that (due to obvious reasons - work stuff). Over the weekend I'll try to cut out and anonymize part of it and share it with you. That should be enough to cover all these use cases besides the slow loading, which you're experiencing yourself so I guess it should be enough :) Optionally, I'll try to generate stuff that doesn't work using the Saleor schema, looking through it, it might also be possible. |
@DamianCzajkowski Here's the folder that contains it all. You can review the commits I added to see the progress of fixing stuff for query in this file that initially did not work. It doesn't reproduce:
Hope this helps! |
Hi guys, Note: Future improvements:
|
The fact that this library requires pre-defining the queries that will be exposed through the Client class seems incredibly limiting. The power of GraphQL is its flexibility -- by default, the entirety of a data model is retrievable through the edges on the root Query type. But this power and flexibility is lost when using this library, because the Client class only allows fetching the data and the fields that have been baked into a finite list of canned queries. In a sense, the end result is much more like interacting with an old-school REST API than a flexible GraphQL API.
Certain design decisions made in this library are very useful -- the use of pydantic, the auto-generation of Python dataclasses and enums from GraphQL types and enums, the support for full mypy typechecking. But what would make this library far more useful is if it exposed the full GraphQL schema, by allowing the user to fully specify the return fields of a query, rather than restricting them to a selection of pre-defined queries.
For example, given this schema:
The following classes would be auto-generated:
Queries could then be executed via the Client class like this, with support for fully specifying the return fields:
(Note: the exact syntax used to specify the return fields here would not actually work, but the point is there is some way of specifying the return fields)
The key differences are:
Optional
to allow for these sparsely-populated objects.The text was updated successfully, but these errors were encountered: