profusion / sgqlc

Simple GraphQL Client
https://sgqlc.readthedocs.io/
ISC License
511 stars 85 forks source link

Support for conditional querying #138

Open rohansharmaa opened 3 years ago

rohansharmaa commented 3 years ago

Sorry, I'm new to this. Is there a way to generate conditional queries like -

query reviews {
      Reviews(where: { id:  {_eq : <some number> } } ) {
          id
          review_text
      }
}

I'm only asking about the "where: { id : { _eq : } } "part, how do I add such conditions in my Operation ?

barbieri commented 3 years ago

These are called arguments in GraphQL world (knowing the name makes it easier to search).

Take a look at examples at https://sgqlc.readthedocs.io/en/latest/sgqlc.operation.html#selecting-to-generate-queries, in particular look for arguments and you'll find this one, among others:

>>> op.repository(id='repo1').__fields__(
...    issues={'title_contains': 'bug'}, # adds field and children
... )

This should also work:

>>> op.repository(id='repo1').issues(title_contains='bug')

Then in your case, the following should work:

op.reviews(where={'id': {'_eq': Variable('some_number') }}).__fields__('id', 'review_text')

Two advices:

rohansharmaa commented 3 years ago

This is what I tried,

class Query(Type):
    reviews = Field(reviews ,args={'where':dict})

op = Operation(Query)
rating = op.reviews(where={'id':{'_eq': 12}}).__fields__('id','review_text')

But it gave me a KeyError: <class 'dict'> followed by a TypeError: Not BaseType or mapped: <class 'dict'> Is dict not supported for arguments, or am I doing it wrong? I will also try the sgqlc-codegen operation as you suggested, but I'm just curious as to why the above method does not work.

barbieri commented 3 years ago

there isn't a mapping to a generic dict, you must create Input types for your where argument. Then you declare which keys do you expect, their types (including wrappers/modifiers such as list, non-null).

How is your server defining this? You need to use the same types and signatures in the client side, otherwise the server may reject it, in particular if you're using variables, any modifier mismatch will cause errors.

If you didn't write the server yet, I recommend you to go explicit and do not try to map a generic JSON object (you can't because all the object keys must be fixed, you'd have to create an array of key-value objects instead). Example schema:

input WhereIntRange {
  min: Int!
  max: Int!
}

input WhereInt {
  eq: Int
  lt: Int
  gt: Int
  le: Int
 ge: Int
 range: WhereIntRange
}

input ReviewsWhere {
  id: WhereInt
}

type Query {
   reviews(where: ReviewsWhere): [Review!]!
}
philon123 commented 3 years ago

@rohansharmaa I assume you are trying to do the same that I am - using Hasura as a backend. Just use the code generator to generate the "Query" part and then your Operation that you posted above should work fine.

So far I've had no trouble integrating sgqlc with Hasura. The code generator works like a charm and I'm successfully running queries. Regarding the syntax, @barbieri thanks for noting that the code generator can transform raw graphql queries to python code! It allows to use the Hasura documentation and GraphiQL to make a query and then see how it should look in gsqlc.

philon123 commented 3 years ago

Let me follow up on the original question. I found that it's possible to write Hasuras dict-like filter queries using the gsqlc syntax. See here two equivalent filter queries:

op.user(where={"height_cm": {"_gte": 200}})  # dict-syntax
op.user(where=schema.user_bool_exp(height_cm=schema.Int_comparison_exp(_gte=200)))  # gsql-syntax

I am wondering why the codegen doesn't generate the gsql-syntax but instead the dict-syntax? I am wondering which is better to use since the gsql-syntax is more type safe but the dict-syntax is easier to read/write.

barbieri commented 3 years ago

is this still an issue? Also, I highly recommend these kind of parameters to be set via Variables, so you give it a name that is used across the operation.

The actual value of the variable you send as a plain Python "JSON-like" object, the server will handle all the needed checks to avoid errors.

rohansharmaa commented 3 years ago

I was on a massive time crunch, so as of now I've manually defined all the queries as a string and using those. Once I find the time to experiment, I'll try out these suggestions and get back to you. Haven't had time to try out the codegen either., but since @philon123 has tried it out and it works fine for him, it should work for me too.

