graphql 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql.rb +46 -29
- data/lib/graphql/call.rb +1 -1
- data/lib/graphql/connection.rb +12 -35
- data/lib/graphql/field.rb +7 -177
- data/lib/graphql/field_definer.rb +5 -15
- data/lib/graphql/introspection/{call_node.rb → call_type.rb} +1 -1
- data/lib/graphql/introspection/field_type.rb +10 -0
- data/lib/graphql/introspection/{root_call_node.rb → root_call_type.rb} +6 -2
- data/lib/graphql/introspection/schema_type.rb +17 -0
- data/lib/graphql/introspection/{type_node.rb → type_type.rb} +4 -2
- data/lib/graphql/node.rb +118 -42
- data/lib/graphql/{parser.rb → parser/parser.rb} +9 -4
- data/lib/graphql/{transform.rb → parser/transform.rb} +4 -2
- data/lib/graphql/query.rb +33 -10
- data/lib/graphql/root_call.rb +26 -13
- data/lib/graphql/root_call_argument.rb +3 -1
- data/lib/graphql/root_call_argument_definer.rb +3 -7
- data/lib/graphql/schema/all.rb +46 -0
- data/lib/graphql/{schema.rb → schema/schema.rb} +27 -39
- data/lib/graphql/schema/schema_validation.rb +32 -0
- data/lib/graphql/syntax/fragment.rb +7 -0
- data/lib/graphql/syntax/query.rb +3 -2
- data/lib/graphql/types/boolean_type.rb +3 -0
- data/lib/graphql/types/number_type.rb +3 -0
- data/lib/graphql/types/object_type.rb +6 -0
- data/lib/graphql/types/string_type.rb +3 -0
- data/lib/graphql/version.rb +1 -1
- data/readme.md +46 -5
- data/spec/graphql/node_spec.rb +6 -5
- data/spec/graphql/{parser_spec.rb → parser/parser_spec.rb} +31 -2
- data/spec/graphql/{transform_spec.rb → parser/transform_spec.rb} +16 -2
- data/spec/graphql/query_spec.rb +27 -9
- data/spec/graphql/root_call_spec.rb +15 -1
- data/spec/graphql/{schema_spec.rb → schema/schema_spec.rb} +15 -50
- data/spec/graphql/schema/schema_validation_spec.rb +48 -0
- data/spec/support/nodes.rb +31 -28
- metadata +47 -47
- data/lib/graphql/introspection/field_node.rb +0 -19
- data/lib/graphql/introspection/schema_node.rb +0 -17
- data/lib/graphql/types/boolean_field.rb +0 -3
- data/lib/graphql/types/connection_field.rb +0 -30
- data/lib/graphql/types/cursor_field.rb +0 -9
- data/lib/graphql/types/number_field.rb +0 -3
- data/lib/graphql/types/object_field.rb +0 -8
- data/lib/graphql/types/string_field.rb +0 -3
- data/lib/graphql/types/type_field.rb +0 -6
- data/spec/graphql/field_spec.rb +0 -63
@@ -1,9 +1,11 @@
|
|
1
1
|
# Created by {RootCall.argument}, used internally by GraphQL
|
2
2
|
class GraphQL::RootCallArgument
|
3
3
|
attr_reader :type, :name, :any_number
|
4
|
-
|
4
|
+
attr_accessor :index
|
5
|
+
def initialize(type:, name:, any_number: false, index: nil)
|
5
6
|
@type = type
|
6
7
|
@name = name
|
7
8
|
@any_number = any_number
|
9
|
+
@index = index
|
8
10
|
end
|
9
11
|
end
|
@@ -2,20 +2,16 @@
|
|
2
2
|
class GraphQL::RootCallArgumentDefiner
|
3
3
|
ARGUMENT_TYPES = [:string, :object, :number]
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
def initialize(call_class)
|
8
|
-
@call_class = call_class
|
9
|
-
@arguments = []
|
5
|
+
def initialize(owner)
|
6
|
+
@owner = owner
|
10
7
|
end
|
11
8
|
|
12
9
|
def none
|
13
|
-
@arguments = []
|
14
10
|
end
|
15
11
|
|
16
12
|
ARGUMENT_TYPES.each do |arg_type|
|
17
13
|
define_method arg_type do |name, any_number: false|
|
18
|
-
@
|
14
|
+
@owner.add_argument(GraphQL::RootCallArgument.new(type: arg_type.to_s, name: name.to_s, any_number: any_number))
|
19
15
|
end
|
20
16
|
end
|
21
17
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# This query string yields the whole schema. Access it from {GraphQL::Schema::Schema#all}
|
2
|
+
GraphQL::Schema::ALL = "
|
3
|
+
schema() {
|
4
|
+
calls {
|
5
|
+
count,
|
6
|
+
edges {
|
7
|
+
node {
|
8
|
+
name,
|
9
|
+
returns,
|
10
|
+
arguments {
|
11
|
+
edges {
|
12
|
+
node {
|
13
|
+
name, type
|
14
|
+
}
|
15
|
+
}
|
16
|
+
}
|
17
|
+
}
|
18
|
+
}
|
19
|
+
},
|
20
|
+
types {
|
21
|
+
count,
|
22
|
+
edges {
|
23
|
+
node {
|
24
|
+
name,
|
25
|
+
fields {
|
26
|
+
count,
|
27
|
+
edges {
|
28
|
+
node {
|
29
|
+
name,
|
30
|
+
type,
|
31
|
+
calls {
|
32
|
+
count,
|
33
|
+
edges {
|
34
|
+
node {
|
35
|
+
name,
|
36
|
+
arguments
|
37
|
+
}
|
38
|
+
}
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
}
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}"
|
@@ -1,19 +1,36 @@
|
|
1
1
|
# {GraphQL::SCHEMA} keeps track of defined nodes, fields and calls.
|
2
2
|
#
|
3
3
|
# Although you don't interact with it directly, it responds to queries for `schema()` and `__type__` info.
|
4
|
-
|
4
|
+
#
|
5
|
+
# You can validate it at runtime with {#validate}
|
6
|
+
# @example
|
7
|
+
# # validate the schema
|
8
|
+
# GraphQL::SCHEMA.validate
|
9
|
+
#
|
10
|
+
require "singleton"
|
11
|
+
|
12
|
+
class GraphQL::Schema::Schema
|
5
13
|
include Singleton
|
6
|
-
attr_reader :types, :calls, :
|
14
|
+
attr_reader :types, :calls, :class_names
|
7
15
|
def initialize
|
8
16
|
@types = {}
|
9
|
-
@connections = {}
|
10
|
-
@fields = {}
|
11
17
|
@class_names = {}
|
12
18
|
@calls = {}
|
13
19
|
end
|
14
20
|
|
21
|
+
# Queries the whole schema and returns the result
|
22
|
+
def all
|
23
|
+
GraphQL::Query.new(GraphQL::Schema::ALL).as_result
|
24
|
+
end
|
25
|
+
|
26
|
+
def validate
|
27
|
+
validation = GraphQL::Schema::SchemaValidation.new
|
28
|
+
validation.validate(self)
|
29
|
+
end
|
30
|
+
|
15
31
|
def add_call(call_class)
|
16
32
|
remove_call(call_class)
|
33
|
+
raise "You can't make #{call_class.name}'s type '#{call_class.schema_name}'" if call_class.schema_name.blank?
|
17
34
|
@calls[call_class.schema_name] = call_class
|
18
35
|
end
|
19
36
|
|
@@ -38,7 +55,10 @@ class GraphQL::Schema
|
|
38
55
|
@types.delete(existing_name)
|
39
56
|
end
|
40
57
|
|
41
|
-
|
58
|
+
node_class.exposes_class_names.each do |exposes_class_name|
|
59
|
+
@class_names[exposes_class_name] = node_class
|
60
|
+
end
|
61
|
+
|
42
62
|
@types[node_class.schema_name] = node_class
|
43
63
|
end
|
44
64
|
|
@@ -47,7 +67,7 @@ class GraphQL::Schema
|
|
47
67
|
end
|
48
68
|
|
49
69
|
def type_names
|
50
|
-
@types.keys
|
70
|
+
@types.keys.sort
|
51
71
|
end
|
52
72
|
|
53
73
|
def type_for_object(app_object)
|
@@ -62,38 +82,6 @@ class GraphQL::Schema
|
|
62
82
|
return @class_names[class_name]
|
63
83
|
end
|
64
84
|
end
|
65
|
-
raise "Couldn't find node for class #{app_class} #{app_object} (ancestors: #{app_class.ancestors.map(&:name)}, defined: #{registered_class_names})"
|
66
|
-
end
|
67
|
-
|
68
|
-
def add_connection(node_class)
|
69
|
-
existing_name = @connections.key(node_class)
|
70
|
-
if existing_name
|
71
|
-
@connections.delete(existing_name)
|
72
|
-
end
|
73
|
-
@connections[node_class.schema_name.to_s] = node_class
|
74
|
-
end
|
75
|
-
|
76
|
-
def get_connection(identifier)
|
77
|
-
@connections[identifier] || GraphQL::Connection.default_connection || raise(GraphQL::ConnectionNotDefinedError.new(identifier))
|
78
|
-
end
|
79
|
-
|
80
|
-
def connection_names
|
81
|
-
@connections.keys
|
82
|
-
end
|
83
|
-
|
84
|
-
def add_field(field_class)
|
85
|
-
existing_name = @fields.key(field_class)
|
86
|
-
if existing_name
|
87
|
-
@fields.delete(existing_name)
|
88
|
-
end
|
89
|
-
@fields[field_class.schema_name.to_s] = field_class
|
90
|
-
end
|
91
|
-
|
92
|
-
def get_field(identifier)
|
93
|
-
@fields[identifier.to_s] || raise(GraphQL::FieldNotDefinedError.new("<unknown>", identifier))
|
94
|
-
end
|
95
|
-
|
96
|
-
def field_names
|
97
|
-
@fields.keys
|
85
|
+
raise "Couldn't find node for class #{app_class} \"#{app_object}\" (ancestors: #{app_class.ancestors.map(&:name)}, defined: #{registered_class_names})"
|
98
86
|
end
|
99
87
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Validates a schema (specifically, {GraphQL::SCHEMA}).
|
2
|
+
#
|
3
|
+
# It checks:
|
4
|
+
# - All classes exposed by nodes actually exist
|
5
|
+
# - Field types requested by nodes actually exist
|
6
|
+
# - Fields' corresponding methods actually exist
|
7
|
+
#
|
8
|
+
# To validate a schema, use {GraphQL::Schema::Schema#validate}.
|
9
|
+
class GraphQL::Schema::SchemaValidation
|
10
|
+
# Validates the schema
|
11
|
+
def validate(schema)
|
12
|
+
schema.types.each do |type_name, type_class|
|
13
|
+
|
14
|
+
type_class.exposes_class_names.each do |exposes_class_name|
|
15
|
+
begin
|
16
|
+
Object.const_get(exposes_class_name)
|
17
|
+
rescue NameError
|
18
|
+
raise GraphQL::ExposesClassMissingError.new(type_class)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
type_class.all_fields.each do |field_name, field_mapping|
|
23
|
+
# Make sure the type exists
|
24
|
+
field_mapping.type_class
|
25
|
+
# Make sure the node can handle it
|
26
|
+
if !type_class.respond_to_field?(field_mapping.name)
|
27
|
+
raise GraphQL::FieldNotDefinedError.new(type_class, field_mapping.name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/graphql/syntax/query.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
class GraphQL::Syntax::Query
|
2
|
-
attr_reader :nodes, :variables
|
3
|
-
def initialize(nodes:, variables:)
|
2
|
+
attr_reader :nodes, :variables, :fragments
|
3
|
+
def initialize(nodes:, variables:, fragments:)
|
4
4
|
@nodes = nodes
|
5
5
|
@variables = variables
|
6
|
+
@fragments = fragments
|
6
7
|
end
|
7
8
|
end
|
data/lib/graphql/version.rb
CHANGED
data/readme.md
CHANGED
@@ -5,7 +5,8 @@
|
|
5
5
|
[![Dependency Status](https://gemnasium.com/rmosolgo/graphql-ruby.svg)](https://gemnasium.com/rmosolgo/graphql-ruby)
|
6
6
|
[![Code Climate](https://codeclimate.com/github/rmosolgo/graphql-ruby/badges/gpa.svg)](https://codeclimate.com/github/rmosolgo/graphql-ruby)
|
7
7
|
[![Test Coverage](https://codeclimate.com/github/rmosolgo/graphql-ruby/badges/coverage.svg)](https://codeclimate.com/github/rmosolgo/graphql-ruby)
|
8
|
-
![
|
8
|
+
[![built with love](https://cloud.githubusercontent.com/assets/2231765/6766607/d07992c6-cfc9-11e4-813f-d9240714dd50.png)](http://rmosolgo.github.io/react-badges/)
|
9
|
+
|
9
10
|
|
10
11
|
Create a GraphQL interface by implementing [__nodes__](#nodes) and [__calls__](#calls), then running [__queries__](#queries).
|
11
12
|
|
@@ -13,14 +14,11 @@ API Docs: <http://rubydoc.info/gems/graphql>
|
|
13
14
|
|
14
15
|
## To do:
|
15
16
|
|
16
|
-
- Allow a default connection class, or some way to infer connection from name
|
17
|
-
- right now, `Introspection::Connection` isn't getting used, only `ApplicationConnection` is.
|
18
17
|
- How do you express failure? HTTP response? `errors` key?
|
19
18
|
- Handle blank objects in nested calls (how? wait for spec)
|
20
19
|
- Implement calls as arguments
|
21
20
|
- double-check how to handle `pals.first(3) { count }`
|
22
21
|
- Implement call argument introspection (wait for spec)
|
23
|
-
- For fields that return objects, can they be queried _without_ other fields? Or must they always have fields?
|
24
22
|
|
25
23
|
## Example Implementation
|
26
24
|
|
@@ -48,7 +46,8 @@ class FishNode < GraphQL::Node
|
|
48
46
|
field.number(:id)
|
49
47
|
field.string(:name)
|
50
48
|
field.string(:species)
|
51
|
-
|
49
|
+
# specify an `AquariumNode`:
|
50
|
+
field.aquarium(:aquarium)
|
52
51
|
end
|
53
52
|
```
|
54
53
|
|
@@ -64,6 +63,48 @@ class AquariumNode < GraphQL::Node
|
|
64
63
|
end
|
65
64
|
```
|
66
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
|
+
|
67
108
|
### Calls
|
68
109
|
|
69
110
|
Calls selectively expose your application to the world. They always return values and they may perform mutations.
|
data/spec/graphql/node_spec.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe GraphQL::Node do
|
4
|
-
let(:query_string) { "type(post) { name, description, fields { count, edges { node { name,
|
4
|
+
let(:query_string) { "type(post) { name, description, fields { count, edges { node { name, type }}} }"}
|
5
5
|
let(:result) { GraphQL::Query.new(query_string).as_result}
|
6
6
|
|
7
7
|
describe '__type__' do
|
8
8
|
let(:title_field) { result["post"]["fields"]["edges"].find {|e| e["node"]["name"] == "title"}["node"] }
|
9
|
+
|
9
10
|
it 'has name' do
|
10
11
|
assert_equal "post", result["post"]["name"]
|
11
12
|
end
|
@@ -16,7 +17,7 @@ describe GraphQL::Node do
|
|
16
17
|
|
17
18
|
it 'has fields' do
|
18
19
|
assert_equal 8, result["post"]["fields"]["count"]
|
19
|
-
assert_equal({ "name" => "title", "
|
20
|
+
assert_equal({ "name" => "title", "type" => "string"}, title_field)
|
20
21
|
end
|
21
22
|
|
22
23
|
describe 'getting the __type__ field' do
|
@@ -56,12 +57,12 @@ describe GraphQL::Node do
|
|
56
57
|
|
57
58
|
describe 'type:' do
|
58
59
|
it 'uses symbols to find built-ins' do
|
59
|
-
|
60
|
-
|
60
|
+
field_mapping = Nodes::CommentNode.all_fields["id"]
|
61
|
+
assert_equal GraphQL::Types::NumberType, field_mapping.type_class
|
61
62
|
end
|
62
63
|
it 'uses the provided class as a superclass' do
|
63
64
|
letters_field = Nodes::CommentNode.all_fields["letters"]
|
64
|
-
|
65
|
+
assert_equal Nodes::LetterSelectionType, letters_field.type_class
|
65
66
|
end
|
66
67
|
end
|
67
68
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe GraphQL::Parser do
|
3
|
+
describe GraphQL::Parser::Parser do
|
4
4
|
let(:parser) { GraphQL::PARSER }
|
5
5
|
|
6
6
|
describe 'query' do
|
@@ -11,7 +11,7 @@ describe GraphQL::Parser do
|
|
11
11
|
it 'parses node and variables' do
|
12
12
|
assert query.parse_with_debug(%{
|
13
13
|
like_page(<page>) {
|
14
|
-
|
14
|
+
$pageFragment
|
15
15
|
}
|
16
16
|
<page>: {
|
17
17
|
"page": {"id": 1},
|
@@ -21,9 +21,14 @@ describe GraphQL::Parser do
|
|
21
21
|
"page": {"id": 1},
|
22
22
|
"person" : { "id", 4}
|
23
23
|
}
|
24
|
+
|
25
|
+
$pageFragment: {
|
26
|
+
page { id }
|
27
|
+
}
|
24
28
|
})
|
25
29
|
end
|
26
30
|
end
|
31
|
+
|
27
32
|
describe 'field' do
|
28
33
|
let(:field) { parser.field }
|
29
34
|
it 'finds words' do
|
@@ -94,6 +99,10 @@ describe GraphQL::Parser do
|
|
94
99
|
assert node.parse_with_debug("viewer() {id}")
|
95
100
|
end
|
96
101
|
|
102
|
+
it 'parses query fragments' do
|
103
|
+
assert node.parse_with_debug("viewer() { id, $someFrag }")
|
104
|
+
end
|
105
|
+
|
97
106
|
it 'parses nested nodes' do
|
98
107
|
assert node.parse_with_debug("
|
99
108
|
node(someone)
|
@@ -136,4 +145,24 @@ describe GraphQL::Parser do
|
|
136
145
|
})
|
137
146
|
end
|
138
147
|
end
|
148
|
+
|
149
|
+
describe 'fragment' do
|
150
|
+
let(:fragment) { parser.fragment }
|
151
|
+
|
152
|
+
it 'gets parts of queries' do
|
153
|
+
assert fragment.parse_with_debug(%{ $frag: { id } })
|
154
|
+
end
|
155
|
+
|
156
|
+
it 'gets nested parts of queries' do
|
157
|
+
assert fragment.parse_with_debug(%{
|
158
|
+
$frag: {
|
159
|
+
item {
|
160
|
+
name,
|
161
|
+
price
|
162
|
+
},
|
163
|
+
$qtyFragment
|
164
|
+
}
|
165
|
+
})
|
166
|
+
end
|
167
|
+
end
|
139
168
|
end
|