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