graphql 0.0.1 → 0.0.2
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 +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
|
+
[](https://travis-ci.org/rmosolgo/graphql-ruby)
|
4
|
+
[](https://rubygems.org/gems/graphql)
|
5
|
+
[](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
|
+

|
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?
|