relay-rb 0.0.1

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: 7c976d6b1f308d77fad6f43623d821150b45df13
4
+ data.tar.gz: d396a7d982e23b6ca71bafbbdbc438b444c0ea6b
5
+ SHA512:
6
+ metadata.gz: 914555d8c843092f3a6fbe1cec290ae032784a4296b32eaf1245d65429693a9305e2e5e5a55ac84f54ba2a45b38cd7fc1813105b6410c4ca452e3c0fc3c29651
7
+ data.tar.gz: 70c0950fcf1b438467aef315b0f572febb7c17609cc4e9d6e00a0f23ea338e5a1b58d10e3560520e4dc47c5224e9fd96899ac1a67df10ebfcd7f7cc76fbec079
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Relay Ruby'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('lib/**/*.rb')
14
+ end
15
+
16
+ Bundler::GemHelper.install_tasks
17
+
18
+ require 'rspec'
19
+ require 'rspec/core/rake_task'
20
+
21
+ RSpec::Core::RakeTask.new(:test) do |test|
22
+ test.verbose = false
23
+ test.rspec_opts = %w[--color --format documentation]
24
+ end
25
+
26
+ task default: :test
@@ -0,0 +1,54 @@
1
+ require 'base64'
2
+
3
+ module Relay
4
+ module Connection
5
+
6
+ ARRAY_CONNECTION_PREFIX = 'arrayconnection'
7
+
8
+ Edge = Struct.new('Edge', :cursor, :node)
9
+ PageInfo = Struct.new('PageInfo', :startCursor, :endCursor, :hasPreviousPage, :hasNextPage)
10
+ ArrayConnection = Struct.new('ArrayConnection', :edges, :pageInfo)
11
+ EmptyConnection = ArrayConnection.new([], PageInfo.new(nil, nil, false, false))
12
+
13
+ def self.connection_from_array(data, args)
14
+ edges = data.each_with_index.map { |item, i| Edge.new(offset_to_cursor(i), item) }
15
+
16
+ start = [get_offset(args[:after], -1), -1].max + 1
17
+ finish = [get_offset(args[:before], edges.size + 1), edges.size + 1].min
18
+
19
+ edges = edges.slice(start, finish)
20
+
21
+ return EmptyConnection if edges.size == 0
22
+
23
+ first_preslice_cursor = edges.first[:cursor]
24
+ last_preslice_cursor = edges.last[:cursor]
25
+
26
+ edges = edges.slice(0, args[:first]) unless args[:first].nil?
27
+ egdes = edges.slice!(- args[:last], edges.size) unless args[:last].nil?
28
+
29
+ return EmptyConnection if edges.size == 0
30
+
31
+ ArrayConnection.new(edges, PageInfo.new(
32
+ edges.first[:cursor],
33
+ edges.last[:cursor],
34
+ edges.first[:cursor] != first_preslice_cursor,
35
+ edges.last[:cursor] != last_preslice_cursor
36
+ ))
37
+ end
38
+
39
+ def self.offset_to_cursor(index)
40
+ Base64.strict_encode64([ARRAY_CONNECTION_PREFIX, index].join(':'))
41
+ end
42
+
43
+ def self.cursor_to_offset(cursor)
44
+ Base64.strict_decode64(cursor).split(':').last.to_i
45
+ end
46
+
47
+ def self.get_offset(cursor, default_offset)
48
+ return default_offset unless cursor
49
+ offset = cursor_to_offset(cursor)
50
+ offset.nil? ? default_offset : offset
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,78 @@
1
+ require_relative 'array'
2
+
3
+ module Relay
4
+ module Connection
5
+
6
+ ConnectionArguments = [
7
+ GraphQL::GraphQLArgument.new(:before, GraphQL::GraphQLString),
8
+ GraphQL::GraphQLArgument.new(:after, GraphQL::GraphQLString),
9
+ GraphQL::GraphQLArgument.new(:first, GraphQL::GraphQLInt),
10
+ GraphQL::GraphQLArgument.new(:last, GraphQL::GraphQLInt)
11
+ ]
12
+
13
+ class ConnectionConfiguration < GraphQL::Configuration::Base
14
+ slot :name, String
15
+ slot :node_type, GraphQL::GraphQLObjectType
16
+ slot :edge_fields, [GraphQL::GraphQLField], singular: :edge_field
17
+ slot :connection_fields, [GraphQL::GraphQLField], singular: :connection_field
18
+ end
19
+
20
+ def self.connection_definitions(configuration)
21
+ name, node_type = configuration.name, configuration.node_type
22
+
23
+ edge_type = GraphQL::GraphQLObjectType.new do
24
+ name name + 'Edge'
25
+ description 'An edge in a connection'
26
+
27
+ field :node, node_type do
28
+ description 'The item at the end of the edge'
29
+ end
30
+
31
+ field :cursor, ! GraphQL::GraphQLString do
32
+ description 'A cursor for use in pagination'
33
+ end
34
+
35
+ fields configuration.edge_fields
36
+ end
37
+
38
+ connection_type = GraphQL::GraphQLObjectType.new do
39
+ name name + 'Connection'
40
+ description 'A connection to a list of items.'
41
+
42
+ field :pageInfo, !PageInfoType do
43
+ description 'Information to aid in pagination.'
44
+ end
45
+
46
+ field :edges, +edge_type do
47
+ description 'Information to aid in pagination.'
48
+ end
49
+
50
+ fields configuration.connection_fields
51
+ end
52
+
53
+ return edge_type, connection_type
54
+ end
55
+
56
+ PageInfoType = GraphQL::GraphQLObjectType.new do
57
+ name 'PageInfo'
58
+ description 'Information about pagination in a connection.'
59
+
60
+ field :hasNextPage, !GraphQL::GraphQLBoolean do
61
+ description 'When paginating forwards, are there more items?'
62
+ end
63
+
64
+ field :hasPreviousPage, !GraphQL::GraphQLBoolean do
65
+ description 'When paginating backwards, are there more items?'
66
+ end
67
+
68
+ field :startCursor, GraphQL::GraphQLString do
69
+ description 'When paginating backwards, the cursor to continue.'
70
+ end
71
+
72
+ field :endCursor, GraphQL::GraphQLString do
73
+ description 'When paginating forwards, the cursor to continue.'
74
+ end
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,38 @@
1
+ require 'graphql'
2
+
3
+ module Relay
4
+
5
+
6
+ GLOBAL_ID_FIELD_DEFAULT_RESOLVE = -> (object) { object.id }
7
+
8
+ class GlobalIDFieldConfiguration < GraphQL::Configuration::Base
9
+ slot :name, String, coerce: -> (v) { v.to_s }
10
+ slot :type_name, String, coerce: -> (v) { v.to_s }
11
+ slot :resolve_id, Proc, null: true
12
+ end
13
+
14
+
15
+ class GraphQL::GraphQLObjectTypeConfiguration
16
+
17
+ def global_id_field(*args, &block)
18
+ configuration = GlobalIDFieldConfiguration.new(*args, &block)
19
+
20
+ resolve_id = configuration.resolve_id || GLOBAL_ID_FIELD_DEFAULT_RESOLVE
21
+
22
+ global_id_field = GraphQL::GraphQLFieldConfiguration.new do
23
+ name configuration.name
24
+
25
+ type !GraphQL::GraphQLID
26
+
27
+ resolve lambda { |object|
28
+ Relay::Node.to_global_id(configuration.type_name, resolve_id.call(object))
29
+ }
30
+ end
31
+
32
+ field(global_id_field)
33
+ end
34
+
35
+ end
36
+
37
+
38
+ end
@@ -0,0 +1,35 @@
1
+ module Relay
2
+ module Node
3
+
4
+ class PluralIdentifyingRootFieldConfiguration < GraphQL::Configuration::Base
5
+ slot :name, String, coerce: -> (v) { v.to_s }
6
+ slot :input_type, GraphQL::GraphQLInputType
7
+ slot :output_type, GraphQL::GraphQLOutputType
8
+ slot :argument_name, String, coerce: -> (v) { v.to_s }
9
+ slot :resolve_single_input, Proc
10
+ end
11
+
12
+ class GraphQL::GraphQLObjectTypeConfiguration
13
+
14
+ def plural_identifying_root_field(*args, &block)
15
+ configuration = PluralIdentifyingRootFieldConfiguration.new(*args, &block)
16
+
17
+ plural_identifying_root_field = GraphQL::GraphQLFieldConfiguration.new do
18
+ name configuration.name
19
+
20
+ type +configuration.output_type
21
+
22
+ arg configuration.argument_name, !+!configuration.input_type
23
+
24
+ resolve lambda { |object, params|
25
+ params[configuration.argument_name.to_sym].map { |param| configuration.resolve_single_input.call(param) }
26
+ }
27
+ end
28
+
29
+ field(plural_identifying_root_field)
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
data/lib/relay/node.rb ADDED
@@ -0,0 +1,50 @@
1
+ require 'graphql'
2
+ require 'base64'
3
+ require_relative 'node/global_id_field'
4
+ require_relative 'node/plural_identifying_root_field'
5
+
6
+ module Relay
7
+
8
+ module Node
9
+
10
+ def self.definitions(fetcher, resolver)
11
+
12
+ interface = GraphQL::GraphQLInterfaceType.new do
13
+ name 'Node'
14
+ description 'An object with ID'
15
+
16
+ field :id, ! GraphQL::GraphQLID do
17
+ description 'The id of the object'
18
+ end
19
+
20
+ resolve_type resolver
21
+ end
22
+
23
+ field = GraphQL::GraphQLField.new do
24
+ name 'node'
25
+ description 'Fetches an object given its ID'
26
+ type interface
27
+
28
+ arg :id, ! GraphQL::GraphQLID do
29
+ description 'The ID of an object'
30
+ end
31
+
32
+ resolve lambda { |root, params, info, *args|
33
+ fetcher.call(params[:id], info)
34
+ }
35
+ end
36
+
37
+ { interface: interface, field: field }
38
+ end
39
+
40
+ def self.to_global_id(type, id)
41
+ Base64.strict_encode64([type, id].join(':'))
42
+ end
43
+
44
+ def self.from_global_id(global_id)
45
+ return Base64.strict_decode64(global_id).split(':')
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,3 @@
1
+ module Relay
2
+ VERSION = '0.0.1'
3
+ end
data/lib/relay.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'relay/node'
2
+ require 'relay/version'
3
+ require 'relay/connection/connection'
4
+
5
+ module Relay
6
+ end
@@ -0,0 +1,85 @@
1
+ require 'relay'
2
+
3
+ RSpec.describe 'Relay Connection' do
4
+
5
+ User = Struct.new('UserInRelayConnection', :name)
6
+
7
+ Users = [
8
+ User.new('Dan'),
9
+ User.new('Nick'),
10
+ User.new('Lee'),
11
+ User.new('Joe'),
12
+ User.new('Tim')
13
+ ]
14
+
15
+ friend_edge, friend_connection = Relay::Connection.connection_definitions(
16
+ Relay::Connection::ConnectionConfiguration.new do
17
+ name 'Friend'
18
+ node_type -> { ConnectionUserType }
19
+
20
+ edge_field :friendship_time, GraphQL::GraphQLString do
21
+ resolve -> { 'Yesterday' }
22
+ end
23
+
24
+ connection_field :total_count, GraphQL::GraphQLInt do
25
+ resolve -> { Users.size }
26
+ end
27
+ end
28
+ )
29
+
30
+ ConnectionUserType = GraphQL::GraphQLObjectType.new do
31
+ name 'User'
32
+
33
+ field :name, GraphQL::GraphQLString
34
+
35
+ field :friends do
36
+ type friend_connection
37
+ args Relay::Connection::ConnectionArguments
38
+ resolve lambda { |user, params|
39
+ Relay::Connection.connection_from_array(Users, params)
40
+ }
41
+ end
42
+ end
43
+
44
+ ConnectionQueryType = GraphQL::GraphQLObjectType.new do
45
+ name 'Query'
46
+
47
+ field :user, ConnectionUserType do
48
+ resolve -> { Users[0] }
49
+ end
50
+ end
51
+
52
+ ConnectionSchema = GraphQL::GraphQLSchema.new do
53
+ query ConnectionQueryType
54
+ end
55
+
56
+
57
+ def q1
58
+ %Q(
59
+ query FriendsQuery {
60
+ user {
61
+ name
62
+ friends(first: 2, after: "YXJyYXljb25uZWN0aW9uOjM=") {
63
+ total_count
64
+ edges {
65
+ friendship_time
66
+ cursor
67
+ node {
68
+ name
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ )
75
+ end
76
+
77
+
78
+ it "Should include connections and edge fields" do
79
+ document = GraphQL::Language.parse(q1)
80
+ executor = GraphQL::Executor.new(document, ConnectionSchema)
81
+ puts executor.execute({})
82
+ end
83
+
84
+
85
+ end
data/spec/node/data.rb ADDED
@@ -0,0 +1,69 @@
1
+ module Relay
2
+ module Node
3
+ module Data
4
+
5
+ UserStruct = Struct.new('User', :id, :name)
6
+
7
+ Users = {
8
+ '1' => UserStruct.new('1', 'John Doe'),
9
+ '2' => UserStruct.new('2', 'Jane Smith')
10
+ }
11
+
12
+ PhotoStruct = Struct.new('Photo', :id, :width)
13
+
14
+ Photos = {
15
+ '3' => PhotoStruct.new('3', 300),
16
+ '4' => PhotoStruct.new('4', 400)
17
+ }
18
+
19
+ fetcher = lambda { |id, info|
20
+ if Users[id.to_s]
21
+ return Users[id.to_s]
22
+ end
23
+ if Photos[id.to_s]
24
+ return Photos[id.to_s]
25
+ end
26
+ }
27
+
28
+ resolver = lambda { |root|
29
+ if Users[root.id.to_s]
30
+ return UserType
31
+ end
32
+
33
+ if Photos[root.id.to_s]
34
+ return PhotoType
35
+ end
36
+ }
37
+
38
+ definitions = Relay::Node.definitions(fetcher, resolver)
39
+
40
+ UserType = GraphQL::GraphQLObjectType.new do
41
+ name 'User'
42
+
43
+ field :id, ! GraphQL::GraphQLID
44
+ field :name, GraphQL::GraphQLString
45
+
46
+ interface definitions[:interface]
47
+ end
48
+
49
+ PhotoType = GraphQL::GraphQLObjectType.new do
50
+ name 'Photo'
51
+
52
+ field :id, ! GraphQL::GraphQLID
53
+ field :width, GraphQL::GraphQLInt
54
+
55
+ interface definitions[:interface]
56
+ end
57
+
58
+ QueryType = GraphQL::GraphQLObjectType.new do
59
+ name 'Query'
60
+
61
+ field definitions[:field]
62
+ end
63
+
64
+ Schema = GraphQL::GraphQLSchema.new do
65
+ query QueryType
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,78 @@
1
+ module Relay
2
+ module Node
3
+ module DataGlobal
4
+
5
+ UserStruct = Struct.new('UserGlobal', :id, :name)
6
+
7
+ Users = {
8
+ '1' => UserStruct.new('1', 'John Doe'),
9
+ '2' => UserStruct.new('2', 'Jane Smith')
10
+ }
11
+
12
+ PhotoStruct = Struct.new('PhotoGlobal', :photo_id, :width)
13
+
14
+ Photos = {
15
+ '3' => PhotoStruct.new('3', 300),
16
+ '4' => PhotoStruct.new('4', 400)
17
+ }
18
+
19
+ fetcher = lambda { |id, *args|
20
+ type, id = Relay::Node.from_global_id(id)
21
+
22
+ case type
23
+ when 'User'
24
+ Users[id]
25
+ when 'Photo'
26
+ Photos[id]
27
+ end
28
+ }
29
+
30
+ resolver = lambda { |root|
31
+ case root
32
+ when UserStruct
33
+ UserType
34
+ when PhotoStruct
35
+ PhotoType
36
+ end
37
+ }
38
+
39
+ definitions = Relay::Node.definitions(fetcher, resolver)
40
+
41
+ UserType = GraphQL::GraphQLObjectType.new do
42
+ name 'User'
43
+
44
+ global_id_field :id, type_name: 'User'
45
+
46
+ field :name, GraphQL::GraphQLString
47
+
48
+ interface definitions[:interface]
49
+ end
50
+
51
+ PhotoType = GraphQL::GraphQLObjectType.new do
52
+ name 'Photo'
53
+
54
+ global_id_field :id, type_name: 'Photo', resolve_id: -> (object) { object.photo_id }
55
+
56
+ field :width, GraphQL::GraphQLInt
57
+
58
+ interface definitions[:interface]
59
+ end
60
+
61
+ QueryType = GraphQL::GraphQLObjectType.new do
62
+ name 'Query'
63
+
64
+ field definitions[:field]
65
+
66
+ field :all, + definitions[:interface] do
67
+ resolve lambda { |*args|
68
+ Users.values.concat(Photos.values)
69
+ }
70
+ end
71
+ end
72
+
73
+ Schema = GraphQL::GraphQLSchema.new do
74
+ query QueryType
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,50 @@
1
+ require 'relay'
2
+ require_relative 'data_global'
3
+
4
+ RSpec.describe 'Relay Node with Global ID' do
5
+
6
+ def schema
7
+ Relay::Node::DataGlobal::Schema
8
+ end
9
+
10
+ def all_q1
11
+ %Q{{
12
+ all {
13
+ id
14
+ }
15
+ }}
16
+ end
17
+
18
+ def all_q2
19
+ %Q{{
20
+ user: node(id: "VXNlcjox") {
21
+ id
22
+ ... on User {
23
+ name
24
+ }
25
+ }
26
+ photo: node(id: "UGhvdG86Mw==") {
27
+ id
28
+ ... on Photo {
29
+ width
30
+ }
31
+ }
32
+ }}
33
+ end
34
+
35
+
36
+ it "Should give different ids" do
37
+ expectations = {all: [{id: 'VXNlcjox'}, {id: 'VXNlcjoy'}, {id: 'UGhvdG86Mw=='}, {id: 'UGhvdG86NA=='}]}
38
+ document = GraphQL::Language.parse(all_q1)
39
+ executor = GraphQL::Executor.new(document, schema)
40
+ expect(executor.execute({})).to eq(expectations)
41
+ end
42
+
43
+ it "Should refetch ids" do
44
+ expectations = {user: {id: 'VXNlcjox', name: 'John Doe'}, photo: {id: 'UGhvdG86Mw==', width: 300}}
45
+ document = GraphQL::Language.parse(all_q2)
46
+ executor = GraphQL::Executor.new(document, schema)
47
+ expect(executor.execute({})).to eq(expectations)
48
+ end
49
+
50
+ end
@@ -0,0 +1,51 @@
1
+ require 'relay'
2
+
3
+ RSpec.describe 'Relay Node Plural' do
4
+
5
+ UserType = GraphQL::GraphQLObjectType.new do
6
+ name 'User'
7
+
8
+ field :username, GraphQL::GraphQLString
9
+ field :url, GraphQL::GraphQLString
10
+ end
11
+
12
+ UserStruct = Struct.new('PluralUser', :username, :url)
13
+
14
+ QueryType = GraphQL::GraphQLObjectType.new do
15
+ name 'Query'
16
+
17
+ plural_identifying_root_field :usernames do
18
+ argument_name :usernames
19
+ input_type GraphQL::GraphQLString
20
+ output_type UserType
21
+
22
+ resolve_single_input lambda { |username, *args|
23
+ UserStruct.new(username, "www.facebook.com/#{username}")
24
+ }
25
+ end
26
+ end
27
+
28
+ Schema = GraphQL::GraphQLSchema.new do
29
+
30
+ query QueryType
31
+
32
+ end
33
+
34
+ def q1
35
+ %Q(
36
+ {
37
+ usernames(usernames: ["dschafer", "leebyron", "schrockn"]) {
38
+ username
39
+ url
40
+ }
41
+ }
42
+ )
43
+ end
44
+
45
+ it "Should allow fetching" do
46
+ document = GraphQL::Language.parse(q1)
47
+ executor = GraphQL::Executor.new(document, Schema)
48
+ puts executor.execute({})
49
+ end
50
+
51
+ end