philon123 commented 3 years ago

The actual value of the variable you send as a plain Python "JSON-like" object, the server will handle all the needed checks to avoid errors.

I love the generated types of sgqlc, because it will not allow me to write a broken query. Basically - if it compiles, it should work. That's great and I tried to apply the thought to Hasuras complex arguments. Do you think it's better not to validate the json argument before sending to the server?

hungertaker commented 3 years ago

[PT-BR] @barbieri, pela localização acho que você é BR. Cara, parabéns pelo trabalho tá incrível. Veja se você pode me ajudar. A biblioteca funciona lisinha com várias operações do Hasura, limit, where, offset e diversas outras, mas não consigo de maneira nenhuma, implementar o order_by. Gerei o codegen direto da query para reproduzir de forma mais resumida. Agradeço imensamente pelo feedback.

[EN] @barbieri, by the location I think you are BR. Dude, congratulations on the job. It is incredible! See if you can help me. The library works smoothly with several Hasura operations, limit,where, offset and several others, but I am not in any way able to implement order_by. I generated the codegen directly from the query to reproduce in a more summarized way. Thank you so much for the feedback.

import sgqlc.types
import sgqlc.operation
import hasura_schema

_schema = hasura_schema
_schema_root = _schema.hasura_schema

__all__ = ('Operations',)

def query_my_query():
    _op = sgqlc.operation.Operation(_schema_root.query_type, name='MyQuery', variables=dict(order_by=sgqlc.types.Arg(sgqlc.types.list_of(sgqlc.types.non_null(_schema.binance_assets_order_by)), default={'id': 'asc'})))
    _op_binance_assets = _op.binance_assets(offset=10, order_by=sgqlc.types.Variable('order_by'))
    _op_binance_assets.fk_info_assets_id()
    _op_binance_assets.fiat()
    return _op

class Query:
    my_query = query_my_query()

class Operations:
    query = Query

""" AssertionError: 'id' (str) is not a JSON Object """
print(query_my_query())
Traceback (most recent call last):
  File "/home/hungertaker/Projects/pysura/graphql/introspection/teste.py", line 19, in <module>
    class Query:
  File "/home/hungertaker/Projects/pysura/graphql/introspection/teste.py", line 20, in Query
    my_query = query_my_query()
  File "/home/hungertaker/Projects/pysura/graphql/introspection/teste.py", line 12, in query_my_query
    _op = sgqlc.operation.Operation(_schema_root.query_type, name='MyQuery', variables=dict(order_by=sgqlc.types.Arg(sgqlc.types.list_of(sgqlc.types.non_null(_schema.binance_assets_order_by)), default={'id': 'asc'})))
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 2169, in __init__
    typ(default)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 973, in __new__
    return [realize_type(v, selection_list) for v in json_data]
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 973, in <listcomp>
    return [realize_type(v, selection_list) for v in json_data]
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 967, in realize_type
    return t(v, selection_list)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 949, in __new__
    return realize_type(json_data, selection_list)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 944, in realize_type
    return t(v, selection_list)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 2566, in __init__
    super().__init__(_json_obj, _selection_list)
  File "/home/hungertaker/Dev/ENVS/pysura/lib/python3.8/site-packages/sgqlc/types/__init__.py", line 1711, in __init__
    assert json_data is None or isinstance(json_data, dict), \
AssertionError: 'id' (str) is not a JSON Object

Process finished with exit code 1
barbieri commented 3 years ago

Hi @hungertaker , yes I'm also 🇧🇷 :-) But let's keep it in english so others may read it as well.

I need the description of binance_assets_order_by, given the traceback it should be an object (class ContainerType). Take GitHub's https://docs.github.com/en/graphql/reference/input-objects#repositoryorder it's a field (enum) and direction (also an enum)

barbieri commented 3 years ago

in your example, it looks wrong. If that was generated, also send me the operation so I can see what's wrong in my codegen, but:

order_by=sgqlc.types.Arg(
  sgqlc.types.list_of(sgqlc.types.non_null(_schema.binance_assets_order_by)), # list! 
  default={'id': 'asc'} # object, not a list...
)

