Skip to content
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

@recursive search #245

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions graphql/execution/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ def execute(
if executor is None:
executor = SyncExecutor()

# operation_name, document_ast

exe_context = ExecutionContext(
schema,
document_ast,
Expand Down
131 changes: 100 additions & 31 deletions graphql/execution/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
# -*- coding: utf-8 -*-
import logging
from traceback import format_exception
from copy import deepcopy

from ..error import GraphQLError
from ..language import ast
from ..pyutils.default_ordered_dict import DefaultOrderedDict
from ..type.definition import GraphQLInterfaceType, GraphQLUnionType
from ..type.directives import GraphQLIncludeDirective, GraphQLSkipDirective
from ..type.directives import (
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLRecursionDirective,
)
from ..type.introspection import (
SchemaMetaFieldDef,
TypeMetaFieldDef,
Expand Down Expand Up @@ -57,16 +62,16 @@ class ExecutionContext(object):
)

def __init__(
self,
schema, # type: GraphQLSchema
document_ast, # type: Document
root_value, # type: Any
context_value, # type: Any
variable_values, # type: Optional[Dict[str, Any]]
operation_name, # type: Optional[str]
executor, # type: Any
middleware, # type: Optional[Any]
allow_subscriptions, # type: bool
self,
schema, # type: GraphQLSchema
document_ast, # type: Document
root_value, # type: Any
context_value, # type: Any
variable_values, # type: Optional[Dict[str, Any]]
operation_name, # type: Optional[str]
executor, # type: Any
middleware, # type: Optional[Any]
allow_subscriptions, # type: bool
):
# type: (...) -> None
"""Constructs a ExecutionContext object from the arguments passed
Expand All @@ -84,9 +89,9 @@ def __init__(
)

if (
not operation_name
or definition.name
and definition.name.value == operation_name
not operation_name
or definition.name
and definition.name.value == operation_name
):
operation = definition

Expand Down Expand Up @@ -218,11 +223,11 @@ def get_operation_root_type(schema, operation):


def collect_fields(
ctx, # type: ExecutionContext
runtime_type, # type: GraphQLObjectType
selection_set, # type: SelectionSet
fields, # type: DefaultOrderedDict
prev_fragment_names, # type: Set[str]
ctx, # type: ExecutionContext
runtime_type, # type: GraphQLObjectType
selection_set, # type: SelectionSet
fields, # type: DefaultOrderedDict
prev_fragment_names, # type: Set[str]
):
# type: (...) -> DefaultOrderedDict
"""
Expand All @@ -237,15 +242,16 @@ def collect_fields(
directives = selection.directives

if isinstance(selection, ast.Field):
if not should_include_node(ctx, directives):
validate = validate_directives(ctx, directives, selection)
if isinstance(validate, bool) and not validate:
continue

name = get_field_entry_key(selection)
fields[name].append(selection)

elif isinstance(selection, ast.InlineFragment):
if not should_include_node(
ctx, directives
ctx, directives
) or not does_fragment_condition_match(ctx, selection, runtime_type):
continue

Expand All @@ -257,17 +263,17 @@ def collect_fields(
frag_name = selection.name.value

if frag_name in prev_fragment_names or not should_include_node(
ctx, directives
ctx, directives
):
continue

prev_fragment_names.add(frag_name)
fragment = ctx.fragments[frag_name]
frag_directives = fragment.directives
if (
not fragment
or not should_include_node(ctx, frag_directives)
or not does_fragment_condition_match(ctx, fragment, runtime_type)
not fragment
or not should_include_node(ctx, frag_directives)
or not does_fragment_condition_match(ctx, fragment, runtime_type)
):
continue

Expand Down Expand Up @@ -316,10 +322,73 @@ def should_include_node(ctx, directives):
return True


def validate_directives(ctx, directives, selection):
for directive in directives:
if directive.name.value in (GraphQLSkipDirective.name, GraphQLIncludeDirective.name):
# @skip, @include checking directive
return should_include_node(ctx, directive)
elif directive.name.value == GraphQLRecursionDirective.name:
# @recursive directive check
build_recursive_selection_set(ctx, directive, selection)


def relay_node_check(selection, frame):
""" Check it if relay structure is presented
modules {
edges {
node {
uid # place new recursive query here
}
}
}
"""
if frame:
relay_frame = frame.pop(0)
else:
return True
for selection in selection.selection_set.selections:
if selection.name.value == relay_frame:
return relay_node_check(selection, frame)
return False


def insert_recursive_selection(selection, depth, frame=[]):
def insert_in_frame(selection, paste_selection, frame=frame):
if frame:
relay_frame = frame.pop(0)
else:
# remove directive
selection.directives = []
paste_selection.directives = []
# return inner selection
returnable_selection_set = selection.selection_set
# insert in depth
returnable_selection_set.selections.append(paste_selection)
return paste_selection
for selection in selection.selection_set.selections:
if selection.name.value == relay_frame:
return insert_in_frame(selection, paste_selection, frame)

# remove_directive(selection)
for counter in range(int(depth)):
copy_selection = deepcopy(selection)
copy_frame = deepcopy(frame)
selection = insert_in_frame(selection, copy_selection, copy_frame)


def build_recursive_selection_set(ctx, directive, selection):
depth_size = directive.arguments[0].value.value
is_relay = relay_node_check(selection, ['edges', 'node'])
if is_relay:
insert_recursive_selection(selection, depth_size, ['edges', 'node'])
else:
insert_recursive_selection(selection, depth_size)


def does_fragment_condition_match(
ctx, # type: ExecutionContext
fragment, # type: Union[FragmentDefinition, InlineFragment]
type_, # type: GraphQLObjectType
ctx, # type: ExecutionContext
fragment, # type: Union[FragmentDefinition, InlineFragment]
type_, # type: GraphQLObjectType
):
# type: (...) -> bool
type_condition_ast = fragment.type_condition
Expand Down Expand Up @@ -356,9 +425,9 @@ def default_resolve_fn(source, info, **args):


def get_field_def(
schema, # type: GraphQLSchema
parent_type, # type: GraphQLObjectType
field_name, # type: str
schema, # type: GraphQLSchema
parent_type, # type: GraphQLObjectType
field_name, # type: str
):
# type: (...) -> Optional[GraphQLField]
"""This method looks up the field on the given type defintion.
Expand Down
16 changes: 9 additions & 7 deletions graphql/language/lexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class TokenKind(object):
INT = 17
FLOAT = 18
STRING = 19
ASTERISK = 20 # recursive symbol


def get_token_desc(token):
Expand All @@ -92,7 +93,7 @@ def get_token_kind_desc(kind):


TOKEN_DESCRIPTION = {
TokenKind.EOF: "EOF",
TokenKind.EOF: "EOF", # end of file
TokenKind.BANG: "!",
TokenKind.DOLLAR: "$",
TokenKind.PAREN_L: "(",
Expand All @@ -111,14 +112,14 @@ def get_token_kind_desc(kind):
TokenKind.INT: "Int",
TokenKind.FLOAT: "Float",
TokenKind.STRING: "String",
TokenKind.ASTERISK: "RS", # recursion selection
}


def char_code_at(s, pos):
# type: (str, int) -> Optional[int]
if 0 <= pos < len(s):
return ord(s[pos])

return None


Expand All @@ -135,6 +136,7 @@ def char_code_at(s, pos):
ord("{"): TokenKind.BRACE_L,
ord("|"): TokenKind.PIPE,
ord("}"): TokenKind.BRACE_R,
ord("*"): TokenKind.ASTERISK, # recursive
}


Expand All @@ -155,14 +157,14 @@ def read_token(source, from_position):

This skips over whitespace and comments until it finds the next lexable
token, then lexes punctuators immediately or calls the appropriate
helper fucntion for more complicated tokens."""
helper function for more complicated tokens."""
body = source.body
body_length = len(body)

position = position_after_whitespace(body, from_position)

if position >= body_length:
return Token(TokenKind.EOF, position, position)
return Token(TokenKind.EOF, position, position) # \n send token

code = char_code_at(body, position)
if code:
Expand All @@ -173,15 +175,15 @@ def read_token(source, from_position):

kind = PUNCT_CODE_TO_KIND.get(code)
if kind is not None:
return Token(kind, position, position + 1)
return Token(kind, position, position + 1) # send token of 20 - asterisk

if code == 46: # .
if code == 46: # . if token is point
if (
char_code_at(body, position + 1)
== char_code_at(body, position + 2)
== 46
):
return Token(TokenKind.SPREAD, position, position + 3)
return Token(TokenKind.SPREAD, position, position + 3) # this definition of fragments

elif 65 <= code <= 90 or code == 95 or 97 <= code <= 122:
# A-Z, _, a-z
Expand Down
21 changes: 13 additions & 8 deletions graphql/language/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
__all__ = ["parse"]


def parse_recursive_body(source,):
# Attrs:
# source: Type[Source]
pass


def parse(source, **kwargs):
# type: (Union[Source, str], **Any) -> Document
"""Given a GraphQL source, parses it into a Document."""
Expand Down Expand Up @@ -241,11 +247,11 @@ def parse_document(parser):
start = parser.token.start
definitions = []
while True:
# all root types (query, subscription, mutation)
definitions.append(parse_definition(parser))

if skip(parser, TokenKind.EOF):
break

return ast.Document(definitions=definitions, loc=loc(parser, start))


Expand All @@ -255,6 +261,7 @@ def parse_definition(parser):
return parse_operation_definition(parser)

if peek(parser, TokenKind.NAME):

name = parser.token.value

if name in ("query", "mutation", "subscription"):
Expand Down Expand Up @@ -307,17 +314,15 @@ def parse_operation_definition(parser):
)


OPERATION_NAMES = frozenset(("query", "mutation", "subscription"))


def parse_operation_type(parser):
# type: (Parser) -> str
operation_token = expect(parser, TokenKind.NAME)
operation = operation_token.value
if operation == "query":
return "query"
elif operation == "mutation":
return "mutation"
elif operation == "subscription":
return "subscription"

if operation in OPERATION_NAMES:
return operation
raise unexpected(parser, operation_token)


Expand Down
23 changes: 22 additions & 1 deletion graphql/type/directives.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from ..pyutils.ordereddict import OrderedDict
from ..utils.assert_valid_name import assert_valid_name
from .definition import GraphQLArgument, GraphQLNonNull, is_input_type
from .scalars import GraphQLBoolean, GraphQLString
from .scalars import GraphQLBoolean, GraphQLString, GraphQLInt


class DirectiveLocation(object):
Expand Down Expand Up @@ -96,6 +96,26 @@ def __init__(self, name, description=None, args=None, locations=None):
],
)


# Recursive directive (TimurMardanov for neomodel)
GraphQLRecursionDirective = GraphQLDirective(
name='recursive',
description = "Recursion of the selection set, with depth.",
args = {
'depth': GraphQLArgument(
type=GraphQLNonNull(GraphQLInt), description='Depth of recursion.',
default_value=1,
)
},
locations=[
DirectiveLocation.FIELD,
DirectiveLocation.FIELD_DEFINITION,
DirectiveLocation.FRAGMENT_SPREAD,
DirectiveLocation.INLINE_FRAGMENT,
],
)
#

"""Constant string used for default reason for a deprecation."""
DEFAULT_DEPRECATION_REASON = "No longer supported"

Expand All @@ -121,4 +141,5 @@ def __init__(self, name, description=None, args=None, locations=None):
GraphQLIncludeDirective,
GraphQLSkipDirective,
GraphQLDeprecatedDirective,
GraphQLRecursionDirective,
]
Loading