dapalex / py-graphql-mapper

A python library for generating GraphQL queries and mutations using plain python objects, no hardcoded strings.Any questions or issues, please report here https://github.com/dapalex/py-graphql-mapper/issues
MIT License
20 stars 2 forks source link

No support for List return types #21

Closed indraneelmax closed 1 year ago

indraneelmax commented 1 year ago
@strawberry.type
class Query:
    list_users: List[User] = strawberry.field(resolver=resolve_list_users)

For a query schema like the above it generates Python objects like -

# from generated queries.py
class listUsers(GQLQuery):
   class UserArgs(GQLArgsSet, GQLObject):
      name: NonNull_str
      login: NonNull_str
      aggregateId: NonNull_str
      limit: NonNull_int

   _args: UserArgs

   type: User

# below from generated gql_types.py

class User(GQLObject):
   aggregateId: str
   login: str
   name: str

The type should be List[User] instead of just User.

Using the query python class listUsers to fetch users works fine but when we try to convert the result into python objects (via GQLResponse.map_gqldata_to_obj) it fails to give correct data throwing below errors as builder.QueryBuilder.set_py_fields expects dataInput to be adict instead of a list as in this case.

ERROR:root:Setting value for element aggregateId failed - 'list' object has no attribute 'keys'
ERROR:root:Setting value for element login failed - 'list' object has no attribute 'keys'
ERROR:root:Setting value for element name failed - 'list' object has no attribute 'keys'

Is this expected or am I doing something wrong?

dapalex commented 1 year ago

Hi indraneelmax,

Thanks for pointing out your issue.

I have been looking into the tests and actually there are many cases where the code generator have created a class representing a query with args and a list of objects as payload.

In your case it should have created a type list_user[User] where list_user is a class inheriting list.

To expose you some concrete examples taken from version 1.1.0 (present on pip):

Given a query type like this into the RapidApi schema:

{
            "name": "extensions",
            "description": "Get extensions for an API",
            "args": [
              {
                "name": "client",
                "description": null,
                "type": {
                  "kind": "SCALAR",
                  "name": "String",
                  "ofType": null
                },
                "defaultValue": null
              },
              {
                "name": "page",
                "description": null,
                "type": {
                  "kind": "SCALAR",
                  "name": "String",
                  "ofType": null
                },
                "defaultValue": null
              },
              {
                "name": "path",
                "description": null,
                "type": {
                  "kind": "SCALAR",
                  "name": "String",
                  "ofType": null
                },
                "defaultValue": null
              }
            ],
            "type": {
              "kind": "LIST",
              "name": null,
              "ofType": {
                "kind": "OBJECT",
                "name": "Extension",
                "ofType": null
              }
            },
            "isDeprecated": false,
            "deprecationReason": null
          },

ref: https://github.com/dapalex/py-graphql-mapper/blob/1.1.0/tests/cli_input/rapid_api/schema.json#L556C12-L556C12

A GQLQuery class gets created:

class extensions(GQLQuery):
   class ExtensionArgs(GQLArgsSet, GQLObject):
      client: str
      page: str
      path: str

   _args: ExtensionArgs

   type: list_Extension[Extension]

ref: https://github.com/dapalex/py-graphql-mapper/blob/1.1.0/tests/output/rapidapi_nodesc/queries.py#L241

The Extension class:

class Extension(GQLObject):
   id: ID
   name: str
   description: str
   title: str
   url: str
   sourceUrl: str
   sourceType: str
   slugifiedKey: str
   isEnabled: bool
   loggedInRequired: bool
   path: str
   extensionConsumers: ExtensionConsumer
   createdAt: DateTime
   updatedAt: DateTime
   deletedAt: DateTime
   topic: str
   order: int

ref: https://github.com/dapalex/py-graphql-mapper/blob/1.1.0/tests/output/rapidapi_nodesc/gql_types.py#L3721C1-L3739C14

And the list_Extension class inheriting list as Payload:

class list_Extension(list, Extension): pass

ref: https://github.com/dapalex/py-graphql-mapper/blob/1.1.0/tests/output/rapidapi_nodesc/queries.py#L11C1-L11C44

Unluckily, there is only one case in Github API similar to your situation:

Query codesOfConduct: https://github.com/dapalex/py-graphql-mapper/blob/1.1.0/tests/output/github_nodesc/queries.py#L26

The list as payload: https://github.com/dapalex/py-graphql-mapper/blob/1.1.0/tests/output/github_nodesc/queries.py#L9

Class codeOfConduct: https://github.com/dapalex/py-graphql-mapper/blob/1.1.0/tests/output/github_nodesc/gql_simple_types.py#L1102

