graphql 0.0.3 → 0.0.4
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/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
|
[](https://gemnasium.com/rmosolgo/graphql-ruby)
|
6
6
|
[](https://codeclimate.com/github/rmosolgo/graphql-ruby)
|
7
7
|
[](https://codeclimate.com/github/rmosolgo/graphql-ruby)
|
8
|
-
](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
|