graphql 0.0.4 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/graph_ql/directive.rb +36 -0
- data/lib/graph_ql/directives/directive_chain.rb +33 -0
- data/lib/graph_ql/directives/include_directive.rb +15 -0
- data/lib/graph_ql/directives/skip_directive.rb +15 -0
- data/lib/graph_ql/enum.rb +34 -0
- data/lib/graph_ql/fields/abstract_field.rb +37 -0
- data/lib/graph_ql/fields/access_field.rb +24 -0
- data/lib/graph_ql/fields/field.rb +34 -0
- data/lib/graph_ql/interface.rb +14 -0
- data/lib/graph_ql/introspection/arguments_field.rb +5 -0
- data/lib/graph_ql/introspection/directive_type.rb +12 -0
- data/lib/graph_ql/introspection/enum_value_type.rb +10 -0
- data/lib/graph_ql/introspection/enum_values_field.rb +15 -0
- data/lib/graph_ql/introspection/field_type.rb +11 -0
- data/lib/graph_ql/introspection/fields_field.rb +14 -0
- data/lib/graph_ql/introspection/input_fields_field.rb +12 -0
- data/lib/graph_ql/introspection/input_value_type.rb +10 -0
- data/lib/graph_ql/introspection/of_type_field.rb +12 -0
- data/lib/graph_ql/introspection/possible_types_field.rb +12 -0
- data/lib/graph_ql/introspection/schema_type.rb +32 -0
- data/lib/graph_ql/introspection/type_kind_enum.rb +7 -0
- data/lib/graph_ql/introspection/type_type.rb +22 -0
- data/lib/graph_ql/parser/nodes.rb +72 -0
- data/lib/graph_ql/parser/parser.rb +108 -0
- data/lib/graph_ql/parser/transform.rb +86 -0
- data/lib/graph_ql/parser/visitor.rb +47 -0
- data/lib/graph_ql/query.rb +50 -0
- data/lib/graph_ql/query/arguments.rb +25 -0
- data/lib/graph_ql/query/field_resolution_strategy.rb +83 -0
- data/lib/graph_ql/query/fragment_spread_resolution_strategy.rb +16 -0
- data/lib/graph_ql/query/inline_fragment_resolution_strategy.rb +14 -0
- data/lib/graph_ql/query/operation_resolver.rb +28 -0
- data/lib/graph_ql/query/selection_resolver.rb +20 -0
- data/lib/graph_ql/query/type_resolver.rb +19 -0
- data/lib/graph_ql/repl.rb +27 -0
- data/lib/graph_ql/schema.rb +30 -0
- data/lib/graph_ql/schema/type_reducer.rb +44 -0
- data/lib/graph_ql/type_kinds.rb +15 -0
- data/lib/graph_ql/types/abstract_type.rb +14 -0
- data/lib/graph_ql/types/boolean_type.rb +6 -0
- data/lib/graph_ql/types/float_type.rb +6 -0
- data/lib/graph_ql/types/input_object_type.rb +17 -0
- data/lib/graph_ql/types/input_value.rb +10 -0
- data/lib/graph_ql/types/int_type.rb +6 -0
- data/lib/graph_ql/types/list_type.rb +10 -0
- data/lib/graph_ql/types/non_null_type.rb +18 -0
- data/lib/graph_ql/types/non_null_with_bang.rb +5 -0
- data/lib/graph_ql/types/object_type.rb +62 -0
- data/lib/graph_ql/types/scalar_type.rb +5 -0
- data/lib/graph_ql/types/string_type.rb +6 -0
- data/lib/graph_ql/types/type_definer.rb +16 -0
- data/lib/graph_ql/union.rb +35 -0
- data/lib/graph_ql/validations/fields_are_defined_on_type.rb +44 -0
- data/lib/graph_ql/validations/fields_will_merge.rb +80 -0
- data/lib/graph_ql/validations/fragments_are_used.rb +24 -0
- data/lib/graph_ql/validator.rb +29 -0
- data/lib/graph_ql/version.rb +3 -0
- data/lib/graphql.rb +92 -99
- data/readme.md +17 -177
- data/spec/graph_ql/directive_spec.rb +81 -0
- data/spec/graph_ql/enum_spec.rb +5 -0
- data/spec/graph_ql/fields/field_spec.rb +10 -0
- data/spec/graph_ql/interface_spec.rb +13 -0
- data/spec/graph_ql/introspection/directive_type_spec.rb +40 -0
- data/spec/graph_ql/introspection/schema_type_spec.rb +39 -0
- data/spec/graph_ql/introspection/type_type_spec.rb +104 -0
- data/spec/graph_ql/parser/parser_spec.rb +120 -0
- data/spec/graph_ql/parser/transform_spec.rb +109 -0
- data/spec/graph_ql/parser/visitor_spec.rb +31 -0
- data/spec/graph_ql/query/operation_resolver_spec.rb +14 -0
- data/spec/graph_ql/query_spec.rb +82 -0
- data/spec/graph_ql/schema/type_reducer_spec.rb +24 -0
- data/spec/graph_ql/types/input_object_type_spec.rb +12 -0
- data/spec/graph_ql/types/object_type_spec.rb +35 -0
- data/spec/graph_ql/union_spec.rb +27 -0
- data/spec/graph_ql/validations/fields_are_defined_on_type_spec.rb +28 -0
- data/spec/graph_ql/validations/fields_will_merge_spec.rb +40 -0
- data/spec/graph_ql/validations/fragments_are_used_spec.rb +28 -0
- data/spec/graph_ql/validator_spec.rb +24 -0
- data/spec/spec_helper.rb +2 -2
- data/spec/support/dummy_app.rb +123 -63
- data/spec/support/dummy_data.rb +11 -0
- metadata +107 -59
- data/lib/graphql/call.rb +0 -8
- data/lib/graphql/connection.rb +0 -65
- data/lib/graphql/field.rb +0 -12
- data/lib/graphql/field_definer.rb +0 -25
- data/lib/graphql/introspection/call_type.rb +0 -13
- data/lib/graphql/introspection/connection.rb +0 -9
- data/lib/graphql/introspection/field_type.rb +0 -10
- data/lib/graphql/introspection/root_call_argument_node.rb +0 -5
- data/lib/graphql/introspection/root_call_type.rb +0 -20
- data/lib/graphql/introspection/schema_call.rb +0 -8
- data/lib/graphql/introspection/schema_type.rb +0 -17
- data/lib/graphql/introspection/type_call.rb +0 -8
- data/lib/graphql/introspection/type_type.rb +0 -18
- data/lib/graphql/node.rb +0 -244
- data/lib/graphql/parser/parser.rb +0 -39
- data/lib/graphql/parser/transform.rb +0 -22
- data/lib/graphql/query.rb +0 -109
- data/lib/graphql/root_call.rb +0 -202
- data/lib/graphql/root_call_argument.rb +0 -11
- data/lib/graphql/root_call_argument_definer.rb +0 -17
- data/lib/graphql/schema/all.rb +0 -46
- data/lib/graphql/schema/schema.rb +0 -87
- data/lib/graphql/schema/schema_validation.rb +0 -32
- data/lib/graphql/syntax/call.rb +0 -8
- data/lib/graphql/syntax/field.rb +0 -9
- data/lib/graphql/syntax/fragment.rb +0 -7
- data/lib/graphql/syntax/node.rb +0 -8
- data/lib/graphql/syntax/query.rb +0 -8
- data/lib/graphql/syntax/variable.rb +0 -7
- data/lib/graphql/types/boolean_type.rb +0 -3
- data/lib/graphql/types/number_type.rb +0 -3
- data/lib/graphql/types/object_type.rb +0 -6
- data/lib/graphql/types/string_type.rb +0 -3
- data/lib/graphql/version.rb +0 -3
- data/spec/graphql/node_spec.rb +0 -69
- data/spec/graphql/parser/parser_spec.rb +0 -168
- data/spec/graphql/parser/transform_spec.rb +0 -157
- data/spec/graphql/query_spec.rb +0 -274
- data/spec/graphql/root_call_spec.rb +0 -69
- data/spec/graphql/schema/schema_spec.rb +0 -93
- data/spec/graphql/schema/schema_validation_spec.rb +0 -48
- data/spec/support/nodes.rb +0 -175
@@ -0,0 +1,44 @@
|
|
1
|
+
class GraphQL::Validations::FieldsAreDefinedOnType
|
2
|
+
TYPE_INFERRENCE_ROOTS = [GraphQL::Nodes::OperationDefinition, GraphQL::Nodes::FragmentDefinition]
|
3
|
+
FIELD_MODIFIERS = [GraphQL::TypeKinds::LIST]
|
4
|
+
|
5
|
+
def validate(context)
|
6
|
+
visitor = context.visitor
|
7
|
+
TYPE_INFERRENCE_ROOTS.each do |node_class|
|
8
|
+
visitor[node_class] << -> (node){ validate_document_part(node, context) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def validate_document_part(part, context)
|
14
|
+
if part.is_a?(GraphQL::Nodes::FragmentDefinition)
|
15
|
+
type = context.schema.types[part.type]
|
16
|
+
validate_selections(type, part.selections, context)
|
17
|
+
elsif part.is_a?(GraphQL::Nodes::OperationDefinition)
|
18
|
+
type = context.schema.public_send(part.operation_type) # mutation root or query root
|
19
|
+
validate_selections(type, part.selections, context)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_selections(type, selections, context)
|
24
|
+
selections
|
25
|
+
.select {|f| f.is_a?(GraphQL::Nodes::Field) } # don't worry about fragments
|
26
|
+
.each do |ast_field|
|
27
|
+
field = type.fields[ast_field.name]
|
28
|
+
if field.nil?
|
29
|
+
context.errors << "Field '#{ast_field.name}' doesn't exist on type '#{type.name}'"
|
30
|
+
else
|
31
|
+
field_type = get_field_type(field)
|
32
|
+
validate_selections(field_type, ast_field.selections, context)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def get_field_type(field)
|
38
|
+
if FIELD_MODIFIERS.include?(field.type.kind)
|
39
|
+
field.type.of_type
|
40
|
+
else
|
41
|
+
field.type
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
class GraphQL::Validations::FieldsWillMerge
|
2
|
+
HAS_SELECTIONS = [GraphQL::Nodes::OperationDefinition, GraphQL::Nodes::InlineFragment]
|
3
|
+
|
4
|
+
def validate(context)
|
5
|
+
fragments = {}
|
6
|
+
has_selections = []
|
7
|
+
visitor = context.visitor
|
8
|
+
HAS_SELECTIONS.each do |node_class|
|
9
|
+
visitor[node_class] << -> (node) { has_selections << node }
|
10
|
+
end
|
11
|
+
visitor[GraphQL::Nodes::FragmentDefinition] << -> (node) { fragments[node.name] = node }
|
12
|
+
visitor[GraphQL::Nodes::Document].leave << -> (node) {
|
13
|
+
has_selections.each { |node| validate_selections(node.selections, {}, fragments, context.errors)}
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def validate_selections(selections, name_to_field, fragments, errors)
|
20
|
+
selections.each do |field|
|
21
|
+
if field.is_a?(GraphQL::Nodes::InlineFragment)
|
22
|
+
validate_selections(field.selections, name_to_field, fragments, errors)
|
23
|
+
elsif field.is_a?(GraphQL::Nodes::FragmentSpread)
|
24
|
+
fragment = fragments[field.name]
|
25
|
+
validate_selections(fragment.selections, name_to_field, fragments, errors)
|
26
|
+
else
|
27
|
+
field_errors = validate_field(field, name_to_field)
|
28
|
+
errors.push(*field_errors)
|
29
|
+
validate_selections(field.selections, name_to_field, fragments, errors)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def validate_field(field, name_to_field)
|
35
|
+
name_in_selection = field.alias || field.name
|
36
|
+
name_to_field[name_in_selection] ||= field
|
37
|
+
first_field_def = name_to_field[name_in_selection]
|
38
|
+
comparison = FieldDefinitionComparison.new(name_in_selection, first_field_def, field)
|
39
|
+
comparison.errors
|
40
|
+
end
|
41
|
+
|
42
|
+
# Compare two field definitions, add errors to the list if there are any
|
43
|
+
class FieldDefinitionComparison
|
44
|
+
NAMED_VALUES = [GraphQL::Nodes::Enum, GraphQL::Nodes::VariableIdentifier]
|
45
|
+
attr_reader :errors
|
46
|
+
def initialize(name, prev_def, next_def)
|
47
|
+
errors = []
|
48
|
+
if prev_def.name != next_def.name
|
49
|
+
errors << "Field '#{name}' has a field conflict: #{prev_def.name} or #{next_def.name}?"
|
50
|
+
end
|
51
|
+
prev_arguments = reduce_list(prev_def.arguments)
|
52
|
+
next_arguments = reduce_list(next_def.arguments)
|
53
|
+
if prev_arguments != next_arguments
|
54
|
+
errors << "Field '#{name}' has an argument conflict: #{JSON.dump(prev_arguments)} or #{JSON.dump(next_arguments)}?"
|
55
|
+
end
|
56
|
+
prev_directive_names = prev_def.directives.map(&:name)
|
57
|
+
next_directive_names = next_def.directives.map(&:name)
|
58
|
+
if prev_directive_names != next_directive_names
|
59
|
+
errors << "Field '#{name}' has a directive conflict: [#{prev_directive_names.join(", ")}] or [#{next_directive_names.join(", ")}]?"
|
60
|
+
end
|
61
|
+
prev_directive_args = prev_def.directives.map {|d| reduce_list(d.arguments) }
|
62
|
+
next_directive_args = next_def.directives.map {|d| reduce_list(d.arguments) }
|
63
|
+
if prev_directive_args != next_directive_args
|
64
|
+
errors << "Field '#{name}' has a directive argument conflict: #{JSON.dump(prev_directive_args)} or #{JSON.dump(next_directive_args)}?"
|
65
|
+
end
|
66
|
+
@errors = errors
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# Turn AST tree into a hash
|
72
|
+
# can't look up args, the names just have to match
|
73
|
+
def reduce_list(args)
|
74
|
+
args.reduce({}) do |memo, a|
|
75
|
+
memo[a.name] = NAMED_VALUES.include?(a.value.class) ? a.value.name : a.value
|
76
|
+
memo
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class GraphQL::Validations::FragmentsAreUsed
|
2
|
+
def validate(context)
|
3
|
+
v = context.visitor
|
4
|
+
used_fragment_names = []
|
5
|
+
defined_fragment_names = []
|
6
|
+
v[GraphQL::Nodes::FragmentSpread] << -> (node) { used_fragment_names << node.name }
|
7
|
+
v[GraphQL::Nodes::FragmentDefinition] << -> (node) { defined_fragment_names << node.name}
|
8
|
+
v[GraphQL::Nodes::Document].leave << -> (node) { add_errors(context.errors, used_fragment_names, defined_fragment_names) }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def add_errors(errors, used_fragment_names, defined_fragment_names)
|
14
|
+
undefined_fragment_names = used_fragment_names - defined_fragment_names
|
15
|
+
if undefined_fragment_names.any?
|
16
|
+
errors << "Some fragments were used but not defined: #{undefined_fragment_names.join(", ")}"
|
17
|
+
end
|
18
|
+
|
19
|
+
unused_fragment_names = defined_fragment_names - used_fragment_names
|
20
|
+
if unused_fragment_names.any?
|
21
|
+
errors << "Some fragments were defined but not used: #{unused_fragment_names.join(", ")}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
class GraphQL::Validator
|
2
|
+
VALIDATORS = [
|
3
|
+
GraphQL::Validations::FragmentsAreUsed,
|
4
|
+
]
|
5
|
+
|
6
|
+
def initialize(schema:, validators: VALIDATORS)
|
7
|
+
@schema = schema
|
8
|
+
@validators = validators
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate(document)
|
12
|
+
context = Context.new(@schema, document)
|
13
|
+
@validators.each do |validator|
|
14
|
+
validator.new.validate(context)
|
15
|
+
end
|
16
|
+
context.visitor.visit(document)
|
17
|
+
context.errors
|
18
|
+
end
|
19
|
+
|
20
|
+
class Context
|
21
|
+
attr_reader :schema, :document, :errors, :visitor
|
22
|
+
def initialize(schema, document)
|
23
|
+
@schema = schema
|
24
|
+
@document = document
|
25
|
+
@visitor = GraphQL::Visitor.new
|
26
|
+
@errors = []
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/graphql.rb
CHANGED
@@ -1,121 +1,114 @@
|
|
1
1
|
require "active_support/core_ext/object/blank"
|
2
2
|
require "active_support/core_ext/string/inflections"
|
3
|
+
require "active_support/dependencies/autoload"
|
3
4
|
require "json"
|
4
5
|
require "parslet"
|
5
6
|
|
6
7
|
module GraphQL
|
7
|
-
|
8
|
-
autoload(:
|
9
|
-
autoload(:
|
10
|
-
autoload(:
|
11
|
-
autoload(:
|
12
|
-
autoload(:Query
|
13
|
-
autoload(:
|
14
|
-
autoload(:
|
15
|
-
autoload(:
|
16
|
-
autoload(:
|
8
|
+
extend ActiveSupport::Autoload
|
9
|
+
autoload(:Directive)
|
10
|
+
autoload(:Enum)
|
11
|
+
autoload(:Interface)
|
12
|
+
autoload(:Parser)
|
13
|
+
autoload(:Query)
|
14
|
+
autoload(:Repl)
|
15
|
+
autoload(:Schema)
|
16
|
+
autoload(:TypeKinds)
|
17
|
+
autoload(:Union)
|
18
|
+
autoload(:Validator)
|
19
|
+
autoload(:VERSION)
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
autoload(:
|
21
|
-
autoload(:
|
22
|
-
autoload(:FieldType, "graphql/introspection/field_type")
|
23
|
-
autoload(:RootCallArgumentNode, "graphql/introspection/root_call_argument_node")
|
24
|
-
autoload(:RootCallType, "graphql/introspection/root_call_type")
|
25
|
-
autoload(:SchemaCall, "graphql/introspection/schema_call")
|
26
|
-
autoload(:SchemaType, "graphql/introspection/schema_type")
|
27
|
-
autoload(:TypeCall, "graphql/introspection/type_call")
|
28
|
-
autoload(:TypeType, "graphql/introspection/type_type")
|
21
|
+
autoload_under "directives" do
|
22
|
+
autoload(:DirectiveChain)
|
23
|
+
autoload(:IncludeDirective)
|
24
|
+
autoload(:SkipDirective)
|
29
25
|
end
|
30
26
|
|
31
|
-
|
32
|
-
|
33
|
-
autoload(:
|
34
|
-
autoload(:
|
27
|
+
autoload_under "fields" do
|
28
|
+
autoload(:AbstractField)
|
29
|
+
autoload(:AccessField)
|
30
|
+
autoload(:Field)
|
35
31
|
end
|
36
32
|
|
37
|
-
|
38
|
-
|
39
|
-
autoload(:
|
40
|
-
autoload(:
|
41
|
-
autoload(:
|
33
|
+
autoload_under "introspection" do
|
34
|
+
autoload(:ArgumentsField)
|
35
|
+
autoload(:DirectiveType)
|
36
|
+
autoload(:EnumValuesField)
|
37
|
+
autoload(:EnumValueType)
|
38
|
+
autoload(:FieldType)
|
39
|
+
autoload(:FieldsField)
|
40
|
+
autoload(:InputValueType)
|
41
|
+
autoload(:InputFieldsField)
|
42
|
+
autoload(:OfTypeField)
|
43
|
+
autoload(:PossibleTypesField)
|
44
|
+
autoload(:SchemaType)
|
45
|
+
autoload(:TypeKindEnum)
|
46
|
+
autoload(:TypeType)
|
42
47
|
end
|
43
48
|
|
44
|
-
|
45
|
-
|
46
|
-
autoload(:
|
47
|
-
autoload(:
|
48
|
-
autoload(:
|
49
|
-
autoload(:Fragment, "graphql/syntax/fragment")
|
50
|
-
autoload(:Node, "graphql/syntax/node")
|
51
|
-
autoload(:Variable, "graphql/syntax/variable")
|
49
|
+
autoload_under "parser" do
|
50
|
+
autoload(:Nodes)
|
51
|
+
autoload(:Parser)
|
52
|
+
autoload(:Transform)
|
53
|
+
autoload(:Visitor)
|
52
54
|
end
|
53
55
|
|
54
|
-
|
55
|
-
|
56
|
-
autoload(:
|
57
|
-
autoload(:
|
58
|
-
autoload(:
|
59
|
-
autoload(:
|
56
|
+
autoload_under "types" do
|
57
|
+
autoload(:AbstractType)
|
58
|
+
autoload(:BOOLEAN_TYPE)
|
59
|
+
autoload(:ScalarType)
|
60
|
+
autoload(:FLOAT_TYPE)
|
61
|
+
autoload(:InputObjectType)
|
62
|
+
autoload(:InputValue)
|
63
|
+
autoload(:INT_TYPE)
|
64
|
+
autoload(:ListType)
|
65
|
+
autoload(:NonNullType)
|
66
|
+
autoload(:NonNullWithBang)
|
67
|
+
autoload(:ObjectType)
|
68
|
+
autoload(:STRING_TYPE)
|
69
|
+
autoload(:TypeDefiner)
|
60
70
|
end
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
class_name = node_class.name
|
68
|
-
defined_field_names = node_class.all_fields.keys
|
69
|
-
super("#{class_name}##{field_name} was requested, but it isn't defined. Defined fields are: #{defined_field_names}")
|
70
|
-
end
|
71
|
-
end
|
72
|
-
# The class that this node is supposed to expose isn't defined
|
73
|
-
class ExposesClassMissingError < Error
|
74
|
-
def initialize(node_class)
|
75
|
-
super("#{node_class.name} exposes #{node_class.exposes_class_names.join(", ")}, but that class wasn't found.")
|
76
|
-
end
|
77
|
-
end
|
78
|
-
# There's no Node defined for that kind of object.
|
79
|
-
class NodeNotDefinedError < Error
|
80
|
-
def initialize(node_name)
|
81
|
-
super("#{node_name} was requested but was not found. Defined nodes are: #{SCHEMA.type_names}")
|
82
|
-
end
|
83
|
-
end
|
84
|
-
# This node doesn't have a connection with that name.
|
85
|
-
class ConnectionNotDefinedError < Error
|
86
|
-
def initialize(node_name)
|
87
|
-
super("#{node_name} was requested but was not found. Defined connections are: #{SCHEMA.connection_names}")
|
88
|
-
end
|
89
|
-
end
|
90
|
-
# The root call of this query isn't in the schema.
|
91
|
-
class RootCallNotDefinedError < Error
|
92
|
-
def initialize(name)
|
93
|
-
super("Call '#{name}' was requested but was not found. Defined calls are: #{SCHEMA.call_names}")
|
94
|
-
end
|
71
|
+
|
72
|
+
module Validations
|
73
|
+
extend ActiveSupport::Autoload
|
74
|
+
autoload(:FieldsWillMerge)
|
75
|
+
autoload(:FragmentsAreUsed)
|
76
|
+
autoload(:FieldsAreDefinedOnType)
|
95
77
|
end
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
78
|
+
|
79
|
+
PARSER = Parser.new
|
80
|
+
TRANSFORM = Transform.new
|
81
|
+
|
82
|
+
def self.parse(string, as: nil)
|
83
|
+
parser = as ? GraphQL::PARSER.send(as) : GraphQL::PARSER
|
84
|
+
tree = parser.parse(string)
|
85
|
+
GraphQL::TRANSFORM.apply(tree)
|
86
|
+
rescue Parslet::ParseFailed => error
|
87
|
+
line, col = error.cause.source.line_and_column
|
88
|
+
raise [line, col, string].join(", ")
|
102
89
|
end
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
90
|
+
|
91
|
+
|
92
|
+
module Definable
|
93
|
+
def attr_definable(*names)
|
94
|
+
attr_accessor(*names)
|
95
|
+
names.each do |name|
|
96
|
+
ivar_name = "@#{name}".to_sym
|
97
|
+
define_method(name) do |new_value=nil|
|
98
|
+
new_value && self.instance_variable_set(ivar_name, new_value)
|
99
|
+
instance_variable_get(ivar_name)
|
100
|
+
end
|
101
|
+
end
|
107
102
|
end
|
108
103
|
end
|
109
104
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
Dir["#{File.dirname(__FILE__)}/graphql/#{preload_dir}/*.rb"].each { |f| require f }
|
105
|
+
module Forwardable
|
106
|
+
def delegate(*methods, to:)
|
107
|
+
methods.each do |method_name|
|
108
|
+
define_method(method_name) do |*args|
|
109
|
+
self.public_send(to).public_send(method_name, *args)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
119
113
|
end
|
120
|
-
|
121
|
-
end
|
114
|
+
end
|
data/readme.md
CHANGED
@@ -2,190 +2,30 @@
|
|
2
2
|
|
3
3
|
[](https://travis-ci.org/rmosolgo/graphql-ruby)
|
4
4
|
[](https://rubygems.org/gems/graphql)
|
5
|
-
[](https://gemnasium.com/rmosolgo/graphql-ruby)
|
6
5
|
[](https://codeclimate.com/github/rmosolgo/graphql-ruby)
|
7
6
|
[](https://codeclimate.com/github/rmosolgo/graphql-ruby)
|
8
7
|
[](http://rmosolgo.github.io/react-badges/)
|
9
8
|
|
9
|
+
__Current status__: rewriting according to spec, see also the previous [prototype implementation](https://github.com/rmosolgo/graphql-ruby/tree/74ad3c30a6d8db010ec3856f5871f8a02fcfba42)!
|
10
10
|
|
11
|
-
|
11
|
+
## Overview
|
12
12
|
|
13
|
-
|
13
|
+
- See the __[test schema](https://github.com/rmosolgo/graphql-ruby/blob/master/spec/support/dummy_app.rb)__ for an example GraphQL schema in Ruby.
|
14
|
+
- See __[query_spec.rb](https://github.com/rmosolgo/graphql-ruby/blob/master/spec/graph_ql/query_spec.rb)__ for an example of query execution.
|
14
15
|
|
15
|
-
## To
|
16
|
+
## To Do:
|
16
17
|
|
17
|
-
-
|
18
|
-
-
|
19
|
-
-
|
20
|
-
-
|
21
|
-
-
|
18
|
+
- Validations:
|
19
|
+
- implement lots of validators
|
20
|
+
- build error object with position
|
21
|
+
- Express failure with `errors` key (http://facebook.github.io/graphql/#sec-Errors)
|
22
|
+
- directives:
|
23
|
+
- `@skip` has precedence over `@include`
|
24
|
+
- directives on fragments: http://facebook.github.io/graphql/#sec-Fragment-Directives
|
25
|
+
- field merging (https://github.com/graphql/graphql-js/issues/19#issuecomment-118515077)
|
22
26
|
|
23
|
-
##
|
27
|
+
## Goals:
|
24
28
|
|
25
|
-
-
|
26
|
-
-
|
27
|
-
|
28
|
-
<a href="http://graphql-ruby-demo.herokuapp.com/" target="_blank"><img src="https://cloud.githubusercontent.com/assets/2231765/6217972/5d24edda-b5ce-11e4-9e07-3548304af862.png" style="max-width: 800px;"/></a>
|
29
|
-
|
30
|
-
|
31
|
-
## Usage
|
32
|
-
|
33
|
-
- Implement _nodes_ that wrap objects in your application
|
34
|
-
- Implement _calls_ that return those objects (and may mutate the application state)
|
35
|
-
- Execute _queries_ and return the result.
|
36
|
-
|
37
|
-
### Nodes
|
38
|
-
|
39
|
-
Nodes are delegators that wrap objects in your app. You must whitelist fields by declaring them in the class definition.
|
40
|
-
|
41
|
-
|
42
|
-
```ruby
|
43
|
-
class FishNode < GraphQL::Node
|
44
|
-
exposes "Fish"
|
45
|
-
cursor(:id)
|
46
|
-
field.number(:id)
|
47
|
-
field.string(:name)
|
48
|
-
field.string(:species)
|
49
|
-
# specify an `AquariumNode`:
|
50
|
-
field.aquarium(:aquarium)
|
51
|
-
end
|
52
|
-
```
|
53
|
-
|
54
|
-
You can also declare connections between objects:
|
55
|
-
|
56
|
-
```ruby
|
57
|
-
class AquariumNode < GraphQL::Node
|
58
|
-
exposes "Aquarium"
|
59
|
-
cursor(:id)
|
60
|
-
field.number(:id)
|
61
|
-
field.number(:occupancy)
|
62
|
-
field.connection(:fishes)
|
63
|
-
end
|
64
|
-
```
|
65
|
-
|
66
|
-
You can make custom connections:
|
67
|
-
|
68
|
-
```ruby
|
69
|
-
class FishSchoolConnection < GraphQL::Connection
|
70
|
-
type :fish_school # now it is a field type
|
71
|
-
call :largest, -> (prev_value, number) { fishes.sort_by(&:weight).first(number.to_i) }
|
72
|
-
|
73
|
-
field.number(:count) # delegated to `target`
|
74
|
-
field.boolean(:has_more)
|
75
|
-
|
76
|
-
def has_more
|
77
|
-
# the `largest()` call may have removed some items:
|
78
|
-
target.count < original_target.count
|
79
|
-
end
|
80
|
-
end
|
81
|
-
```
|
82
|
-
|
83
|
-
Then use them:
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
class AquariumNode < GraphQL::Node
|
87
|
-
field.fish_school(:fishes)
|
88
|
-
end
|
89
|
-
```
|
90
|
-
|
91
|
-
And in queries:
|
92
|
-
|
93
|
-
```
|
94
|
-
aquarium(1) {
|
95
|
-
name,
|
96
|
-
occupancy,
|
97
|
-
fishes.largest(3) {
|
98
|
-
edges {
|
99
|
-
node { name, species }
|
100
|
-
},
|
101
|
-
count,
|
102
|
-
has_more
|
103
|
-
}
|
104
|
-
}
|
105
|
-
}
|
106
|
-
```
|
107
|
-
|
108
|
-
### Calls
|
109
|
-
|
110
|
-
Calls selectively expose your application to the world. They always return values and they may perform mutations.
|
111
|
-
|
112
|
-
Calls declare returns, declare arguments, and implement `#execute!`.
|
113
|
-
|
114
|
-
This call just finds values:
|
115
|
-
|
116
|
-
```ruby
|
117
|
-
class FindFishCall < GraphQL::RootCall
|
118
|
-
returns :fish
|
119
|
-
argument.number(:id)
|
120
|
-
def execute!(id)
|
121
|
-
Fish.find(id)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
```
|
125
|
-
|
126
|
-
This call performs a mutation:
|
127
|
-
|
128
|
-
```ruby
|
129
|
-
class RelocateFishCall < GraphQL::RootCall
|
130
|
-
returns :fish, :previous_aquarium, :new_aquarium
|
131
|
-
argument.number(:fish_id)
|
132
|
-
argument.number(:new_aquarium_id)
|
133
|
-
|
134
|
-
def execute!(fish_id, new_aquarium_id)
|
135
|
-
fish = Fish.find(fish_id)
|
136
|
-
|
137
|
-
# context is defined by the query, see below
|
138
|
-
if !context[:user].can_move?(fish)
|
139
|
-
raise RelocateNotAllowedError
|
140
|
-
end
|
141
|
-
|
142
|
-
previous_aquarium = fish.aquarium
|
143
|
-
new_aquarium = Aquarium.find(new_aquarium_id)
|
144
|
-
fish.update_attributes(aquarium: new_aquarium)
|
145
|
-
{
|
146
|
-
fish: fish,
|
147
|
-
previous_aquarium: previous_aquarium,
|
148
|
-
new_aquarium: new_aquarium,
|
149
|
-
}
|
150
|
-
end
|
151
|
-
end
|
152
|
-
```
|
153
|
-
|
154
|
-
### Queries
|
155
|
-
|
156
|
-
When your system is setup, you can perform queries from a string.
|
157
|
-
|
158
|
-
```ruby
|
159
|
-
query_str = "find_fish(1) { name, species } "
|
160
|
-
query = GraphQL::Query.new(query_str)
|
161
|
-
result = query.as_result
|
162
|
-
|
163
|
-
result
|
164
|
-
# {
|
165
|
-
# "1" => {
|
166
|
-
# "name" => "Sharky",
|
167
|
-
# "species" => "Goldfish",
|
168
|
-
# }
|
169
|
-
# }
|
170
|
-
```
|
171
|
-
|
172
|
-
Each query may also define a `context` object which will be accessible at every point in execution.
|
173
|
-
|
174
|
-
```ruby
|
175
|
-
query_str = "move_fish(1, 3) { fish { name }, new_aquarium { occupancy } }"
|
176
|
-
query_ctx = {user: current_user, request: request}
|
177
|
-
query = GraphQL::Query.new(query_str, context: query_ctx)
|
178
|
-
result = query.as_result
|
179
|
-
|
180
|
-
result
|
181
|
-
# {
|
182
|
-
# "fish" => {
|
183
|
-
# "name" => "Sharky"
|
184
|
-
# },
|
185
|
-
# "new_aquarium" => {
|
186
|
-
# "occupancy" => 12
|
187
|
-
# }
|
188
|
-
# }
|
189
|
-
```
|
190
|
-
|
191
|
-
You could do something like this [inside a Rails controller](https://github.com/rmosolgo/graphql-ruby-demo/blob/master/app/controllers/queries_controller.rb#L5).
|
29
|
+
- Implement the GraphQL spec & support a Relay front end
|
30
|
+
- Provide idiomatic, plain-Ruby API with similarities to reference implementation where possible
|
31
|
+
- Support `graphql-rails`
|