If the variable is declared as $orderBy=[binance_assets_order_by!], the default should be [{'id': 'asc'}] or something like that.

hungertaker commented 3 years ago

Worked perfectly. Thank you very much. The abstraction you made from graphql to python, in the case of Hasura, which is a mirror of Postgres, allows to consult the entire endpoint, with a few classes. Soon I share with the community here some examples and ideas.

hungertaker commented 3 years ago

The query, codegen and schema.binance_assets_order_by:

query:

query MyQuery($order_by: [binance_assets_order_by!] = {id: asc}) {
  binance_assets(offset: 10, order_by: $order_by) {
    fk_info_assets_id
    fiat
  }
}

codegen

import sgqlc.types
import sgqlc.operation
import hasura_schema
import json

_schema = hasura_schema
_schema_root = _schema.hasura_schema

__all__ = ('Operations',)

def query_my_query():

    _op = sgqlc.operation.Operation(_schema_root.query_type, name='MyQuery', variables=dict(
        order_by=sgqlc.types.Arg(sgqlc.types.list_of(
            sgqlc.types.non_null(_schema.binance_assets_order_by)),
            default={'id': 'asc'}))) # Correct default=[{'id': 'asc'}]

    _op_binance_assets = _op.binance_assets(offset=10, order_by=sgqlc.types.Variable('order_by'))
    _op_binance_assets.fk_info_assets_id()
    _op_binance_assets.fiat()
    return _op

class Query:
    my_query = query_my_query()

class Operations:
    query = Query

schema.binance_assets_order_by

class binance_assets_order_by(sgqlc.types.Input):
    __schema__ = hasura_schema
    __field_names__ = ('fiat', 'fk_info_assets_id', 'id', 'last_update', 'name', 'pairs_by_quote_symbol_id_aggregate', 'pairs_aggregate', 'symbol')
    fiat = sgqlc.types.Field(order_by, graphql_name='fiat')
    fk_info_assets_id = sgqlc.types.Field(order_by, graphql_name='fk_info_assets_id')
    id = sgqlc.types.Field(order_by, graphql_name='id')
    last_update = sgqlc.types.Field(order_by, graphql_name='last_update')
    name = sgqlc.types.Field(order_by, graphql_name='name')
    pairs_by_quote_symbol_id_aggregate = sgqlc.types.Field('binance_pairs_aggregate_order_by', graphql_name='pairsByQuoteSymbolId_aggregate')
    pairs_aggregate = sgqlc.types.Field('binance_pairs_aggregate_order_by', graphql_name='pairs_aggregate')
    symbol = sgqlc.types.Field(order_by, graphql_name='symbol')
barbieri commented 3 years ago

yes, your query is wrong, I wonder it was passing the checks, but if you try to paste that in GraphiQL/Explorer it should fail. Could you check with the latest master? Now it's doing much more checks and I think it would point out the error.

hungertaker commented 3 years ago

Exactly. The query is wrong. I had tested it on Hasura's Graphi and as it passed I thought everything was ok. I generated codegen again with the [...] ones and everything worked out. I'm going to test it on the new master and I'm already giving the feeedback.

barbieri commented 3 years ago

@hungertaker could you confirm the current master reports the incorrect query?

barbieri commented 3 years ago

https://pypi.org/project/sgqlc/14.0/ is released with that fix, so you can use the package.

CalebEverett commented 3 years ago

I was trying to query a list of objects based on the property of a child object. Is it possible to query selection lists with this syntax?

op.repositories(owner='Alice').__fields__(
     issues={'title_contains': 'bug'}, # adds field and children
)
barbieri commented 3 years ago

it should be, isn't it working? could you paste the error or at least print the operation and paste the results?

Just notice this will only query issues, ok?

CalebEverett commented 3 years ago

HI, thank you. It is saying that the key doesn't exist. I put it into a colab here. I was using the introspection and code generation (really nice - thank you) and finding the generated filter object with that key for the object I was querying, but maybe the schema isn't implemented to perform that operation?

https://colab.research.google.com/drive/1p1UjF4qPLWNPK05s1dMRA34OMEu9ouN-#scrollTo=kGECcPjsQVVk

