graphql-relay 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,5 @@
1
+ module GraphQL
2
+ module Relay
3
+ VERSION = '0.1.0'
4
+ end
5
+ 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
@@ -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