graphql 0.0.1 → 0.0.2
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 +78 -9
- data/lib/graphql/call.rb +7 -0
- data/lib/graphql/connection.rb +44 -0
- data/lib/graphql/field.rb +117 -0
- data/lib/graphql/field_definer.rb +29 -0
- data/lib/graphql/introspection/call_node.rb +13 -0
- data/lib/graphql/introspection/connection.rb +9 -0
- data/lib/graphql/introspection/field_node.rb +19 -0
- data/lib/graphql/introspection/root_call_argument_node.rb +5 -0
- data/lib/graphql/introspection/root_call_node.rb +16 -0
- data/lib/graphql/introspection/schema_call.rb +8 -0
- data/lib/graphql/introspection/schema_node.rb +17 -0
- data/lib/graphql/introspection/type_call.rb +8 -0
- data/lib/graphql/introspection/type_node.rb +16 -0
- data/lib/graphql/node.rb +141 -34
- data/lib/graphql/parser.rb +19 -8
- data/lib/graphql/query.rb +64 -21
- data/lib/graphql/root_call.rb +176 -0
- data/lib/graphql/root_call_argument.rb +8 -0
- data/lib/graphql/root_call_argument_definer.rb +20 -0
- data/lib/graphql/schema.rb +99 -0
- data/lib/graphql/syntax/call.rb +3 -12
- data/lib/graphql/syntax/field.rb +4 -2
- data/lib/graphql/syntax/node.rb +3 -10
- data/lib/graphql/syntax/query.rb +7 -0
- data/lib/graphql/syntax/variable.rb +7 -0
- data/lib/graphql/transform.rb +14 -5
- data/lib/graphql/types/boolean_field.rb +3 -0
- data/lib/graphql/types/connection_field.rb +30 -0
- data/lib/graphql/types/cursor_field.rb +9 -0
- data/lib/graphql/types/number_field.rb +3 -0
- data/lib/graphql/types/object_field.rb +8 -0
- data/lib/graphql/types/string_field.rb +3 -0
- data/lib/graphql/types/type_field.rb +6 -0
- data/lib/graphql/version.rb +3 -0
- data/readme.md +142 -10
- data/spec/graphql/field_spec.rb +66 -0
- data/spec/graphql/node_spec.rb +68 -0
- data/spec/graphql/parser_spec.rb +75 -25
- data/spec/graphql/query_spec.rb +185 -83
- data/spec/graphql/root_call_spec.rb +55 -0
- data/spec/graphql/schema_spec.rb +128 -0
- data/spec/graphql/transform_spec.rb +124 -39
- data/spec/spec_helper.rb +2 -1
- data/spec/support/dummy_app.rb +43 -23
- data/spec/support/nodes.rb +145 -32
- metadata +78 -16
- data/lib/graphql/collection_edge.rb +0 -62
- data/lib/graphql/syntax/edge.rb +0 -12
@@ -0,0 +1,20 @@
|
|
1
|
+
class GraphQL::RootCallArgumentDefiner
|
2
|
+
ARGUMENT_TYPES = [:string, :object, :number]
|
3
|
+
|
4
|
+
attr_reader :arguments
|
5
|
+
|
6
|
+
def initialize(call_class)
|
7
|
+
@call_class = call_class
|
8
|
+
@arguments = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def none
|
12
|
+
@arguments = []
|
13
|
+
end
|
14
|
+
|
15
|
+
ARGUMENT_TYPES.each do |arg_type|
|
16
|
+
define_method arg_type do |name, any_number: false|
|
17
|
+
@arguments << GraphQL::RootCallArgument.new(type: arg_type.to_s, name: name.to_s, any_number: any_number)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# {GraphQL::SCHEMA} keeps track of defined nodes, fields and calls.
|
2
|
+
# Although you don't interact with it directly, it responds to queries for `schema()` and `__type__` info.
|
3
|
+
|
4
|
+
class GraphQL::Schema
|
5
|
+
include Singleton
|
6
|
+
attr_reader :types, :calls, :fields, :class_names, :connections
|
7
|
+
def initialize
|
8
|
+
@types = {}
|
9
|
+
@connections = {}
|
10
|
+
@fields = {}
|
11
|
+
@class_names = {}
|
12
|
+
@calls = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_call(call_class)
|
16
|
+
remove_call(call_class)
|
17
|
+
@calls[call_class.schema_name] = call_class
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_call(identifier)
|
21
|
+
@calls[identifier.to_s] || raise(GraphQL::RootCallNotDefinedError.new(identifier))
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove_call(call_class)
|
25
|
+
existing_name = @calls.key(call_class)
|
26
|
+
if existing_name
|
27
|
+
@calls.delete(existing_name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def call_names
|
32
|
+
@calls.keys
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_type(node_class)
|
36
|
+
existing_name = @types.key(node_class)
|
37
|
+
if existing_name
|
38
|
+
@types.delete(existing_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
@class_names[node_class.ruby_class_name] = node_class
|
42
|
+
@types[node_class.schema_name] = node_class
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_type(identifier)
|
46
|
+
@types[identifier.to_s] || raise(GraphQL::NodeNotDefinedError.new(identifier))
|
47
|
+
end
|
48
|
+
|
49
|
+
def type_names
|
50
|
+
@types.keys
|
51
|
+
end
|
52
|
+
|
53
|
+
def type_for_object(app_object)
|
54
|
+
registered_class_names = @class_names.keys
|
55
|
+
if app_object.is_a?(Class)
|
56
|
+
app_class = app_object
|
57
|
+
else
|
58
|
+
app_class = app_object.class
|
59
|
+
end
|
60
|
+
app_class.ancestors.map(&:name).each do |class_name|
|
61
|
+
if registered_class_names.include?(class_name)
|
62
|
+
return @class_names[class_name]
|
63
|
+
end
|
64
|
+
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
|
98
|
+
end
|
99
|
+
end
|
data/lib/graphql/syntax/call.rb
CHANGED
@@ -1,17 +1,8 @@
|
|
1
1
|
class GraphQL::Syntax::Call
|
2
|
-
attr_reader :identifier, :
|
3
|
-
def initialize(identifier:,
|
2
|
+
attr_reader :identifier, :arguments, :calls
|
3
|
+
def initialize(identifier:, arguments: nil, calls: [])
|
4
4
|
@identifier = identifier
|
5
|
-
@
|
5
|
+
@arguments = arguments
|
6
6
|
@calls = calls
|
7
7
|
end
|
8
|
-
|
9
|
-
def execute!(query)
|
10
|
-
node_class = query.get_node(identifier)
|
11
|
-
node_class.call(argument)
|
12
|
-
end
|
13
|
-
|
14
|
-
def to_query
|
15
|
-
(["#{identifier}(#{argument})"] + calls.map(&:to_query)).join(".")
|
16
|
-
end
|
17
8
|
end
|
data/lib/graphql/syntax/field.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
class GraphQL::Syntax::Field
|
2
|
-
attr_reader :identifier, :calls
|
3
|
-
def initialize(identifier:, calls: [])
|
2
|
+
attr_reader :identifier, :alias_name, :calls, :fields
|
3
|
+
def initialize(identifier:, alias_name: nil, calls: [], fields: [])
|
4
4
|
@identifier = identifier
|
5
|
+
@alias_name = alias_name
|
5
6
|
@calls = calls
|
7
|
+
@fields = fields
|
6
8
|
end
|
7
9
|
end
|
data/lib/graphql/syntax/node.rb
CHANGED
@@ -1,15 +1,8 @@
|
|
1
1
|
class GraphQL::Syntax::Node
|
2
|
-
attr_reader :identifier, :
|
3
|
-
def initialize(identifier:,
|
2
|
+
attr_reader :identifier, :arguments, :fields
|
3
|
+
def initialize(identifier:, arguments:, fields: [])
|
4
4
|
@identifier = identifier
|
5
|
-
@
|
5
|
+
@arguments = arguments
|
6
6
|
@fields = fields
|
7
7
|
end
|
8
|
-
|
9
|
-
def execute!(query)
|
10
|
-
obj = identifier.execute!(query)
|
11
|
-
fields.each do |field|
|
12
|
-
obj.apply_field(field)
|
13
|
-
end
|
14
|
-
end
|
15
8
|
end
|
data/lib/graphql/transform.rb
CHANGED
@@ -1,10 +1,19 @@
|
|
1
1
|
class GraphQL::Transform < Parslet::Transform
|
2
|
+
# query
|
3
|
+
rule(nodes: sequence(:n), variables: sequence(:v)) { GraphQL::Syntax::Query.new(nodes: n, variables: v)}
|
2
4
|
# node
|
3
|
-
rule(identifier: simple(:i),
|
4
|
-
# edge
|
5
|
-
rule(identifier: simple(:i), calls: sequence(:c), fields: sequence(:f)) { GraphQL::Syntax::Edge.new(identifier: i.to_s, fields: f, calls: c)}
|
5
|
+
rule(identifier: simple(:i), arguments: sequence(:a), fields: sequence(:f)) {GraphQL::Syntax::Node.new(identifier: i.to_s, arguments: a, fields: f)}
|
6
6
|
# field
|
7
|
-
rule(identifier: simple(:i)) { GraphQL::Syntax::Field.new(identifier: i.to_s)}
|
7
|
+
rule(identifier: simple(:i), calls: sequence(:c), fields: sequence(:f), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, fields: f, calls: c, alias_name: a.to_s)}
|
8
|
+
rule(identifier: simple(:i), calls: sequence(:c), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, calls: c, alias_name: a.to_s)}
|
9
|
+
rule(identifier: simple(:i), calls: sequence(:c), fields: sequence(:f)) { GraphQL::Syntax::Field.new(identifier: i.to_s, fields: f, calls: c)}
|
10
|
+
rule(identifier: simple(:i), calls: sequence(:c)) { GraphQL::Syntax::Field.new(identifier: i.to_s, calls: c)}
|
11
|
+
rule(identifier: simple(:i), alias_name: simple(:a)) { GraphQL::Syntax::Field.new(identifier: i.to_s, alias_name: a.to_s)}
|
8
12
|
# call
|
9
|
-
rule(identifier: simple(:i),
|
13
|
+
rule(identifier: simple(:i), arguments: sequence(:a)) { GraphQL::Syntax::Call.new(identifier: i.to_s, arguments: a) }
|
14
|
+
# argument
|
15
|
+
rule(argument: simple(:a)) { a.to_s }
|
16
|
+
rule(identifier: simple(:i)) { i.to_s }
|
17
|
+
# variable
|
18
|
+
rule(identifier: simple(:i), json_string: simple(:j)) { GraphQL::Syntax::Variable.new(identifier: i.to_s, json_string: j.to_s)}
|
10
19
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class GraphQL::Types::ConnectionField < GraphQL::Field
|
2
|
+
type "connection"
|
3
|
+
|
4
|
+
["connection_class_name", "node_class_name"].each do |method_name|
|
5
|
+
define_method(method_name) do
|
6
|
+
const_get(method_name.upcase)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def connection_class
|
11
|
+
if connection_class_name.present?
|
12
|
+
Object.const_get(connection_class_name)
|
13
|
+
else
|
14
|
+
GraphQL::SCHEMA.get_connection(name)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def as_node
|
19
|
+
items = finished_value
|
20
|
+
connection_class.new(
|
21
|
+
items,
|
22
|
+
query: query,
|
23
|
+
fields: fields,
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def as_result
|
28
|
+
as_node.as_result
|
29
|
+
end
|
30
|
+
end
|
data/readme.md
CHANGED
@@ -1,18 +1,150 @@
|
|
1
1
|
# graphql
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
-
|
3
|
+
[![Build Status](https://travis-ci.org/rmosolgo/graphql-ruby.svg?branch=master)](https://travis-ci.org/rmosolgo/graphql-ruby)
|
4
|
+
[![Gem Version](https://badge.fury.io/rb/graphql.svg)](https://rubygems.org/gems/graphql)
|
5
|
+
[![Dependency Status](https://gemnasium.com/rmosolgo/graphql-ruby.svg)](https://gemnasium.com/rmosolgo/graphql-ruby)
|
6
6
|
|
7
|
-
See `/spec/support/dummy_app/nodes.rb` for node examples
|
8
7
|
|
9
|
-
|
8
|
+
Create a GraphQL interface by implementing _nodes_ and _connections_, then running queries.
|
10
9
|
|
11
|
-
|
10
|
+
## To do:
|
12
11
|
|
13
12
|
|
14
|
-
|
13
|
+
- Allow a default connection class, or some way to infer connection from name
|
14
|
+
- right now, `Introspection::Connection` isn't getting used, only `ApplicationConnection` is.
|
15
|
+
|
16
|
+
- How do you express failure? HTTP response? `errors` key?
|
17
|
+
- Handle blank objects in nested calls
|
18
|
+
- Implement calls as arguments
|
19
|
+
- double-check how to handle `pals.first(3) { count }`
|
20
|
+
- Implement call argument introspection (wait for spec)
|
21
|
+
- For fields that return objects, can they be queried _without_ other fields? Or must they always have fields?
|
22
|
+
- __document__ (wait for spec)
|
23
|
+
|
24
|
+
## Example Implementation
|
25
|
+
|
26
|
+
- See test implementation in [`/spec/support/dummy_app/nodes.rb`](https://github.com/rmosolgo/graphql/blob/master/spec/support/nodes.rb)
|
27
|
+
- See `graphql-ruby-demo` with Rails on [github](https://github.com/rmosolgo/graphql-ruby-demo) or [heroku](http://graphql-ruby-demo.herokuapp.com/)
|
28
|
+
|
29
|
+
![gql](https://cloud.githubusercontent.com/assets/2231765/6217972/5d24edda-b5ce-11e4-9e07-3548304af862.png)
|
30
|
+
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
- Implement _nodes_ that wrap objects in your application
|
35
|
+
- Implement _calls_ that return those objects (and may mutate the application state)
|
36
|
+
- Execute _queries_ and return the result.
|
37
|
+
|
38
|
+
### Nodes
|
39
|
+
|
40
|
+
Nodes are delegators that wrap objects in your app. You must whitelist fields by declaring them in the class definition.
|
41
|
+
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class FishNode < GraphQL::Node
|
45
|
+
exposes "Fish"
|
46
|
+
cursor(:id)
|
47
|
+
field.number(:id)
|
48
|
+
field.string(:name)
|
49
|
+
field.string(:species)
|
50
|
+
field.object(: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
|
+
### Calls
|
67
|
+
|
68
|
+
Calls selectively expose your application to the world. They always return values and they may perform mutations.
|
69
|
+
|
70
|
+
Calls declare returns, declare arguments, and implement `#execute!`.
|
71
|
+
|
72
|
+
This call just finds values:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class FindFishCall < GraphQL::RootCall
|
76
|
+
returns :fish
|
77
|
+
argument.number(:id)
|
78
|
+
def execute!(id)
|
79
|
+
Fish.find(id)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
83
|
+
|
84
|
+
This call performs a mutation:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
class RelocateFishCall < GraphQL::RootCall
|
88
|
+
returns :fish, :previous_aquarium, :new_aquarium
|
89
|
+
argument.number(:fish_id)
|
90
|
+
argument.number(:new_aquarium_id)
|
91
|
+
|
92
|
+
def execute!(fish_id, new_aquarium_id)
|
93
|
+
fish = Fish.find(fish_id)
|
94
|
+
|
95
|
+
# context is defined by the query, see below
|
96
|
+
if !context[:user].can_move?(fish)
|
97
|
+
raise RelocateNotAllowedError
|
98
|
+
end
|
99
|
+
|
100
|
+
previous_aquarium = fish.aquarium
|
101
|
+
new_aquarium = Aquarium.find(new_aquarium_id)
|
102
|
+
fish.update_attributes(aquarium: new_aquarium)
|
103
|
+
{
|
104
|
+
fish: fish,
|
105
|
+
previous_aquarium: previous_aquarium,
|
106
|
+
new_aquarium: new_aquarium,
|
107
|
+
}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
```
|
111
|
+
|
112
|
+
### Queries
|
113
|
+
|
114
|
+
When your system is setup, you can perform queries from a string.
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
query_str = "find_fish(1) { name, species } "
|
118
|
+
query = GraphQL::Query.new(query_str)
|
119
|
+
result = query.as_result
|
120
|
+
|
121
|
+
result
|
122
|
+
# {
|
123
|
+
# "1" => {
|
124
|
+
# "name" => "Sharky",
|
125
|
+
# "species" => "Goldfish",
|
126
|
+
# }
|
127
|
+
# }
|
128
|
+
```
|
129
|
+
|
130
|
+
Each query may also define a `context` object which will be accessible at every point in execution.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
query_str = "move_fish(1, 3) { fish { name }, new_aquarium { occupancy } }"
|
134
|
+
query_ctx = {user: current_user, request: request}
|
135
|
+
query = GraphQL::Query.new(query_str, context: query_ctx)
|
136
|
+
result = query.as_result
|
137
|
+
|
138
|
+
result
|
139
|
+
# {
|
140
|
+
# "fish" => {
|
141
|
+
# "name" => "Sharky"
|
142
|
+
# },
|
143
|
+
# "new_aquarium" => {
|
144
|
+
# "occupancy" => 12
|
145
|
+
# }
|
146
|
+
# }
|
147
|
+
```
|
148
|
+
|
149
|
+
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).
|
15
150
|
|
16
|
-
- Better class inference. Declaring edge classes is stupid.
|
17
|
-
- How to authenticate?
|
18
|
-
- What do graphql mutation queries even look like?
|