op = Operation(schema.Query)
positions = op.positions(first=2, where={"pool": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8"}).__fields__(
    tick_upper={"tick_idx_lte": 200000}
)
endpoint(op)
KeyError                                  Traceback (most recent call last)
<ipython-input-9-6102748212fb> in <module>()
      3     tick_upper={"tick_idx_lte": 200000}
      4 )
----> 5 endpoint(op)

7 frames
/usr/local/lib/python3.7/dist-packages/sgqlc/types/__init__.py in __to_graphql_input__(self, values, indent, indent_string)
   2391             args = []
   2392             for k, v in values.items():
-> 2393                 p = self[k]
   2394                 args.append(p.__to_graphql_input__(v))
   2395             s.extend((', '.join(args), ')'))

KeyError: 'tick_idx_lte'

I also tried this formulation:

op = Operation(schema.Query)
op.positions(
    first=2,
    where={"pool": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "tick_upper": {"tick_idx_lte": 200000}}
)
endpoint(op)

and got back this error:


op = Operation(schema.Query)
op.positions(
    first=2,
    where={"pool": "0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8", "tick_upper": {"tick_idx_lte": 200000}}
)
endpoint(op)
GraphQL query failed with 1 errors
{'errors': [{'locations': [{'column': 93, 'line': 2}],
   'message': 'Unexpected `"tick_idx_lte"[StringValue]`\nExpected `Name`, `:` or `}`'}]}
barbieri commented 3 years ago

Well, I just did the introspection here and checked it, you can search for Position_filter, both in the json or the generated Python file. The tick_upper is a simple string, other similar fields are:

class Position_filter(sgqlc.types.Input):
    # ...
    tick_upper = sgqlc.types.Field(String, graphql_name='tickUpper')
    tick_upper_not = sgqlc.types.Field(String, graphql_name='tickUpper_not')
    tick_upper_gt = sgqlc.types.Field(String, graphql_name='tickUpper_gt')
    tick_upper_lt = sgqlc.types.Field(String, graphql_name='tickUpper_lt')
    tick_upper_gte = sgqlc.types.Field(String, graphql_name='tickUpper_gte')
    tick_upper_lte = sgqlc.types.Field(String, graphql_name='tickUpper_lte')
    tick_upper_in = sgqlc.types.Field(sgqlc.types.list_of(sgqlc.types.non_null(String)), graphql_name='tickUpper_in')
    tick_upper_not_in = sgqlc.types.Field(sgqlc.types.list_of(sgqlc.types.non_null(String)), graphql_name='tickUpper_not_in')
    tick_upper_contains = sgqlc.types.Field(String, graphql_name='tickUpper_contains')
    tick_upper_not_contains = sgqlc.types.Field(String, graphql_name='tickUpper_not_contains')
    tick_upper_starts_with = sgqlc.types.Field(String, graphql_name='tickUpper_starts_with')
    tick_upper_not_starts_with = sgqlc.types.Field(String, graphql_name='tickUpper_not_starts_with')
    tick_upper_ends_with = sgqlc.types.Field(String, graphql_name='tickUpper_ends_with')
    tick_upper_not_ends_with = sgqlc.types.Field(String, graphql_name='tickUpper_not_ends_with')

You can confirm in JSON:

            {
              "defaultValue": null,
              "description": null,
              "name": "tickUpper",
              "type": {
                "kind": "SCALAR",
                "name": "String",
                "ofType": null
              }
            },
            {
              "defaultValue": null,
              "description": null,
              "name": "tickUpper_not",
              "type": {
                "kind": "SCALAR",
                "name": "String",
                "ofType": null
              }
            },

The only occurrence of tickIdx_lte (tick_idx_lte) is in Tick_filter:

            {
              "defaultValue": null,
              "description": null,
              "name": "tickIdx_lte",
              "type": {
                "kind": "SCALAR",
                "name": "BigInt",
                "ofType": null
              }
            },

And Tick_filter is used in Pool.ticks, Query.ticks, Subscription.ticks... none in Positions.

CalebEverett commented 3 years ago

Thanks for taking a look and that helps for looking at the schema to see what is possible in the future. Looks like the filter I need is not implemented.