-
Notifications
You must be signed in to change notification settings - Fork 51
/
graphql_schema.py
364 lines (312 loc) · 18.3 KB
/
graphql_schema.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
# Copyright 2019-present Kensho Technologies, LLC.
from collections import OrderedDict
from itertools import chain
from graphql.type import (
GraphQLBoolean, GraphQLField, GraphQLFloat, GraphQLInt, GraphQLInterfaceType, GraphQLList,
GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLUnionType
)
import six
from ..schema import (
DIRECTIVES, EXTENDED_META_FIELD_DEFINITIONS, GraphQLDate, GraphQLDateTime, GraphQLDecimal
)
from .exceptions import IllegalGraphQLRepresentationError
from .schema_properties import (
EDGE_DESTINATION_PROPERTY_NAME, EDGE_SOURCE_PROPERTY_NAME, PROPERTY_TYPE_BOOLEAN_ID,
PROPERTY_TYPE_DATE_ID, PROPERTY_TYPE_DATETIME_ID, PROPERTY_TYPE_DECIMAL_ID,
PROPERTY_TYPE_DOUBLE_ID, PROPERTY_TYPE_EMBEDDED_LIST_ID, PROPERTY_TYPE_EMBEDDED_SET_ID,
PROPERTY_TYPE_FLOAT_ID, PROPERTY_TYPE_INTEGER_ID, PROPERTY_TYPE_STRING_ID
)
def _property_descriptor_to_graphql_type(property_obj):
"""Return the best GraphQL type representation for an OrientDB property descriptor."""
property_type = property_obj.type_id
scalar_types = {
PROPERTY_TYPE_BOOLEAN_ID: GraphQLBoolean,
PROPERTY_TYPE_DATE_ID: GraphQLDate,
PROPERTY_TYPE_DATETIME_ID: GraphQLDateTime,
PROPERTY_TYPE_DECIMAL_ID: GraphQLDecimal,
PROPERTY_TYPE_DOUBLE_ID: GraphQLFloat,
PROPERTY_TYPE_FLOAT_ID: GraphQLFloat,
PROPERTY_TYPE_INTEGER_ID: GraphQLInt,
PROPERTY_TYPE_STRING_ID: GraphQLString,
}
result = scalar_types.get(property_type, None)
if result:
return result
mapping_types = {
PROPERTY_TYPE_EMBEDDED_SET_ID: GraphQLList,
PROPERTY_TYPE_EMBEDDED_LIST_ID: GraphQLList,
}
wrapping_type = mapping_types.get(property_type, None)
if wrapping_type:
linked_property_obj = property_obj.qualifier
# There are properties that are embedded collections of non-primitive types,
# for example, ProxyEventSet.scalar_parameters.
# The GraphQL compiler does not currently support these.
if linked_property_obj in scalar_types:
return wrapping_type(scalar_types[linked_property_obj])
# We weren't able to represent this property in GraphQL, so we'll hide it instead.
return None
def _get_union_type_name(type_names_to_union):
"""Construct a unique union type name based on the type names being unioned."""
if not type_names_to_union:
raise AssertionError(u'Expected a non-empty list of type names to union, received: '
u'{}'.format(type_names_to_union))
return u'Union__' + u'__'.join(sorted(type_names_to_union))
def _get_fields_for_class(schema_graph, graphql_types, field_type_overrides, hidden_classes,
cls_name):
"""Return a dict from field name to GraphQL field type, for the specified graph class."""
properties = schema_graph.get_element_by_class_name(cls_name).properties
# Add leaf GraphQL fields (class properties).
all_properties = {
property_name: _property_descriptor_to_graphql_type(property_obj)
for property_name, property_obj in six.iteritems(properties)
}
result = {
property_name: graphql_representation
for property_name, graphql_representation in six.iteritems(all_properties)
if graphql_representation is not None
}
# Add edge GraphQL fields (edges to other vertex classes).
schema_element = schema_graph.get_element_by_class_name(cls_name)
outbound_edges = (
('out_{}'.format(out_edge_name),
schema_graph.get_element_by_class_name(out_edge_name).properties[
EDGE_DESTINATION_PROPERTY_NAME].qualifier)
for out_edge_name in schema_element.out_connections
)
inbound_edges = (
('in_{}'.format(in_edge_name),
schema_graph.get_element_by_class_name(in_edge_name).properties[
EDGE_SOURCE_PROPERTY_NAME].qualifier)
for in_edge_name in schema_element.in_connections
)
for field_name, to_type_name in chain(outbound_edges, inbound_edges):
edge_endpoint_type_name = None
subclasses = schema_graph.get_subclass_set(to_type_name)
to_type_abstract = schema_graph.get_element_by_class_name(to_type_name).abstract
if not to_type_abstract and len(subclasses) > 1:
# If the edge endpoint type has no subclasses, it can't be coerced into any other type.
# If the edge endpoint type is abstract (an interface type), we can already
# coerce it to the proper type with a GraphQL fragment. However, if the endpoint type
# is non-abstract and has subclasses, we need to return its subclasses as an union type.
# This is because GraphQL fragments cannot be applied on concrete types, and
# GraphQL does not support inheritance of concrete types.
type_names_to_union = [
subclass
for subclass in subclasses
if subclass not in hidden_classes
]
if type_names_to_union:
edge_endpoint_type_name = _get_union_type_name(type_names_to_union)
else:
if to_type_name not in hidden_classes:
edge_endpoint_type_name = to_type_name
if edge_endpoint_type_name is not None:
# If we decided to not hide this edge due to its endpoint type being non-representable,
# represent the edge field as the GraphQL type List(edge_endpoint_type_name).
result[field_name] = GraphQLList(graphql_types[edge_endpoint_type_name])
for field_name, field_type in six.iteritems(field_type_overrides):
if field_name not in result:
raise AssertionError(u'Attempting to override field "{}" from class "{}", but the '
u'class does not contain said field'.format(field_name, cls_name))
else:
result[field_name] = field_type
return result
def _create_field_specification(schema_graph, graphql_types, field_type_overrides,
hidden_classes, cls_name):
"""Return a function that specifies the fields present on the given type."""
def field_maker_func():
"""Create and return the fields for the given GraphQL type."""
result = EXTENDED_META_FIELD_DEFINITIONS.copy()
result.update(OrderedDict([
(name, GraphQLField(value))
for name, value in sorted(six.iteritems(_get_fields_for_class(
schema_graph, graphql_types, field_type_overrides, hidden_classes, cls_name)),
key=lambda x: x[0])
]))
return result
return field_maker_func
def _create_interface_specification(schema_graph, graphql_types, hidden_classes, cls_name):
"""Return a function that specifies the interfaces implemented by the given type."""
def interface_spec():
"""Return a list of GraphQL interface types implemented by the type named 'cls_name'."""
abstract_inheritance_set = (
superclass_name
for superclass_name in sorted(list(schema_graph.get_inheritance_set(cls_name)))
if (superclass_name not in hidden_classes and
schema_graph.get_element_by_class_name(superclass_name).abstract)
)
return [
graphql_types[x]
for x in abstract_inheritance_set
if x not in hidden_classes
]
return interface_spec
def _create_union_types_specification(schema_graph, graphql_types, hidden_classes, base_name):
"""Return a function that gives the types in the union type rooted at base_name."""
# When edges point to vertices of type base_name, and base_name is both non-abstract and
# has subclasses, we need to represent the edge endpoint type with a union type based on
# base_name and its subclasses. This function calculates what types that union should include.
def types_spec():
"""Return a list of GraphQL types that this class' corresponding union type includes."""
return [
graphql_types[x]
for x in sorted(list(schema_graph.get_subclass_set(base_name)))
if x not in hidden_classes
]
return types_spec
def get_graphql_schema_from_schema_graph(schema_graph, class_to_field_type_overrides=None,
hidden_classes=None):
"""Return a GraphQL schema object corresponding to the schema of the given schema graph.
Args:
schema_graph: SchemaGraph
class_to_field_type_overrides: optional dict, class name -> {field name -> field type},
(string -> {string -> GraphQLType}). Used to override the type of a field in the class
where it's first defined and all the the class's subclasses.
hidden_classes: optional set of Strings, classes to not include in the GraphQL schema.
Returns:
tuple (GraphQL schema object, GraphQL type equivalence hints dict), or (None, None)
if there is no schema data in the graph yet.
For example, the graph has no schema data if applying schema updates from the very
first update. We have to return None because the GraphQL library does not support
empty schema objects -- the root object must have some keys and values.
"""
if class_to_field_type_overrides is None:
class_to_field_type_overrides = dict()
# Assert that the fields we want to override are not defined in a superclass.
for class_name, field_type_overrides in six.iteritems(class_to_field_type_overrides):
for superclass_name in schema_graph.get_inheritance_set(class_name):
if superclass_name != class_name:
superclass = schema_graph.get_element_by_class_name(superclass_name)
for field_name in field_type_overrides:
if field_name in superclass.properties:
raise AssertionError(
u'Attempting to override field "{}" from class "{}", but the field is '
u'defined in superclass "{}"'
.format(field_name, class_name, superclass_name))
# The field types of subclasses must also be overridden.
# Remember that the result returned by get_subclass_set(class_name) includes class_name itself.
inherited_field_type_overrides = dict()
for superclass_name, field_type_overrides in class_to_field_type_overrides.items():
for subclass_name in schema_graph.get_subclass_set(superclass_name):
inherited_field_type_overrides.setdefault(subclass_name, dict())
inherited_field_type_overrides[subclass_name].update(field_type_overrides)
if hidden_classes is None:
hidden_classes = dict()
# If user fails to input hidden types, assert that at all vertices have valid GraphQL reps.
for vertex_cls_name in schema_graph.vertex_class_names:
vertex_cls = schema_graph.get_element_by_class_name(vertex_cls_name)
if len(vertex_cls.properties) == 0:
raise IllegalGraphQLRepresentationError(
u'Class "{}" does not have any properties. Therefore, it is not representable '
u'in the Graph and must be explicitly added to the hidden classes.'
.format(vertex_cls_name))
else:
for hidden_class_name in hidden_classes:
# Assert that hidden type exists in the schema
schema_graph.get_element_by_class_name_or_raise(hidden_class_name)
graphql_types = OrderedDict()
type_equivalence_hints = OrderedDict()
# For each vertex class, construct its analogous GraphQL type representation.
for vertex_cls_name in sorted(schema_graph.vertex_class_names):
vertex_cls = schema_graph.get_element_by_class_name(vertex_cls_name)
if vertex_cls_name in hidden_classes:
continue
inherited_field_type_overrides.setdefault(vertex_cls_name, dict())
field_type_overrides = inherited_field_type_overrides[vertex_cls_name]
# We have to use delayed type binding here, because some of the type references
# are circular: if an edge connects vertices of types A and B, then
# GraphQL type A has a List[B] field, and type B has a List[A] field.
# To avoid the circular dependency, GraphQL allows us to initialize the types
# initially without their field information, and fill in their field information
# later using a lambda function as the second argument to GraphQLObjectType.
# This lambda function will be called on each type after all types are created
# in their initial blank state.
#
# However, 'cls_name' is a variable that would not be correctly bound
# if we naively tried to construct a lambda in-place, because Python lambdas
# are not closures. Instead, call a function with 'cls_name' as an argument,
# and have that function construct and return the required lambda.
field_specification_lambda = _create_field_specification(
schema_graph, graphql_types, field_type_overrides, hidden_classes, vertex_cls_name)
# Abstract classes are interfaces, concrete classes are object types.
current_graphql_type = None
if vertex_cls.abstract:
# "fields" is a kwarg in the interface constructor, even though
# it's a positional arg in the object type constructor.
current_graphql_type = GraphQLInterfaceType(vertex_cls_name,
fields=field_specification_lambda)
else:
# For similar reasons as the field_specification_lambda,
# we need to create an interface specification lambda function that
# specifies the interfaces implemented by this type.
interface_specification_lambda = _create_interface_specification(
schema_graph, graphql_types, hidden_classes, vertex_cls_name)
# N.B.: Ignore the "is_type_of" argument below, it is simply a circumvention of
# a sanity check inside the GraphQL library. The library assumes that we'll use
# its execution system, so it complains that we don't provide a means to
# differentiate between different implementations of the same interface.
# We don't care, because we compile the GraphQL query to a database query.
current_graphql_type = GraphQLObjectType(vertex_cls_name,
field_specification_lambda,
interfaces=interface_specification_lambda,
is_type_of=lambda: None)
graphql_types[vertex_cls_name] = current_graphql_type
# For each vertex class, construct all union types representations.
for vertex_cls_name in sorted(schema_graph.vertex_class_names):
vertex_cls = schema_graph.get_element_by_class_name(vertex_cls_name)
if vertex_cls_name in hidden_classes:
continue
vertex_cls_subclasses = schema_graph.get_subclass_set(vertex_cls_name)
if not vertex_cls.abstract and len(vertex_cls_subclasses) > 1:
# In addition to creating this class' corresponding GraphQL type, we'll need a
# union type to represent it when it appears as the endpoint of an edge.
union_type_name = _get_union_type_name(vertex_cls_subclasses)
# For similar reasons as the field_specification_lambda,
# we need to create a union type specification lambda function that specifies
# the types that this union type is composed of.
type_specification_lambda = _create_union_types_specification(
schema_graph, graphql_types, hidden_classes, vertex_cls_name)
union_type = GraphQLUnionType(union_type_name, types=type_specification_lambda)
graphql_types[union_type_name] = union_type
type_equivalence_hints[graphql_types[vertex_cls_name]] = union_type
# Include all abstract non-vertex classes whose only non-abstract subclasses are vertices.
for non_graph_cls_name in schema_graph.non_graph_class_names:
if non_graph_cls_name in hidden_classes:
continue
if not schema_graph.get_element_by_class_name(non_graph_cls_name).abstract:
continue
cls_subclasses = schema_graph.get_subclass_set(non_graph_cls_name)
# No need to add the possible abstract class if it doesn't have subclasses besides itself.
if len(cls_subclasses) > 1:
all_non_abstract_subclasses_are_vertices = True
# Check all non-abstract subclasses are vertices.
for subclass_name in cls_subclasses:
subclass = schema_graph.get_element_by_class_name(subclass_name)
if subclass_name != non_graph_cls_name:
if not subclass.abstract and not subclass.is_vertex:
all_non_abstract_subclasses_are_vertices = False
break
if all_non_abstract_subclasses_are_vertices:
# Add abstract class as an interface.
inherited_field_type_overrides.setdefault(non_graph_cls_name, dict())
field_type_overrides = inherited_field_type_overrides[non_graph_cls_name]
field_specification_lambda = _create_field_specification(
schema_graph, graphql_types, field_type_overrides, hidden_classes,
non_graph_cls_name)
graphql_type = GraphQLInterfaceType(non_graph_cls_name,
fields=field_specification_lambda)
graphql_types[non_graph_cls_name] = graphql_type
if not graphql_types:
# After evaluating all subclasses of V, we weren't able to find any visible schema data
# to import into the GraphQL schema object.
return None, None
# Create the root query GraphQL type. Consists of all non-union classes, i.e.
# all non-abstract classes (as GraphQL types) and all abstract classes (as GraphQL interfaces).
RootSchemaQuery = GraphQLObjectType('RootSchemaQuery', OrderedDict([
(name, GraphQLField(value))
for name, value in sorted(six.iteritems(graphql_types), key=lambda x: x[0])
if not isinstance(value, GraphQLUnionType)
]))
schema = GraphQLSchema(RootSchemaQuery, directives=DIRECTIVES)
return schema, type_equivalence_hints