graphql-relay 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 +7 -0
- data/README.md +73 -0
- data/lib/graphql/definition_helpers/defined_by_config/definition_config.rb +17 -0
- data/lib/graphql/relay.rb +12 -0
- data/lib/graphql/relay/array_connection.rb +48 -0
- data/lib/graphql/relay/base_connection.rb +83 -0
- data/lib/graphql/relay/connection_field.rb +23 -0
- data/lib/graphql/relay/connection_type.rb +10 -0
- data/lib/graphql/relay/edge.rb +26 -0
- data/lib/graphql/relay/global_id_field.rb +18 -0
- data/lib/graphql/relay/mutation.rb +128 -0
- data/lib/graphql/relay/node.rb +64 -0
- data/lib/graphql/relay/page_info.rb +11 -0
- data/lib/graphql/relay/relation_connection.rb +70 -0
- data/lib/graphql/relay/version.rb +5 -0
- data/spec/graphql/relay/array_connection_spec.rb +75 -0
- data/spec/graphql/relay/mutation_spec.rb +34 -0
- data/spec/graphql/relay/node_spec.rb +26 -0
- data/spec/graphql/relay/relation_connection_spec.rb +77 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/star_wars_data.rb +68 -0
- data/spec/support/star_wars_schema.rb +114 -0
- metadata +240 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5fc4e49e9a152eb3d06a00ea6dc479ca20ad3b85
|
4
|
+
data.tar.gz: 3babba6d3a517b0d0a0da7afced0e005dcfa75f1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f84efd30fb1fe8cdd776656e5d8e3bba8ec2ff2cecdaf403fa2652dcc1d4bfced113b6535d89cdc51bf6f2033fc3702f284fc8c758f5329affaefedb4bb7dd3f
|
7
|
+
data.tar.gz: 9331b1c45d8401c462192fd5c431736dbd4924f812223781827dc145a4b7c2eb89e127431156ee8667e14aaae64f7d6466ec1cbc3f5e9a414f693efbbedde098
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# graphql-relay
|
2
|
+
|
3
|
+
Helpers for using [`graphql`](https://github.com/rmosolgo/graphql-ruby) with Relay.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem "graphql-relay"
|
9
|
+
```
|
10
|
+
|
11
|
+
```
|
12
|
+
bundle install
|
13
|
+
```
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
### Global Ids
|
18
|
+
|
19
|
+
Global Ids provide refetching & global identification for Relay.
|
20
|
+
|
21
|
+
You should implement an object that responds to `#object_from_id(global_id)` & `#type_from_object(object)`, then pass it to `GraphQL::Relay::Node.create(implementation)`. [Example](https://github.com/rmosolgo/graphql-relay-ruby/blob/120b750cf86f1eb5c9997b588f022b2ef3a0012c/spec/support/star_wars_schema.rb#L4-L15)
|
22
|
+
|
23
|
+
Then, you can add global id fields to your types with `GraphQL::Relay::GlobalIdField.new(type_name)`. [Example](https://github.com/rmosolgo/graphql-relay-ruby/blob/120b750cf86f1eb5c9997b588f022b2ef3a0012c/spec/support/star_wars_schema.rb#L22)
|
24
|
+
|
25
|
+
### Connections
|
26
|
+
|
27
|
+
Connections will provide arguments, pagination and `pageInfo` for `Array`s or `ActiveRecord::Relation`s.
|
28
|
+
|
29
|
+
To create a connection, you should:
|
30
|
+
- create a connection type; then
|
31
|
+
- implement the field to return objects
|
32
|
+
|
33
|
+
To create a connection type, use either `GraphQL::Relay::ArrayConnection.create_type(base_type)` or for an `ActiveRecord::Relation`, use `GraphQL::Relay::RelationConnection.create_type(base_type)`. [Array example](https://github.com/rmosolgo/graphql-relay-ruby/blob/120b750cf86f1eb5c9997b588f022b2ef3a0012c/spec/support/star_wars_schema.rb#L27), [Relation example](https://github.com/rmosolgo/graphql-relay-ruby/blob/120b750cf86f1eb5c9997b588f022b2ef3a0012c/spec/support/star_wars_schema.rb#L39)
|
34
|
+
|
35
|
+
Then, implement the field. It's different than a normal field:
|
36
|
+
- use the `connection` helper to define it, instead of `field`
|
37
|
+
- use the newly-created connection type as the field's return type
|
38
|
+
- implement `resolve` to return an Array or an ActiveRecord::Relation, depending on the connection type.
|
39
|
+
|
40
|
+
[Example](https://github.com/rmosolgo/graphql-relay-ruby/blob/120b750cf86f1eb5c9997b588f022b2ef3a0012c/spec/support/star_wars_schema.rb#L48-L61)
|
41
|
+
|
42
|
+
### Mutations
|
43
|
+
|
44
|
+
Mutations allow Relay to mutate your system. When you define a mutation, you'll be defining:
|
45
|
+
- A field for your schema's `mutation` root
|
46
|
+
- A derived `InputObjectType` for input values
|
47
|
+
- A derived `ObjectType` for return values
|
48
|
+
|
49
|
+
You _don't_ define anything having to do with `clientMutationId`. That's automatically created.
|
50
|
+
|
51
|
+
To define a mutation, use `GraphQL::Relay::Mutation.define`. Inside the block, you should configure:
|
52
|
+
- `name`, which will name the mutation field & derived types
|
53
|
+
- `input_field`s, which will be applied to the derived `InputObjectType`
|
54
|
+
- `return_field`s, which will be applied to the derived `ObjectType`
|
55
|
+
- `resolve(-> (inputs, ctx))`, the mutation which will actually happen
|
56
|
+
|
57
|
+
The resolve proc:
|
58
|
+
- Takes `inputs`, which is a hash whose keys are the ones defined by `input_field`
|
59
|
+
- Takes `ctx`, which is the query context you passed with the `context:` keyword
|
60
|
+
- Must return a hash with keys matching your defined `return_field`s
|
61
|
+
|
62
|
+
Examples:
|
63
|
+
- Definition: [example](https://github.com/rmosolgo/graphql-relay-ruby/blob/120b750cf86f1eb5c9997b588f022b2ef3a0012c/spec/support/star_wars_schema.rb#L74-L93)
|
64
|
+
- Mount on mutation type: [example](https://github.com/rmosolgo/graphql-relay-ruby/blob/120b750cf86f1eb5c9997b588f022b2ef3a0012c/spec/support/star_wars_schema.rb#L111)
|
65
|
+
|
66
|
+
## Todo
|
67
|
+
|
68
|
+
- [ ] pluralIdentifyingRootField
|
69
|
+
|
70
|
+
## More Resources
|
71
|
+
|
72
|
+
- [`graphql`](https://github.com/rmosolgo/graphql-ruby) Ruby gem
|
73
|
+
- [`graphql-relay-js`](https://github.com/graphql/graphql-relay-js) JavaScript helpers for GraphQL and Relay
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module DefinitionHelpers
|
3
|
+
module DefinedByConfig
|
4
|
+
class DefinitionConfig
|
5
|
+
# Wraps a field definition with a ConnectionField
|
6
|
+
def connection(name, type = nil, desc = nil, property: nil, &block)
|
7
|
+
underlying_field = field(name, type, desc, property: property, &block)
|
8
|
+
connection_field = GraphQL::Relay::ConnectionField.create(underlying_field)
|
9
|
+
fields[name.to_s] = connection_field
|
10
|
+
end
|
11
|
+
|
12
|
+
alias :return_field :field
|
13
|
+
alias :return_fields :fields
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'graphql/definition_helpers/defined_by_config/definition_config'
|
3
|
+
require 'graphql/relay/node'
|
4
|
+
require 'graphql/relay/page_info'
|
5
|
+
require 'graphql/relay/edge'
|
6
|
+
require 'graphql/relay/connection_type'
|
7
|
+
require 'graphql/relay/base_connection'
|
8
|
+
require 'graphql/relay/array_connection'
|
9
|
+
require 'graphql/relay/relation_connection'
|
10
|
+
require 'graphql/relay/global_id_field'
|
11
|
+
require 'graphql/relay/connection_field'
|
12
|
+
require 'graphql/relay/mutation'
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
class ArrayConnection < BaseConnection
|
4
|
+
# Just to encode data in the cursor, use something that won't conflict
|
5
|
+
CURSOR_SEPARATOR = "---"
|
6
|
+
|
7
|
+
def cursor_from_node(item)
|
8
|
+
idx = sliced_nodes.find_index(item)
|
9
|
+
cursor_parts = [(order || "none"), idx]
|
10
|
+
Base64.strict_encode64(cursor_parts.join(CURSOR_SEPARATOR))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
# apply first / last limit results
|
16
|
+
def paged_nodes
|
17
|
+
@paged_nodes = begin
|
18
|
+
items = sliced_nodes
|
19
|
+
first && items = items.first(first)
|
20
|
+
last && items.length > last && items.last(last)
|
21
|
+
items
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Apply cursors to edges
|
26
|
+
def sliced_nodes
|
27
|
+
@sliced_nodes ||= begin
|
28
|
+
items = object
|
29
|
+
if order
|
30
|
+
# Remove possible direction marker:
|
31
|
+
order_name = order.sub(/^-/, '')
|
32
|
+
items = items.sort_by { |item| item.public_send(order_name) }
|
33
|
+
order.start_with?("-") && items = items.reverse
|
34
|
+
end
|
35
|
+
after && items = items[(1 + index_from_cursor(after))..-1]
|
36
|
+
before && items = items[0..(index_from_cursor(before) - 1)]
|
37
|
+
items
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def index_from_cursor(cursor)
|
42
|
+
decoded = Base64.decode64(cursor)
|
43
|
+
order, index = decoded.split(CURSOR_SEPARATOR)
|
44
|
+
index.to_i
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# Subclasses must implement:
|
4
|
+
# - {#cursor_from_node}, which returns an opaque cursor for the given item
|
5
|
+
# - {#sliced_edges}, which slices by `before` & `after`
|
6
|
+
# - {#paged_edges}, which applies `first` & `last` limits
|
7
|
+
#
|
8
|
+
# In a subclass, you have access to
|
9
|
+
# - {#object}, the object which the connection will wrap
|
10
|
+
# - {#first}, {#after}, {#last}, {#before} (arguments passed to the field)
|
11
|
+
#
|
12
|
+
class BaseConnection
|
13
|
+
# Just to encode data in the cursor, use something that won't conflict
|
14
|
+
CURSOR_SEPARATOR = "---"
|
15
|
+
|
16
|
+
# Create a connection which exposes edges of this type
|
17
|
+
def self.create_type(wrapped_type)
|
18
|
+
edge_type = Edge.create_type(wrapped_type)
|
19
|
+
|
20
|
+
connection_type = ConnectionType.define do
|
21
|
+
name("#{wrapped_type.name}Connection")
|
22
|
+
field :edges, types[edge_type]
|
23
|
+
field :pageInfo, PageInfo, property: :page_info
|
24
|
+
end
|
25
|
+
|
26
|
+
connection_type.connection_class = self
|
27
|
+
|
28
|
+
connection_type
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :object, :arguments
|
32
|
+
|
33
|
+
def initialize(object, arguments)
|
34
|
+
@object = object
|
35
|
+
@arguments = arguments
|
36
|
+
end
|
37
|
+
|
38
|
+
# Provide easy access to provided arguments:
|
39
|
+
METHODS_FROM_ARGUMENTS = [:first, :after, :last, :before, :order]
|
40
|
+
|
41
|
+
METHODS_FROM_ARGUMENTS.each do |arg_name|
|
42
|
+
define_method(arg_name) do
|
43
|
+
arguments[arg_name]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Wrap nodes in {Edge}s so they expose cursors.
|
48
|
+
def edges
|
49
|
+
@edges ||= paged_nodes.map { |item| Edge.new(item, self) }
|
50
|
+
end
|
51
|
+
|
52
|
+
# Support the `pageInfo` field
|
53
|
+
def page_info
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
# Used by `pageInfo`
|
58
|
+
def has_next_page
|
59
|
+
first && sliced_nodes.count > first
|
60
|
+
end
|
61
|
+
|
62
|
+
# Used by `pageInfo`
|
63
|
+
def has_previous_page
|
64
|
+
last && sliced_nodes.count > last
|
65
|
+
end
|
66
|
+
|
67
|
+
# An opaque operation which returns a connection-specific cursor.
|
68
|
+
def cursor_from_node(object)
|
69
|
+
raise NotImplementedError, "must return a cursor for this object/connection pair"
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def paged_nodes
|
75
|
+
raise NotImplementedError, "must items for this connection after paging"
|
76
|
+
end
|
77
|
+
|
78
|
+
def sliced_nodes
|
79
|
+
raise NotImplementedError, "must all items for this connection after chopping off first and last"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# The best way to make these is with the connection helper,
|
4
|
+
# @see {GraphQL::DefinitionHelpers::DefinedByConfig::DefinitionConfig}
|
5
|
+
class ConnectionField
|
6
|
+
def self.create(underlying_field)
|
7
|
+
field = GraphQL::Field.define do
|
8
|
+
argument :first, types.Int
|
9
|
+
argument :after, types.String
|
10
|
+
argument :last, types.Int
|
11
|
+
argument :before, types.String
|
12
|
+
argument :order, types.String
|
13
|
+
|
14
|
+
type(-> { underlying_field.type })
|
15
|
+
resolve -> (obj, args, ctx) {
|
16
|
+
items = underlying_field.resolve(obj, args, ctx)
|
17
|
+
underlying_field.type.connection_class.new(items, args)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# An ObjectType which also stores its {#connection_class}
|
4
|
+
class ConnectionType < GraphQL::ObjectType
|
5
|
+
defined_by_config :name, :fields, :interfaces
|
6
|
+
# @return [Class] A subclass of {BaseConnection}
|
7
|
+
attr_accessor :connection_class
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# Mostly an internal concern.
|
4
|
+
#
|
5
|
+
# Wraps an object as a `node`, and exposes a connection-specific `cursor`.
|
6
|
+
class Edge < GraphQL::ObjectType
|
7
|
+
attr_reader :node
|
8
|
+
def initialize(node, connection)
|
9
|
+
@node = node
|
10
|
+
@connection = connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def cursor
|
14
|
+
@cursor ||= @connection.cursor_from_node(node)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create_type(wrapped_type)
|
18
|
+
GraphQL::ObjectType.define do
|
19
|
+
name("#{wrapped_type.name}Edge")
|
20
|
+
field :node, wrapped_type
|
21
|
+
field :cursor, !types.String
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# @example Create a field that returns the global ID for an object
|
4
|
+
# RestaurantType = GraphQL::ObjectType.define do
|
5
|
+
# name "Restaurant"
|
6
|
+
# field :id, field: GraphQL::Relay::GlobalIdField.new("Restaurant")
|
7
|
+
# end
|
8
|
+
class GlobalIdField < GraphQL::Field
|
9
|
+
def initialize(type_name, property: :id)
|
10
|
+
self.arguments = {}
|
11
|
+
self.type = !GraphQL::ID_TYPE
|
12
|
+
self.resolve = -> (obj, args, ctx) {
|
13
|
+
Node.to_global_id(type_name, obj.public_send(property))
|
14
|
+
}
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# Define a Relay mutation:
|
4
|
+
# - give it a name (used for derived inputs & outputs)
|
5
|
+
# - declare its inputs
|
6
|
+
# - declare its outputs
|
7
|
+
# - declare the mutation procedure
|
8
|
+
#
|
9
|
+
# `resolve` should return a hash with a key for each of the `return_field`s
|
10
|
+
#
|
11
|
+
# Inputs will also contain a `clientMutationId`
|
12
|
+
#
|
13
|
+
# @example Updating the name of an item
|
14
|
+
# UpdateNameMutation = GraphQL::Relay::Mutation.define do
|
15
|
+
# name "UpdateName"
|
16
|
+
#
|
17
|
+
# input_field :name, !types.String
|
18
|
+
# input_field :itemId, !types.ID
|
19
|
+
#
|
20
|
+
# return_field :item, Item
|
21
|
+
#
|
22
|
+
# resolve -> (inputs, ctx) {
|
23
|
+
# item = Item.find_by_id(inputs[:id])
|
24
|
+
# item.update(name: inputs[:name])
|
25
|
+
# {item: item}
|
26
|
+
# }
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# MutationType = GraphQL::ObjectType.define do
|
30
|
+
# # The mutation object exposes a field:
|
31
|
+
# field :updateName, UpdateNameMutation.field
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # Then query it:
|
35
|
+
# query_string = %|
|
36
|
+
# mutation updateName {
|
37
|
+
# updateName(input: {itemId: 1, name: "new name", clientMutationId: "1234"}) {
|
38
|
+
# item { name }
|
39
|
+
# clientMutationId
|
40
|
+
# }|
|
41
|
+
#
|
42
|
+
# GraphQL::Query.new(MySchema, query_string).result
|
43
|
+
# # {"data" => {
|
44
|
+
# # "updateName" => {
|
45
|
+
# # "item" => { "name" => "new name"},
|
46
|
+
# # "clientMutationId" => "1234"
|
47
|
+
# # }
|
48
|
+
# # }}
|
49
|
+
#
|
50
|
+
class Mutation
|
51
|
+
include GraphQL::DefinitionHelpers::DefinedByConfig
|
52
|
+
defined_by_config :name, :description, :return_fields, :input_fields, :resolve
|
53
|
+
attr_accessor :name, :description, :return_fields, :input_fields
|
54
|
+
|
55
|
+
def resolve=(proc)
|
56
|
+
@resolve_proc = proc
|
57
|
+
end
|
58
|
+
|
59
|
+
def field
|
60
|
+
@field ||= begin
|
61
|
+
field_return_type = self.return_type
|
62
|
+
field_input_type = self.input_type
|
63
|
+
field_resolve_proc = -> (obj, args, ctx){
|
64
|
+
results_hash = @resolve_proc.call(args[:input], ctx)
|
65
|
+
Result.new(arguments: args, result: results_hash)
|
66
|
+
}
|
67
|
+
GraphQL::Field.define do
|
68
|
+
type(field_return_type)
|
69
|
+
argument :input, !field_input_type
|
70
|
+
resolve(field_resolve_proc)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def return_type
|
76
|
+
@return_type ||= begin
|
77
|
+
mutation_name = name
|
78
|
+
type_name = "#{mutation_name}Payload"
|
79
|
+
type_fields = return_fields
|
80
|
+
GraphQL::ObjectType.define do
|
81
|
+
name(type_name)
|
82
|
+
description("Autogenerated return type of #{mutation_name}")
|
83
|
+
field :clientMutationId, !types.String
|
84
|
+
type_fields.each do |name, field_obj|
|
85
|
+
field name, field: field_obj
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def input_type
|
92
|
+
@input_type ||= begin
|
93
|
+
mutation_name = name
|
94
|
+
type_name = "#{mutation_name}Input"
|
95
|
+
type_fields = input_fields
|
96
|
+
GraphQL::InputObjectType.define do
|
97
|
+
name(type_name)
|
98
|
+
description("Autogenerated input type of #{mutation_name}")
|
99
|
+
input_field :clientMutationId, !types.String
|
100
|
+
type_fields.each do |name, field_obj|
|
101
|
+
input_field name, field_obj.type, field_obj.description, default_value: field_obj.default_value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Result
|
108
|
+
attr_reader :arguments, :result
|
109
|
+
def initialize(arguments:, result:)
|
110
|
+
@arguments = arguments
|
111
|
+
@result = result
|
112
|
+
end
|
113
|
+
|
114
|
+
def clientMutationId
|
115
|
+
arguments[:clientMutationId]
|
116
|
+
end
|
117
|
+
|
118
|
+
def method_missing(name, *args, &block)
|
119
|
+
if result.key?(name)
|
120
|
+
result[name]
|
121
|
+
else
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
module GraphQL
|
3
|
+
module Relay
|
4
|
+
# To get a `NodeField` and `NodeInterface`,
|
5
|
+
# define an object that responds to:
|
6
|
+
# - object_from_id
|
7
|
+
# - type_from_object
|
8
|
+
# and pass it to `Node.create`
|
9
|
+
#
|
10
|
+
class Node
|
11
|
+
include Singleton
|
12
|
+
|
13
|
+
# Allows you to call methods on the class
|
14
|
+
def self.method_missing(method_name, *args, &block)
|
15
|
+
if instance.respond_to?(method_name)
|
16
|
+
instance.send(method_name, *args, &block)
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Return interface and field using implementation
|
23
|
+
def create(implementation)
|
24
|
+
interface = create_interface(implementation)
|
25
|
+
field = create_field(implementation, interface)
|
26
|
+
[interface, field]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create a global ID for type-name & ID
|
30
|
+
# (This is an opaque transform)
|
31
|
+
def to_global_id(type_name, id)
|
32
|
+
Base64.strict_encode64("#{type_name}-#{id}")
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get type-name & ID from global ID
|
36
|
+
# (This reverts the opaque transform)
|
37
|
+
def from_global_id(global_id)
|
38
|
+
Base64.decode64(global_id).split("-")
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def create_interface(implementation)
|
44
|
+
GraphQL::InterfaceType.define do
|
45
|
+
name "Node"
|
46
|
+
field :id, !types.ID
|
47
|
+
resolve_type -> (obj) {
|
48
|
+
implementation.type_from_object(obj)
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_field(implementation, interface)
|
54
|
+
GraphQL::Field.define do
|
55
|
+
type(interface)
|
56
|
+
argument :id, !types.ID
|
57
|
+
resolve -> (obj, args, ctx) {
|
58
|
+
implementation.object_from_id(args[:id])
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
# Wrap a Connection and expose its page info
|
4
|
+
PageInfo = GraphQL::ObjectType.define do
|
5
|
+
name("PageInfo")
|
6
|
+
description("Metadata about a connection")
|
7
|
+
field :hasNextPage, !types.Boolean, property: :has_next_page
|
8
|
+
field :hasPreviousPage, !types.Boolean, property: :has_previous_page
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module GraphQL
|
2
|
+
module Relay
|
3
|
+
class RelationConnection < BaseConnection
|
4
|
+
def cursor_from_node(item)
|
5
|
+
order_value = item.public_send(order_name)
|
6
|
+
cursor_parts = [order, order_value]
|
7
|
+
Base64.strict_encode64(cursor_parts.join(CURSOR_SEPARATOR))
|
8
|
+
end
|
9
|
+
|
10
|
+
def order
|
11
|
+
@order ||= (super || "id")
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# apply first / last limit results
|
18
|
+
def paged_nodes
|
19
|
+
@paged_nodes = begin
|
20
|
+
items = sliced_nodes
|
21
|
+
first && items = items.first(first)
|
22
|
+
last && items.length > last && items.last(last)
|
23
|
+
items
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Apply cursors to edges
|
28
|
+
def sliced_nodes
|
29
|
+
@sliced_nodes ||= begin
|
30
|
+
items = object
|
31
|
+
|
32
|
+
if order
|
33
|
+
items = items.order(order_name => order_direction)
|
34
|
+
end
|
35
|
+
|
36
|
+
if after
|
37
|
+
_o, order_value = slice_from_cursor(after)
|
38
|
+
sort_query = order_direction == :asc ? "? > ?" : "? < ?"
|
39
|
+
puts sort_query, order_name, order_value
|
40
|
+
items = items.where(sort_query, order_name, order_value)
|
41
|
+
end
|
42
|
+
|
43
|
+
if before
|
44
|
+
_o, order_value = slice_from_cursor(before)
|
45
|
+
sort_query = order_direction == :asc ? "? < ?" : "? > ?"
|
46
|
+
p [sort_query, order_name, order_value]
|
47
|
+
items = items.where(sort_query, order_name, order_value)
|
48
|
+
end
|
49
|
+
|
50
|
+
items
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def slice_from_cursor(cursor)
|
55
|
+
decoded = Base64.decode64(cursor)
|
56
|
+
order, order_value = decoded.split(CURSOR_SEPARATOR)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Remove possible direction marker:
|
60
|
+
def order_name
|
61
|
+
@order_name ||= order.sub(/^-/, '')
|
62
|
+
end
|
63
|
+
|
64
|
+
# Check for direction marker
|
65
|
+
def order_direction
|
66
|
+
@order_direction ||= order.start_with?("-") ? :desc : :asc
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GraphQL::Relay::ArrayConnection do
|
4
|
+
def get_names(result)
|
5
|
+
ships = result["data"]["rebels"]["ships"]["edges"]
|
6
|
+
names = ships.map { |e| e["node"]["name"] }
|
7
|
+
end
|
8
|
+
describe "results" do
|
9
|
+
let(:query_string) {%|
|
10
|
+
query getShips($first: Int, $after: String, $last: Int, $before: String, $order: String){
|
11
|
+
rebels {
|
12
|
+
ships(first: $first, after: $after, last: $last, before: $before, order: $order) {
|
13
|
+
edges {
|
14
|
+
cursor
|
15
|
+
node {
|
16
|
+
name
|
17
|
+
}
|
18
|
+
}
|
19
|
+
pageInfo {
|
20
|
+
hasNextPage
|
21
|
+
}
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|}
|
26
|
+
it 'limits the result' do
|
27
|
+
result = query(query_string, "first" => 2)
|
28
|
+
number_of_ships = get_names(result).length
|
29
|
+
assert_equal(2, number_of_ships)
|
30
|
+
assert_equal(true, result["data"]["rebels"]["ships"]["pageInfo"]["hasNextPage"])
|
31
|
+
|
32
|
+
result = query(query_string, "first" => 3)
|
33
|
+
number_of_ships = get_names(result).length
|
34
|
+
assert_equal(3, number_of_ships)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'provides pageInfo' do
|
38
|
+
result = query(query_string, "first" => 2)
|
39
|
+
assert_equal(true, result["data"]["rebels"]["ships"]["pageInfo"]["hasNextPage"])
|
40
|
+
|
41
|
+
result = query(query_string, "first" => 100)
|
42
|
+
assert_equal(false, result["data"]["rebels"]["ships"]["pageInfo"]["hasNextPage"])
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'slices the result' do
|
46
|
+
result = query(query_string, "first" => 3)
|
47
|
+
assert_equal(["X-Wing", "Y-Wing", "A-Wing"], get_names(result))
|
48
|
+
|
49
|
+
# After the last result, find the next 2:
|
50
|
+
last_cursor = result["data"]["rebels"]["ships"]["edges"].last["cursor"]
|
51
|
+
|
52
|
+
result = query(query_string, "after" => last_cursor, "first" => 2)
|
53
|
+
assert_equal(["Millenium Falcon", "Home One"], get_names(result))
|
54
|
+
|
55
|
+
result = query(query_string, "before" => last_cursor, "last" => 2)
|
56
|
+
assert_equal(["X-Wing", "Y-Wing"], get_names(result))
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'paginates with order' do
|
60
|
+
result = query(query_string, "first" => 2, "order" => "name")
|
61
|
+
assert_equal(["A-Wing", "Home One"], get_names(result))
|
62
|
+
|
63
|
+
# After the last result, find the next 2:
|
64
|
+
last_cursor = result["data"]["rebels"]["ships"]["edges"].last["cursor"]
|
65
|
+
|
66
|
+
result = query(query_string, "after" => last_cursor, "first" => 2, "order" => "name")
|
67
|
+
assert_equal(["Millenium Falcon", "X-Wing"], get_names(result))
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'paginates with reverse order' do
|
71
|
+
result = query(query_string, "first" => 2, "order" => "-name")
|
72
|
+
assert_equal(["Y-Wing", "X-Wing"], get_names(result))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GraphQL::Relay::Mutation do
|
4
|
+
let(:query_string) {%|
|
5
|
+
mutation addBagel {
|
6
|
+
introduceShip(input: {shipName: "Bagel", factionId: "1", clientMutationId: "1234"}) {
|
7
|
+
clientMutationId
|
8
|
+
ship { name, id }
|
9
|
+
faction { name }
|
10
|
+
}
|
11
|
+
}
|
12
|
+
|}
|
13
|
+
let(:introspect) {%|
|
14
|
+
{
|
15
|
+
__schema {
|
16
|
+
types { name, fields { name } }
|
17
|
+
}
|
18
|
+
}
|
19
|
+
|}
|
20
|
+
|
21
|
+
it "returns the result & clientMutationId" do
|
22
|
+
result = query(query_string)
|
23
|
+
expected = {"data" => {
|
24
|
+
"introduceShip" => {
|
25
|
+
"clientMutationId" => "1234",
|
26
|
+
"ship" => {
|
27
|
+
"name" => "Bagel",
|
28
|
+
"id" => GraphQL::Relay::Node.to_global_id("Ship", "9"),
|
29
|
+
},
|
30
|
+
"faction" => {"name" => STAR_WARS_DATA["Faction"]["1"].name }
|
31
|
+
}
|
32
|
+
}}
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GraphQL::Relay::Node do
|
4
|
+
describe 'NodeField' do
|
5
|
+
it 'finds objects by id' do
|
6
|
+
global_id = GraphQL::Relay::Node.to_global_id("Ship", "1")
|
7
|
+
result = query(%|{node(id: "#{global_id}") { id, ... on Ship { name } }}|)
|
8
|
+
expected = {"data" => {
|
9
|
+
"node" => {
|
10
|
+
"id" => global_id,
|
11
|
+
"name" => "X-Wing"
|
12
|
+
}
|
13
|
+
}}
|
14
|
+
assert_equal(expected, result)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'to_global_id / from_global_id ' do
|
19
|
+
it 'Converts typename and ID to and from ID' do
|
20
|
+
global_id = GraphQL::Relay::Node.to_global_id("SomeType", "123")
|
21
|
+
type_name, id = GraphQL::Relay::Node.from_global_id(global_id)
|
22
|
+
assert_equal("SomeType", type_name)
|
23
|
+
assert_equal("123", id)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GraphQL::Relay::RelationConnection do
|
4
|
+
def get_names(result)
|
5
|
+
ships = result["data"]["empire"]["bases"]["edges"]
|
6
|
+
names = ships.map { |e| e["node"]["name"] }
|
7
|
+
end
|
8
|
+
|
9
|
+
def get_page_info(result)
|
10
|
+
result["data"]["empire"]["bases"]["pageInfo"]
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "results" do
|
14
|
+
let(:query_string) {%|
|
15
|
+
query getShips($first: Int, $after: String, $last: Int, $before: String, $order: String){
|
16
|
+
empire {
|
17
|
+
bases(first: $first, after: $after, last: $last, before: $before, order: $order) {
|
18
|
+
edges {
|
19
|
+
cursor
|
20
|
+
node {
|
21
|
+
name
|
22
|
+
}
|
23
|
+
}
|
24
|
+
pageInfo {
|
25
|
+
hasNextPage
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|}
|
31
|
+
it 'limits the result' do
|
32
|
+
result = query(query_string, "first" => 2)
|
33
|
+
assert_equal(2, get_names(result).length)
|
34
|
+
|
35
|
+
result = query(query_string, "first" => 3)
|
36
|
+
assert_equal(3, get_names(result).length)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'provides pageInfo' do
|
40
|
+
result = query(query_string, "first" => 2)
|
41
|
+
assert_equal(true, get_page_info(result)["hasNextPage"])
|
42
|
+
|
43
|
+
result = query(query_string, "first" => 100)
|
44
|
+
assert_equal(false, get_page_info(result)["hasNextPage"])
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'slices the result' do
|
48
|
+
result = query(query_string, "first" => 2)
|
49
|
+
assert_equal(["Death Star", "Shield Generator"], get_names(result))
|
50
|
+
|
51
|
+
# After the last result, find the next 2:
|
52
|
+
last_cursor = result["data"]["empire"]["bases"]["edges"].last["cursor"]
|
53
|
+
|
54
|
+
result = query(query_string, "after" => last_cursor, "first" => 2)
|
55
|
+
assert_equal(["Headquarters"], get_names(result))
|
56
|
+
|
57
|
+
result = query(query_string, "before" => last_cursor, "last" => 2)
|
58
|
+
assert_equal(["Death Star"], get_names(result))
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'paginates with order' do
|
62
|
+
result = query(query_string, "first" => 2, "order" => "name")
|
63
|
+
assert_equal(["Death Star", "Headquarters"], get_names(result))
|
64
|
+
|
65
|
+
# After the last result, find the next 2:
|
66
|
+
last_cursor = result["data"]["empire"]["bases"]["edges"].last["cursor"]
|
67
|
+
|
68
|
+
result = query(query_string, "after" => last_cursor, "first" => 2, "order" => "name")
|
69
|
+
assert_equal(["Shield Generator"], get_names(result))
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'paginates with reverse order' do
|
73
|
+
result = query(query_string, "first" => 2, "order" => "-name")
|
74
|
+
assert_equal(["Shield Generator", "Headquarters"], get_names(result))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "codeclimate-test-reporter"
|
2
|
+
CodeClimate::TestReporter.start
|
3
|
+
require "sqlite3"
|
4
|
+
require "active_record"
|
5
|
+
require "graphql"
|
6
|
+
require "graphql/relay"
|
7
|
+
require "minitest/autorun"
|
8
|
+
require "minitest/focus"
|
9
|
+
require "minitest/reporters"
|
10
|
+
require 'pry'
|
11
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
12
|
+
# Filter out Minitest backtrace while allowing backtrace from other libraries to be shown.
|
13
|
+
Minitest.backtrace_filter = Minitest::BacktraceFilter.new
|
14
|
+
|
15
|
+
# Load support files
|
16
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
17
|
+
|
18
|
+
|
19
|
+
def query(string, variables={})
|
20
|
+
GraphQL::Query.new(StarWarsSchema, string, variables: variables).result
|
21
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
|
2
|
+
names = [
|
3
|
+
'X-Wing',
|
4
|
+
'Y-Wing',
|
5
|
+
'A-Wing',
|
6
|
+
'Millenium Falcon',
|
7
|
+
'Home One',
|
8
|
+
'TIE Fighter',
|
9
|
+
'TIE Interceptor',
|
10
|
+
'Executor',
|
11
|
+
]
|
12
|
+
|
13
|
+
rebels = OpenStruct.new({
|
14
|
+
id: '1',
|
15
|
+
name: 'Alliance to Restore the Republic',
|
16
|
+
ships: ['1', '2', '3', '4', '5'],
|
17
|
+
bases: ['11', '12']
|
18
|
+
})
|
19
|
+
|
20
|
+
|
21
|
+
empire = OpenStruct.new({
|
22
|
+
id: '2',
|
23
|
+
name: 'Galactic Empire',
|
24
|
+
ships: ['6', '7', '8'],
|
25
|
+
bases: ['13', '14', '15']
|
26
|
+
})
|
27
|
+
|
28
|
+
## Set up "Bases" in ActiveRecord
|
29
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
30
|
+
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
|
31
|
+
|
32
|
+
ActiveRecord::Schema.define do
|
33
|
+
self.verbose = false
|
34
|
+
create_table :bases do |t|
|
35
|
+
t.column :name, :string
|
36
|
+
t.column :planet, :string
|
37
|
+
t.column :faction_id, :integer
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Base < ActiveRecord::Base
|
42
|
+
end
|
43
|
+
|
44
|
+
Base.create(id: 11, name: "Yavin", planet: "Yavin 4", faction_id: 1)
|
45
|
+
Base.create(id: 12, name: "Echo Base", planet: "Hoth", faction_id: 1)
|
46
|
+
Base.create(id: 13, name: "Death Star", planet: nil, faction_id: 2)
|
47
|
+
Base.create(id: 14, name: "Shield Generator", planet: "Endor", faction_id: 2)
|
48
|
+
Base.create(id: 15, name: "Headquarters", planet: "Coruscant", faction_id: 2)
|
49
|
+
|
50
|
+
STAR_WARS_DATA = {
|
51
|
+
"Faction" => {
|
52
|
+
"1" => rebels,
|
53
|
+
"2" => empire,
|
54
|
+
},
|
55
|
+
"Ship" => names.each_with_index.reduce({}) do |memo, (name, idx)|
|
56
|
+
id = (idx + 1).to_s
|
57
|
+
memo[id] = OpenStruct.new(name: name, id: id)
|
58
|
+
memo
|
59
|
+
end
|
60
|
+
}
|
61
|
+
|
62
|
+
def STAR_WARS_DATA.create_ship(name, faction_id)
|
63
|
+
new_id = (self["Ship"].keys.map(&:to_i).max + 1).to_s
|
64
|
+
new_ship = OpenStruct.new(id: new_id, name: name)
|
65
|
+
self["Ship"][new_id] = new_ship
|
66
|
+
self["Faction"][faction_id]["ships"] << new_id
|
67
|
+
new_ship
|
68
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# Taken from graphql-relay-js
|
2
|
+
# https://github.com/graphql/graphql-relay-js/blob/master/src/__tests__/starWarsSchema.js
|
3
|
+
|
4
|
+
class NodeImplementation
|
5
|
+
def object_from_id(id)
|
6
|
+
type_name, id = GraphQL::Relay::Node.from_global_id(id)
|
7
|
+
STAR_WARS_DATA[type_name][id]
|
8
|
+
end
|
9
|
+
|
10
|
+
def type_from_object(object)
|
11
|
+
STAR_WARS_DATA["Faction"].values.include?(object) ? Faction : Ship
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
NodeInterface, NodeField = GraphQL::Relay::Node.create(NodeImplementation.new)
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
Ship = GraphQL::ObjectType.define do
|
20
|
+
name "Ship"
|
21
|
+
interfaces [NodeInterface]
|
22
|
+
field :id, field: GraphQL::Relay::GlobalIdField.new("Ship")
|
23
|
+
field :name, types.String
|
24
|
+
end
|
25
|
+
|
26
|
+
# Define a connection which will wrap an array:
|
27
|
+
ShipConnection = GraphQL::Relay::ArrayConnection.create_type(Ship)
|
28
|
+
|
29
|
+
|
30
|
+
BaseType = GraphQL::ObjectType.define do
|
31
|
+
name "Base"
|
32
|
+
interfaces [NodeInterface]
|
33
|
+
field :id, field: GraphQL::Relay::GlobalIdField.new("Base")
|
34
|
+
field :name, types.String
|
35
|
+
field :planet, types.String
|
36
|
+
end
|
37
|
+
|
38
|
+
# Define a connection which will wrap an array:
|
39
|
+
BaseConnection = GraphQL::Relay::RelationConnection.create_type(BaseType)
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
Faction = GraphQL::ObjectType.define do
|
44
|
+
name "Faction"
|
45
|
+
interfaces [NodeInterface]
|
46
|
+
field :id, field: GraphQL::Relay::GlobalIdField.new("Faction")
|
47
|
+
field :name, types.String
|
48
|
+
connection :ships, ShipConnection do
|
49
|
+
# Resolve field should return an Array, the Connection
|
50
|
+
# will do the rest!
|
51
|
+
resolve -> (obj, args, ctx) {
|
52
|
+
obj.ships.map {|ship_id| STAR_WARS_DATA["Ship"][ship_id] }
|
53
|
+
}
|
54
|
+
end
|
55
|
+
connection :bases, BaseConnection do
|
56
|
+
# Resolve field should return an Array, the Connection
|
57
|
+
# will do the rest!
|
58
|
+
resolve -> (obj, args, ctx) {
|
59
|
+
Base.where(id: obj.bases)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Define a mutation. It will also:
|
65
|
+
# - define a derived InputObjectType
|
66
|
+
# - define a derived ObjectType (for return)
|
67
|
+
# - define a field, accessible from {Mutation#field}
|
68
|
+
#
|
69
|
+
# The resolve proc takes `inputs, ctx`, where:
|
70
|
+
# - `inputs` has the keys defined with `input_field`
|
71
|
+
# - `ctx` is the Query context (like normal fields)
|
72
|
+
#
|
73
|
+
# Notice that you leave out clientMutationId.
|
74
|
+
IntroduceShipMutation = GraphQL::Relay::Mutation.define do
|
75
|
+
# Used as the root for derived types:
|
76
|
+
name "IntroduceShip"
|
77
|
+
|
78
|
+
# Nested under `input` in the query:
|
79
|
+
input_field :shipName, !types.String
|
80
|
+
input_field :factionId, !types.ID
|
81
|
+
|
82
|
+
# Result may have access to these fields:
|
83
|
+
return_field :ship, Ship
|
84
|
+
return_field :faction, Faction
|
85
|
+
|
86
|
+
# Here's the mutation operation:
|
87
|
+
resolve -> (inputs, ctx) {
|
88
|
+
faction_id = inputs["factionId"]
|
89
|
+
ship = STAR_WARS_DATA.create_ship(inputs["shipName"], faction_id)
|
90
|
+
faction = STAR_WARS_DATA["Faction"][faction_id]
|
91
|
+
{ ship: ship, faction: faction }
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
QueryType = GraphQL::ObjectType.define do
|
96
|
+
name "Query"
|
97
|
+
field :rebels, Faction do
|
98
|
+
resolve -> (obj, args, ctx) { STAR_WARS_DATA["Faction"]["1"]}
|
99
|
+
end
|
100
|
+
|
101
|
+
field :empire, Faction do
|
102
|
+
resolve -> (obj, args, ctx) { STAR_WARS_DATA["Faction"]["2"]}
|
103
|
+
end
|
104
|
+
|
105
|
+
field :node, field: NodeField
|
106
|
+
end
|
107
|
+
|
108
|
+
MutationType = GraphQL::ObjectType.define do
|
109
|
+
name "Mutation"
|
110
|
+
# The mutation object exposes a field:
|
111
|
+
field :introduceShip, field: IntroduceShipMutation.field
|
112
|
+
end
|
113
|
+
|
114
|
+
StarWarsSchema = GraphQL::Schema.new(query: QueryType, mutation: MutationType)
|
metadata
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: graphql-relay
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Robert Mosolgo
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: graphql
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.6'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: activerecord
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: codeclimate-test-reporter
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.4'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.4'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: pry
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.10'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.10'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: guard
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '2.12'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '2.12'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: guard-bundler
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.1'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.1'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: guard-minitest
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.4'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.4'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '5'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '5'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: minitest-focus
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '1.1'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '1.1'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: minitest-reporters
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '1.0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '1.0'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: rake
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '10.4'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '10.4'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: sqlite3
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - ">="
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '0'
|
181
|
+
description: Connections, UUID, etc?
|
182
|
+
email:
|
183
|
+
- rdmosolgo@gmail.com
|
184
|
+
executables: []
|
185
|
+
extensions: []
|
186
|
+
extra_rdoc_files: []
|
187
|
+
files:
|
188
|
+
- README.md
|
189
|
+
- lib/graphql/definition_helpers/defined_by_config/definition_config.rb
|
190
|
+
- lib/graphql/relay.rb
|
191
|
+
- lib/graphql/relay/array_connection.rb
|
192
|
+
- lib/graphql/relay/base_connection.rb
|
193
|
+
- lib/graphql/relay/connection_field.rb
|
194
|
+
- lib/graphql/relay/connection_type.rb
|
195
|
+
- lib/graphql/relay/edge.rb
|
196
|
+
- lib/graphql/relay/global_id_field.rb
|
197
|
+
- lib/graphql/relay/mutation.rb
|
198
|
+
- lib/graphql/relay/node.rb
|
199
|
+
- lib/graphql/relay/page_info.rb
|
200
|
+
- lib/graphql/relay/relation_connection.rb
|
201
|
+
- lib/graphql/relay/version.rb
|
202
|
+
- spec/graphql/relay/array_connection_spec.rb
|
203
|
+
- spec/graphql/relay/mutation_spec.rb
|
204
|
+
- spec/graphql/relay/node_spec.rb
|
205
|
+
- spec/graphql/relay/relation_connection_spec.rb
|
206
|
+
- spec/spec_helper.rb
|
207
|
+
- spec/support/star_wars_data.rb
|
208
|
+
- spec/support/star_wars_schema.rb
|
209
|
+
homepage: http://github.com/rmosolgo/graphql-relay-ruby
|
210
|
+
licenses:
|
211
|
+
- MIT
|
212
|
+
metadata: {}
|
213
|
+
post_install_message:
|
214
|
+
rdoc_options: []
|
215
|
+
require_paths:
|
216
|
+
- lib
|
217
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
218
|
+
requirements:
|
219
|
+
- - ">="
|
220
|
+
- !ruby/object:Gem::Version
|
221
|
+
version: 2.1.0
|
222
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
223
|
+
requirements:
|
224
|
+
- - ">="
|
225
|
+
- !ruby/object:Gem::Version
|
226
|
+
version: '0'
|
227
|
+
requirements: []
|
228
|
+
rubyforge_project:
|
229
|
+
rubygems_version: 2.4.5
|
230
|
+
signing_key:
|
231
|
+
specification_version: 4
|
232
|
+
summary: Relay helpers for GraphQL
|
233
|
+
test_files:
|
234
|
+
- spec/graphql/relay/array_connection_spec.rb
|
235
|
+
- spec/graphql/relay/mutation_spec.rb
|
236
|
+
- spec/graphql/relay/node_spec.rb
|
237
|
+
- spec/graphql/relay/relation_connection_spec.rb
|
238
|
+
- spec/spec_helper.rb
|
239
|
+
- spec/support/star_wars_data.rb
|
240
|
+
- spec/support/star_wars_schema.rb
|