relay-rb 0.0.1

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