evo-company / hiku

Library to write servers for GraphQL-like query languages
http://hiku.readthedocs.io
BSD 3-Clause "New" or "Revised" License
60 stars 13 forks source link

Link does not resolve on Union types #149

Open n4mespace opened 5 months ago

n4mespace commented 5 months ago

When I tried to create a query with a link within a Union type, got an unexpected error

example query

    query SearchMedia($text: String) {
      searchMedia(text: $text) {
        __typename
        ... on Audio {
          duration
          user {
            id
          }
        }
        ... on Video {
          thumbnailUrl(size: 100)
        }
      }
    }

example error

"Cannot query field 'user' on type 'Media'. "
"Did you mean to use an inline fragment on 'Audio' or 'Video'?"

another libraries, like strawberry allow such a behavior, GraphQL doc also says it's valid

n4mespace commented 5 months ago

example test case for tests/test_union.py

import pytest
from hiku.denormalize.graphql import DenormalizeGraphQL

from hiku.endpoint.graphql import GraphQLEndpoint
from hiku.engine import Engine
from hiku.executors.sync import SyncExecutor
from hiku.graph import Field, Graph, Link, Node, Option, Root, Union
from hiku.types import (
    Any,
    Integer,
    Optional,
    Sequence,
    String,
    TypeRef,
    UnionRef,
)
from hiku.utils import listify
from hiku.readers.graphql import read
from hiku.validate.graph import GraphValidationError
from hiku.validate.query import validate

def execute(graph, query):
    engine = Engine(SyncExecutor())
    result = engine.execute(graph, query)
    return DenormalizeGraphQL(graph, result, "query").process(query)

@listify
def resolve_user_fields(fields, ids):
    def get_field(fname, id_):
        if fname == 'id':
            return id_

    for id_ in ids:
        yield [get_field(f.name, id_) for f in fields]

@listify
def resolve_audio_fields(fields, ids):
    def get_field(fname, id_):
        if fname == 'id':
            return id_
        if fname == 'duration':
            return f'{id_}s'
        if fname == '_user':
            return id_

    for id_ in ids:
        yield [get_field(f.name, id_) for f in fields]

@listify
def resolve_video_fields(fields, ids):
    def get_field(fname, id_):
        if fname == 'id':
            return id_
        if fname == 'thumbnailUrl':
            return f'/video/{id_}'

    for id_ in ids:
        yield [get_field(f.name, id_) for f in fields]

def link_user_media():
    return [
        (1, TypeRef['Audio']),
        (2, TypeRef['Video']),
    ]

def link_user():
    return 111

def direct_link(ids_):
    return ids_

def search_media(opts):
    if opts['text'] != 'foo':
        return []
    return [
        (1, TypeRef['Audio']),
        (2, TypeRef['Video']),
        (3, TypeRef['Audio']),
        (4, TypeRef['Video']),
    ]

def get_media():
    return 1, TypeRef['Audio']

def maybe_get_media():
    return 2, TypeRef['Video']

GRAPH = Graph([
    Node('Audio', [
        Field('id', Integer, resolve_audio_fields),
        Field('duration', String, resolve_audio_fields),
        Field('_user', Any, resolve_audio_fields),
        Link('user', TypeRef['User'], direct_link, requires='_user'),
    ]),
    Node('Video', [
        Field('id', Integer, resolve_video_fields),
        Field('thumbnailUrl', String, resolve_video_fields, options=[
            Option('size', Integer),
        ]),
    ]),
    Node('User', [
        Field('id', Integer, resolve_user_fields),
        Link('media', Sequence[UnionRef['Media']], link_user_media, requires=None),
    ]),
    Root([
        Link(
            'searchMedia',
            Sequence[UnionRef['Media']],
            search_media,
            options=[
                Option('text', String),
            ],
            requires=None
        ),
        Link('media', UnionRef['Media'], get_media, requires=None),
        Link('maybeMedia', Optional[UnionRef['Media']], maybe_get_media, requires=None),
        Link('user', Optional[TypeRef['User']], link_user, requires=None),
    ]),
], unions=[
    Union('Media', ['Audio', 'Video']),
])

...

def test_validate_query_can_contain_shared_links():
    query = """
    query SearchMedia($text: String) {
      searchMedia(text: $text) {
        __typename
        ... on Audio {
          duration
          user {
            id
          }
        }
        ... on Video {
          thumbnailUrl(size: 100)
        }
      }
    }
    """

    errors = validate(GRAPH, read(query, {'text': 'foo'}))
    assert not errors