Skip to content

Commit

Permalink
graphql: Add orderBy argument.
Browse files Browse the repository at this point in the history
All fields that return a list of objects can now be ordered by one or
more of the scalar fields on that object.
  • Loading branch information
vpetrovykh committed Jun 28, 2018
1 parent aa1c201 commit 20f59c4
Show file tree
Hide file tree
Showing 5 changed files with 681 additions and 51 deletions.
94 changes: 80 additions & 14 deletions edb/lang/graphql/translator.py
Expand Up @@ -400,8 +400,12 @@ def visit_Field(self, node):
if shape:
shape.elements = vals
if filterable:
args_dict = {arg.name: arg for arg in node.arguments or []}
filterable.where = self._visit_path_filter(args_dict)
where, orderby, offset, limit = \
self._visit_arguments(node.arguments)
filterable.where = where
filterable.orderby = orderby
filterable.offset = offset
filterable.limit = limit

path.pop()
return spec
Expand Down Expand Up @@ -452,12 +456,17 @@ def _validate_fragment_type(self, frag, spread):
self._context.path.append([Step(frag.on, frag_type)])
self._context.include_base.append(is_specialized)

def _visit_path_filter(self, arguments):
f_arg = arguments.get('filter')
if not f_arg:
return None
def _visit_arguments(self, arguments):
where = offset = limit = None
orderby = []

for arg in arguments:
if arg.name == 'filter':
where = self.visit(arg.value)
elif arg.name == 'order':
orderby = self.visit_order(arg.value)

return self.visit(f_arg.value)
return where, orderby, offset, limit

def get_path_prefix(self, end_trim=None):
# flatten the path
Expand Down Expand Up @@ -494,19 +503,19 @@ def visit_InputObjectLiteral(self, node):
return self._join_expressions(result)

def visit_ObjectField(self, node):
name_parts = node.name
fname = node.name

# handle boolean ops
if name_parts == 'and':
if fname == 'and':
return self._visit_list_of_inputs(node.value.value, ast.ops.AND)
elif name_parts == 'or':
elif fname == 'or':
return self._visit_list_of_inputs(node.value.value, ast.ops.OR)
elif name_parts == 'not':
elif fname == 'not':
return qlast.UnaryOp(op=ast.ops.NOT,
operand=self.visit(node.value))

# handle various scalar ops
op = gt.GQL_TO_OPS_MAP.get(name_parts)
op = gt.GQL_TO_OPS_MAP.get(fname)

if op:
value = self.visit(node.value)
Expand All @@ -516,12 +525,12 @@ def visit_ObjectField(self, node):
_, target = self._get_parent_and_current_type()

name = self.get_path_prefix()
name.append(qlast.Ptr(ptr=qlast.ObjectRef(name=name_parts)))
name.append(qlast.Ptr(ptr=qlast.ObjectRef(name=fname)))
name = qlast.Path(steps=name)

# potentially need to cast the 'name' side into a <str>, so as
# to be compatible with the 'value'
typename = target.get_field_type(name_parts).short_name
typename = target.get_field_type(fname).short_name
if (typename != 'str' and
gt.EDB_TO_GQL_SCALARS_MAP[typename] in {GraphQLString,
GraphQLID}):
Expand All @@ -534,6 +543,63 @@ def visit_ObjectField(self, node):

return self.visit(node.value)

def visit_order(self, node):
# if there is no specific ordering, then order by id
if not node.value:
return [qlast.SortExpr(
path=qlast.Path(
steps=[qlast.Ptr(ptr=qlast.ObjectRef(name='id'))],
partial=True,
),
direction=qlast.SortAsc,
)]

# Ordering is handled by specifying a list of special Ordering objects.
# Validation is already handled by this point.
orderby = []
for enum in node.value:
name, direction, nulls = self._visit_order_item(enum)
orderby.append(qlast.SortExpr(
path=qlast.Path(
steps=[qlast.Ptr(ptr=qlast.ObjectRef(name=name))],
partial=True,
),
direction=direction,
nones_order=nulls,
))

return orderby

def _visit_order_item(self, node):
name = node.name
direction = nulls = None

for part in node.value.value:
if part.name == 'dir':
direction = part.value.value
if part.name == 'nulls':
nulls = part.value.value

# direction is a required field, so we can rely on it having
# one of two values
if direction == 'ASC':
direction = qlast.SortAsc
# nulls are optional, but are 'SMALLEST' by default
if nulls == 'BIGGEST':
nulls = qlast.NonesLast
else:
nulls = qlast.NonesFirst

else: # DESC
direction = qlast.SortDesc
# nulls are optional, but are 'SMALLEST' by default
if nulls == 'BIGGEST':
nulls = qlast.NonesFirst
else:
nulls = qlast.NonesLast

return name, direction, nulls

def visit_Variable(self, node):
return qlast.Parameter(name=node.value[1:])

Expand Down
98 changes: 81 additions & 17 deletions edb/lang/graphql/types.py
Expand Up @@ -18,7 +18,7 @@


from collections import OrderedDict
from functools import partial, lru_cache
from functools import partial
from graphql import (
GraphQLSchema,
GraphQLObjectType,
Expand All @@ -34,7 +34,9 @@
GraphQLFloat,
GraphQLBoolean,
GraphQLID,
GraphQLEnumType,
)
from graphql.type import GraphQLEnumValue
import itertools