There are other cases in the tests with the same mapping logic.

My first guess is that in your situation there could be some different way of defining the type that I didn't encounter neither managed, I'll try to dig into the process to see if I can find any flaw, in the meanwhile I would ask you more details if you can, among them:

Thanks again

indraneelmax commented 1 year ago

Thank you for your quick response.

Also, I am using https://strawberry.rocks/docs/guides/tools#merge_types

Query = merge_types(
    "Query",
    (
        user_graphql.Query,
        test_graphql.Query,
    ),
)
Mutation = merge_types(
    "Mutation",
    (
        user_graphql.Mutation,
        test_graphql.Mutation,
    ),
)

Thank you for sharing those examples. I also wanted to point out -

dapalex commented 1 year ago

Hi indraneelmax,

Thanks for the prompt feedback.

For the last issue regarding the python classes not generated following the name I am not exactly sure of what you mean. Right now the queries get fetched from the fields within a type of kind OBJECT with name "Query".

.................
"types": [
      {
        "kind": "OBJECT",
        "name": "Query",
        "description": null,
        "fields": [
          {
.................

Do you mean parameterizing the name following the name in queryType?

.................
"__schema": {
    "queryType": {
      "name": "Query"
    },
.................

For the main issue I have found actually a bug during the construction of the main schema python class, in the next few days I should be able to release a new version with the fix. So for the example you noticed the query licenses will have to be:

class licenses(GQLQuery):
   """
   licenses - Return a list of known open source licenses

   """
   type: NonNull_list_License[License]

This will probably solve your issue with listUsers query as well.

Note: The example you showed about License [e58e1ad] refers to an older version (1.0.0 in January), the last version is 1.1.0 (February)

dapalex commented 1 year ago

I pushed the fix in the feature branch associated with this issue, if you want to try it now you can use this https://github.com/dapalex/py-graphql-mapper/tree/21-no-support-for-list-return-types

Anyway I will publish a new version of the library with the fix soon

Thanks

indraneelmax commented 1 year ago

For the last issue regarding the python classes not generated following the name I am not exactly sure of what you mean.

The below class MyQuery does not generate schema for list_users

@strawberry.type
class MyQuery:
    list_users: List[User] = strawberry.field(resolver=resolve_list_users)

while below class Query generates schema for list_users

@strawberry.type
class Query:
    list_users: List[User] = strawberry.field(resolver=resolve_list_users)

Thank you for the fix branch, I will try to give it a shot and get back to you.

dapalex commented 1 year ago

Thanks, I just pushed also that change. Now queries don't get recognized by the canonical name "Query" but whatever name has been defined in

"queryType": {
      "name": "......"
    },
indraneelmax commented 1 year ago

Hi, Your branch does fix the list return types ( I still have to pull and test for the name fix).

class listUsers(GQLQuery):
   class UserArgs(GQLArgsSet, GQLObject):
      name: NonNull_str
      login: NonNull_str
      aggregateId: NonNull_str
      limit: NonNull_int

   _args: UserArgs

   type: NonNull_list_User[User]
class User(GQLObject):
   aggregateId: str
   login: str
   name: str

class NonNull_list_User(list, User): pass
  1. I am looking for a way to only query certain attributes for the returned User (e.g. login, name only instead of all attributes). I came across the set_show method through which I could set fields to false, but that seems to create a problem when converting the fetched result into python object via map_gqldata_to_obj. For e.g
    query = listUsers(limit=2)  # from queries.py
    query.set_show("listUsers.aggregateId", False) # Do not want the aggregateID field retured back as part of User.

    For above query for listUsers is returning a json response like below -

    {'data': {'listUsers': [ {'login': 'testUser2', 'name': 'Test User 2 '}, { 'login': 'testUser', 'name': 'Test User 1 '}]}}
    
    gql_response = GQLResponse(response, log_progress=True)
    gql_response.map_gqldata_to_obj(query.type)
    print('resultObject: ' + str(gql_response.result_obj))

ERROR:root:Setting value for element aggregateId failed - can only concatenate str (not "NonNull_list_User") to str ERROR:root:Setting value for element aggregateId failed - can only concatenate str (not "NonNull_list_User") to str resultObject: NonNull_list_User(aggregateId='', login='testUser', name='Test User 1 ')


2. For the same query for `listUsers` as from above notice that the `gql_response.result_obj` has just one user unfortunately (event without using `set_show`) -

resultObject: NonNull_list_User(aggregateId='', login='testUser', name='Test User 1 ')