from edb.lang.common import ast
Expand Down Expand Up @@ -97,6 +99,7 @@ def __init__(self, edb_schema):
self._gql_interfaces = {}
self._gql_objtypes = {}
self._gql_inobjtypes = {}
self._gql_ordertypes = {}

self._define_types()

Expand Down Expand Up @@ -154,7 +157,12 @@ def _get_target(self, ptr):

return target

@lru_cache(maxsize=None)
def _get_args(self, typename):
return {
'filter': GraphQLArgument(self._gql_inobjtypes[typename]),
'order': GraphQLArgument(self._gql_ordertypes[typename]),
}

def get_fields(self, typename):
fields = OrderedDict()

Expand All @@ -165,9 +173,7 @@ def get_fields(self, typename):
continue
fields[name.split('::', 1)[1]] = GraphQLField(
GraphQLList(GraphQLNonNull(gqltype)),
args={
'filter': GraphQLArgument(self._gql_inobjtypes[name]),
},
args=self._get_args(name),
)
else:
edb_type = self.edb_schema.get(typename)
Expand All @@ -178,18 +184,16 @@ def get_fields(self, typename):
ptr = edb_type.resolve_pointer(self.edb_schema, name)
target = self._get_target(ptr)
if target:
args = None
if isinstance(ptr.target, ObjectType):
args = {
'filter': GraphQLArgument(
self._gql_inobjtypes[ptr.target.name]),
}
args = self._get_args(ptr.target.name)
else:
args = None

fields[name.name] = GraphQLField(target, args=args)

return fields

@lru_cache(maxsize=None)
def get_input_fields(self, typename):
def get_filter_fields(self, typename):
selftype = self._gql_inobjtypes[typename]
fields = OrderedDict()
fields['and'] = GraphQLInputObjectField(
Expand Down Expand Up @@ -220,7 +224,7 @@ def get_input_fields(self, typename):

return fields

def define_generic_input_types(self):
def define_generic_filter_types(self):
eq = ['eq', 'neq']
comp = eq + ['gte', 'gt', 'lte', 'lt']
string = comp + ['like', 'ilike']
Expand All @@ -238,11 +242,64 @@ def _make_generic_input_type(self, base, ops):
fields={op: GraphQLInputObjectField(base) for op in ops},
)

def define_generic_order_types(self):
self._gql_ordertypes['directionEnum'] = GraphQLEnumType(
'directionEnum',
values=OrderedDict(
ASC=GraphQLEnumValue(),
DESC=GraphQLEnumValue()
)
)
self._gql_ordertypes['nullsOrderingEnum'] = GraphQLEnumType(
'nullsOrderingEnum',
values=OrderedDict(
SMALLEST=GraphQLEnumValue(),
BIGGEST=GraphQLEnumValue(),
)
)
self._gql_ordertypes['Ordering'] = GraphQLInputObjectType(
'Ordering',
fields=OrderedDict(
dir=GraphQLInputObjectField(
GraphQLNonNull(self._gql_ordertypes['directionEnum']),
),
nulls=GraphQLInputObjectField(
self._gql_ordertypes['nullsOrderingEnum'],
default_value='SMALLEST',
),
)
)

def get_order_fields(self, typename):
fields = OrderedDict()

edb_type = self.edb_schema.get(typename)
for name in sorted(edb_type.pointers, key=lambda x: x.name):
if name.name == '__type__':
continue

ptr = edb_type.resolve_pointer(self.edb_schema, name)

if not isinstance(ptr.target, ScalarType):
continue

target = self._convert_edb_type(ptr.target)
# this makes sure that we can only order by properties
# that can be reflected into GraphQL
intype = self._gql_inobjtypes.get(f'Filter{target.name}')
if intype:
fields[name.name] = GraphQLInputObjectField(
self._gql_ordertypes['Ordering']
)

return fields

def _define_types(self):
interface_types = []
obj_types = []

self.define_generic_input_types()
self.define_generic_filter_types()
self.define_generic_order_types()

for modname in self.modules:
# get all descendants of this abstract type
Expand All @@ -266,11 +323,18 @@ def _define_types(self):
self._gql_interfaces[t.name] = gqltype

# input object types corresponding to this interface
gqlintype = GraphQLInputObjectType(
gqlfiltertype = GraphQLInputObjectType(
name='Filter' + short_name,
fields=partial(self.get_input_fields, t.name),
fields=partial(self.get_filter_fields, t.name),
)
self._gql_inobjtypes[t.name] = gqlfiltertype

# ordering input type
gqlordertype = GraphQLInputObjectType(
name='Order' + short_name,
fields=partial(self.get_order_fields, t.name),
)
self._gql_inobjtypes[t.name] = gqlintype
self._gql_ordertypes[t.name] = gqlordertype

# object types
for t in obj_types:
Expand Down
16 changes: 16 additions & 0 deletions tests/schemas/graphql_setup.eql
Expand Up @@ -72,3 +72,19 @@ INSERT Person {
active := True,
score := 4.2
};

WITH MODULE test
INSERT Foo {
`select` := 'a',
};

WITH MODULE test
INSERT Foo {
`select` := 'b',
after := 'w',
};

WITH MODULE test
INSERT Foo {
after := 'q',
};

0 comments on commit 20f59c4

Please sign in to comment.