graphql-connections 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 17bf53a70dde489ced3c574a122728fd47122369a7e50ef8d45f5cf9c8743a09
4
- data.tar.gz: 4f0b7158004251fced3b9989a0b0e1251e79f93208372b6c12aa60003816defd
3
+ metadata.gz: 4f48d965da430fdbc88992199c0fef0e92e0bfe6f75f900735abd80d96edc098
4
+ data.tar.gz: 3513296ea1afb7d5300c1c9737462bd5baf407f797cb45737e1fa97ccdc52bfb
5
5
  SHA512:
6
- metadata.gz: 4d4e4beaa0f3124dfbfe6d99cfe1e911b38f24aee3fe6072aa69e38b9303042748f0ca18e4df973e67895901cf15978d5aabe4ad02396ae531cd1e1024df87d9
7
- data.tar.gz: a981adbe63d036bf326a8ab6c0bd6a2fdf1b071f003f6665fab8302c48c9e69a1679353238c33d01f4a3469c3ae39363a2247b02bec703206ef4f30e457c8761
6
+ metadata.gz: 5f728d6e4fc47a7e0d7bb87cef013542e31370d33da2eb9289244dddb8a9dbd7ff80588726c437b1edc43fe569029c087a2016c76e62f80b30218f4b99a44060
7
+ data.tar.gz: 1f294f11f5f1e24bc525d1893ecd1de64ba39aae1bcbc9fd72487bfbd3d55b31983627de6c36c97d109b1f989b215fa312615750a31bea307de9d781aa5f2812
data/README.md CHANGED
@@ -37,6 +37,28 @@ Records are sorted by model's primary key by default. You can change this behavi
37
37
  GraphQL::Connections::Stable.new(Message.all, primary_key: :created_at)
38
38
  ```
39
39
 
40
+ In case when you want records to be sorted by more than one field (i.e., _keyset pagination_), you can use `keys` param:
41
+
42
+ ```ruby
43
+ GraphQL::Connections::Stable.new(Message.all, keys: %w[name id])
44
+ ```
45
+
46
+ When you pass only one key, a primary key will be added as a second one:
47
+
48
+ ```ruby
49
+ GraphQL::Connections::Stable.new(Message.all, keys: [:name])
50
+ ```
51
+
52
+ **NOTE:** Currently we support maximum two keys in the keyset.
53
+
54
+ Also, you can pass the `:desc` option to reverse the relation:
55
+
56
+ ```ruby
57
+ GraphQL::Connections::Stable.new(Message.all, keys: %w[name id], desc: true)
58
+ ```
59
+
60
+ **NOTE:** `:desc` option is not implemented for stable connections with `:primary_key` passed; if you need it—use keyset pagination or implement `:desc` option for us 🙂.
61
+
40
62
  Also, you can disable opaque cursors by setting `opaque_cursor` param:
41
63
 
42
64
  ```ruby
@@ -53,7 +75,7 @@ class ApplicationSchema < GraphQL::Schema
53
75
  end
54
76
  ```
55
77
 
56
- **NOTE:** Don't use stable connections for relations whose ordering is too complicated for cursor generation.
78
+ **NOTE:** Don't use stable connections for relations whose ordering is too complicated for cursor generation.
57
79
 
58
80
  ## Development
59
81
 
@@ -9,3 +9,10 @@ module GraphQL
9
9
  end
10
10
 
11
11
  require "graphql/connections/stable"
12
+
13
+ require "graphql/connections/base"
14
+ require "graphql/connections/key_asc"
15
+
16
+ require "graphql/connections/keyset/base"
17
+ require "graphql/connections/keyset/asc"
18
+ require "graphql/connections/keyset/desc"
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ # Base class for pagination implementations
6
+ class Base < ::GraphQL::Pagination::Connection
7
+ attr_reader :opaque_cursor
8
+
9
+ delegate :arel_table, to: :items
10
+
11
+ def initialize(*args, opaque_cursor: true, **kwargs)
12
+ @opaque_cursor = opaque_cursor
13
+
14
+ super(*args, **kwargs)
15
+ end
16
+
17
+ def primary_key
18
+ @primary_key ||= items.model.primary_key
19
+ end
20
+
21
+ def nodes
22
+ @nodes ||= limited_relation
23
+ end
24
+
25
+ def has_previous_page # rubocop:disable Naming/PredicateName
26
+ raise NotImplementedError
27
+ end
28
+
29
+ def has_next_page # rubocop:disable Naming/PredicateName
30
+ raise NotImplementedError
31
+ end
32
+
33
+ def cursor_for(item)
34
+ raise NotImplementedError
35
+ end
36
+
37
+ private
38
+
39
+ def serialize(cursor)
40
+ case cursor
41
+ when Time, DateTime, Date
42
+ cursor.iso8601
43
+ else
44
+ cursor.to_s
45
+ end
46
+ end
47
+
48
+ def limited_relation
49
+ raise NotImplementedError
50
+ end
51
+
52
+ def sliced_relation
53
+ raise NotImplementedError
54
+ end
55
+
56
+ def after_cursor
57
+ @after_cursor ||= opaque_cursor ? decode(after) : after
58
+ end
59
+
60
+ def before_cursor
61
+ @before_cursor ||= opaque_cursor ? decode(before) : before
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ # Implements pagination by one field with asc order
6
+ class KeyAsc < ::GraphQL::Connections::Base
7
+ def initialize(*args, primary_key: nil, **kwargs)
8
+ @primary_key = primary_key
9
+
10
+ super(*args, **kwargs)
11
+ end
12
+
13
+ def has_previous_page # rubocop:disable Naming/PredicateName, Metrics/AbcSize
14
+ if last
15
+ nodes.any? && items.where(arel_table[primary_key].lt(nodes.first[primary_key])).exists?
16
+ elsif after
17
+ items.where(arel_table[primary_key].lteq(after_cursor)).exists?
18
+ else
19
+ false
20
+ end
21
+ end
22
+
23
+ def has_next_page # rubocop:disable Naming/PredicateName, Metrics/AbcSize
24
+ if first
25
+ nodes.any? && items.where(arel_table[primary_key].gt(nodes.last[primary_key])).exists?
26
+ elsif before
27
+ items.where(arel_table[primary_key].gteq(before_cursor)).exists?
28
+ else
29
+ false
30
+ end
31
+ end
32
+
33
+ def cursor_for(item)
34
+ cursor = serialize(item[primary_key])
35
+ cursor = encode(cursor) if opaque_cursor
36
+ cursor
37
+ end
38
+
39
+ private
40
+
41
+ def limited_relation # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
42
+ scope = sliced_relation
43
+ nodes = []
44
+
45
+ if first
46
+ nodes |= scope
47
+ .reorder(arel_table[primary_key].asc)
48
+ .limit(first)
49
+ .to_a
50
+ end
51
+
52
+ if last
53
+ nodes |= scope
54
+ .reorder(arel_table[primary_key].desc)
55
+ .limit(last)
56
+ .to_a.reverse!
57
+ end
58
+
59
+ nodes
60
+ end
61
+
62
+ def sliced_relation # rubocop:disable Metrics/AbcSize
63
+ items
64
+ .yield_self { |s| after ? s.where(arel_table[primary_key].gt(after_cursor)) : s }
65
+ .yield_self { |s| before ? s.where(arel_table[primary_key].lt(before_cursor)) : s }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ module Keyset
6
+ # Implements keyset pagination by two fields with asc order
7
+ class Asc < ::GraphQL::Connections::Keyset::Base
8
+ # rubocop:disable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength
9
+ def has_previous_page
10
+ if last
11
+ nodes.any? &&
12
+ items.where(arel_table[field_key].eq(nodes.first[field_key]))
13
+ .where(arel_table[primary_key].lt(nodes.first[primary_key]))
14
+ .or(items.where(arel_table[field_key].lt(nodes.first[field_key])))
15
+ .exists?
16
+ elsif after
17
+ items
18
+ .where(arel_table[field_key].lt(after_cursor_date))
19
+ .or(
20
+ items.where(arel_table[field_key].eq(after_cursor_date))
21
+ .where(arel_table[primary_key].lt(after_cursor_primary_key))
22
+ ).exists?
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def has_next_page
29
+ if first
30
+ nodes.any? &&
31
+ items.where(arel_table[field_key].eq(nodes.last[field_key]))
32
+ .where(arel_table[primary_key].gt(nodes.last[primary_key]))
33
+ .or(items.where(arel_table[field_key].gt(nodes.last[field_key])))
34
+ .exists?
35
+ elsif before
36
+ items
37
+ .where(arel_table[field_key].gt(before_cursor_date))
38
+ .or(
39
+ items.where(arel_table[field_key].eq(before_cursor_date))
40
+ .where(arel_table[primary_key].gt(before_cursor_primary_key))
41
+ ).exists?
42
+ else
43
+ false
44
+ end
45
+ end
46
+ # rubocop:enable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength
47
+
48
+ private
49
+
50
+ def limited_relation # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
51
+ scope = sliced_relation
52
+ nodes = []
53
+
54
+ if first
55
+ nodes |= scope
56
+ .reorder(arel_table[field_key].asc, arel_table[primary_key].asc)
57
+ .limit(first).to_a
58
+ end
59
+
60
+ if last
61
+ nodes |= scope
62
+ .reorder(arel_table[field_key].desc, arel_table[primary_key].desc)
63
+ .limit(last).to_a.reverse!
64
+ end
65
+
66
+ nodes
67
+ end
68
+
69
+ def sliced_relation_after(relation) # rubocop:disable Metrics/AbcSize
70
+ relation
71
+ .where(arel_table[field_key].eq(after_cursor_date))
72
+ .where(arel_table[primary_key].gt(after_cursor_primary_key))
73
+ .or(relation.where(arel_table[field_key].gt(after_cursor_date)))
74
+ end
75
+
76
+ def sliced_relation_before(relation) # rubocop:disable Metrics/AbcSize
77
+ relation
78
+ .where(arel_table[field_key].eq(before_cursor_date))
79
+ .where(arel_table[primary_key].lt(before_cursor_primary_key))
80
+ .or(relation.where(arel_table[field_key].lt(before_cursor_date)))
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ module Keyset
6
+ # Base class for keyset pagination implementations
7
+ class Base < ::GraphQL::Connections::Base
8
+ attr_reader :field_key
9
+
10
+ SEPARATOR = "/"
11
+
12
+ def initialize(*args, keys:, separator: SEPARATOR, **kwargs)
13
+ @field_key, @primary_key = keys
14
+ @separator = separator
15
+
16
+ super(*args, **kwargs)
17
+ end
18
+
19
+ def cursor_for(item)
20
+ cursor = [item[field_key], item[primary_key]].map { |value| serialize(value) }.join(@separator)
21
+ cursor = encode(cursor) if opaque_cursor
22
+ cursor
23
+ end
24
+
25
+ private
26
+
27
+ def sliced_relation
28
+ items
29
+ .yield_self { |s| after ? sliced_relation_after(s) : s }
30
+ .yield_self { |s| before ? sliced_relation_before(s) : s }
31
+ end
32
+
33
+ def after_cursor_date
34
+ @after_cursor_date ||= after_cursor.first
35
+ end
36
+
37
+ def after_cursor_primary_key
38
+ @after_cursor_primary_key ||= after_cursor.last
39
+ end
40
+
41
+ def after_cursor
42
+ @after_cursor ||= super.split(SEPARATOR)
43
+ end
44
+
45
+ def before_cursor_date
46
+ @before_cursor_date ||= before_cursor.first
47
+ end
48
+
49
+ def before_cursor_primary_key
50
+ @before_cursor_primary_key ||= before_cursor.last
51
+ end
52
+
53
+ def before_cursor
54
+ @before_cursor ||= super.split(SEPARATOR)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ module Keyset
6
+ # Implements keyset pagination by two fields with desc order
7
+ class Desc < ::GraphQL::Connections::Keyset::Base
8
+ # rubocop:disable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength
9
+ def has_previous_page
10
+ if last
11
+ nodes.any? &&
12
+ items.where(arel_table[field_key].eq(nodes.first[field_key]))
13
+ .where(arel_table[primary_key].gt(nodes.first[primary_key]))
14
+ .or(items.where(arel_table[field_key].gt(nodes.first[field_key])))
15
+ .exists?
16
+ elsif after
17
+ items
18
+ .where(arel_table[field_key].gt(after_cursor_date))
19
+ .or(
20
+ items.where(arel_table[field_key].eq(after_cursor_date))
21
+ .where(arel_table[primary_key].gt(after_cursor_primary_key))
22
+ ).exists?
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def has_next_page
29
+ if first
30
+ nodes.any? &&
31
+ items.where(arel_table[field_key].eq(nodes.last[field_key]))
32
+ .where(arel_table[primary_key].lt(nodes.last[primary_key]))
33
+ .or(items.where(arel_table[field_key].lt(nodes.last[field_key])))
34
+ .exists?
35
+ elsif before
36
+ items
37
+ .where(arel_table[field_key].lt(before_cursor_date))
38
+ .or(
39
+ items.where(arel_table[field_key].eq(before_cursor_date))
40
+ .where(arel_table[primary_key].lt(before_cursor_primary_key))
41
+ ).exists?
42
+ else
43
+ false
44
+ end
45
+ end
46
+ # rubocop:enable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength
47
+
48
+ def cursor_for(item)
49
+ cursor = [item[field_key], item[primary_key]].map { |value| serialize(value) }.join(@separator)
50
+ cursor = encode(cursor) if opaque_cursor
51
+ cursor
52
+ end
53
+
54
+ private
55
+
56
+ def limited_relation # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
57
+ scope = sliced_relation
58
+ nodes = []
59
+
60
+ if first
61
+ nodes |= scope
62
+ .reorder(arel_table[field_key].desc, arel_table[primary_key].desc)
63
+ .limit(first).to_a
64
+ end
65
+
66
+ if last
67
+ nodes |= scope
68
+ .reorder(arel_table[field_key].asc, arel_table[primary_key].asc)
69
+ .limit(last)
70
+ .to_a.reverse!
71
+ end
72
+
73
+ nodes
74
+ end
75
+
76
+ def sliced_relation_after(relation) # rubocop:disable Metrics/AbcSize
77
+ relation
78
+ .where(arel_table[field_key].eq(after_cursor_date))
79
+ .where(arel_table[primary_key].lt(after_cursor_primary_key))
80
+ .or(relation.where(arel_table[field_key].lt(after_cursor_date)))
81
+ end
82
+
83
+ def sliced_relation_before(relation) # rubocop:disable Metrics/AbcSize
84
+ relation
85
+ .where(arel_table[field_key].eq(before_cursor_date))
86
+ .where(arel_table[primary_key].gt(before_cursor_primary_key))
87
+ .or(relation.where(arel_table[field_key].gt(before_cursor_date)))
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -11,96 +11,18 @@ module GraphQL
11
11
  #
12
12
  # For more information see GraphQL Cursor Connections Specification
13
13
  # https://relay.dev/graphql/connections.htm
14
- class Stable < ::GraphQL::Pagination::Connection
15
- attr_reader :opaque_cursor
14
+ module Stable
15
+ def self.new(*args, desc: false, keys: nil, **kwargs)
16
+ if kwargs[:primary_key] || keys.nil?
17
+ raise NotImplementedError, "desc connection is not implemented yet" if desc
16
18
 
17
- delegate :arel_table, to: :items
18
-
19
- def initialize(*args, primary_key: nil, opaque_cursor: true, **kwargs)
20
- @primary_key = primary_key
21
- @opaque_cursor = opaque_cursor
22
-
23
- super(*args, **kwargs)
24
- end
25
-
26
- def primary_key
27
- @primary_key ||= items.model.primary_key
28
- end
29
-
30
- def nodes
31
- @nodes ||= limited_relation
32
- end
33
-
34
- def has_previous_page # rubocop:disable Naming/PredicateName
35
- if last
36
- nodes.any? && items.where(arel_table[primary_key].lt(nodes.first[primary_key])).exists?
37
- elsif after
38
- items.where(arel_table[primary_key].lteq(after_cursor)).exists?
39
- else
40
- false
41
- end
42
- end
43
-
44
- def has_next_page # rubocop:disable Naming/PredicateName
45
- if first
46
- nodes.any? && items.where(arel_table[primary_key].gt(nodes.last[primary_key])).exists?
47
- elsif before
48
- items.where(arel_table[primary_key].gteq(before_cursor)).exists?
49
- else
50
- false
51
- end
52
- end
53
-
54
- def cursor_for(item)
55
- cursor = serialize(item[primary_key])
56
- cursor = encode(cursor) if opaque_cursor
57
- cursor
58
- end
59
-
60
- private
61
-
62
- def serialize(cursor)
63
- case cursor
64
- when Time, DateTime, Date
65
- cursor.iso8601
66
- else
67
- cursor.to_s
68
- end
69
- end
70
-
71
- def limited_relation
72
- scope = sliced_relation
73
- nodes = []
74
-
75
- if first
76
- nodes |= scope.
77
- reorder(arel_table[primary_key].asc).
78
- limit(first).
79
- to_a
19
+ return GraphQL::Connections::KeyAsc.new(*args, **kwargs)
80
20
  end
81
21
 
82
- if last
83
- nodes |= scope.
84
- reorder(arel_table[primary_key].desc).
85
- limit(last).
86
- to_a.reverse!
87
- end
88
-
89
- nodes
90
- end
91
-
92
- def sliced_relation
93
- items.
94
- yield_self { |s| after ? s.where(arel_table[primary_key].gt(after_cursor)) : s }.
95
- yield_self { |s| before ? s.where(arel_table[primary_key].lt(before_cursor)) : s }
96
- end
97
-
98
- def after_cursor
99
- @after_cursor ||= opaque_cursor ? decode(after) : after
100
- end
22
+ raise ArgumentError, "keyset for more that 2 keys is not implemented yet" if keys.length > 2
101
23
 
102
- def before_cursor
103
- @before_cursor ||= opaque_cursor ? decode(before) : before
24
+ klass = desc ? GraphQL::Connections::Keyset::Desc : GraphQL::Connections::Keyset::Asc
25
+ klass.new(*args, **kwargs.merge(keys: keys))
104
26
  end
105
27
  end
106
28
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Paging
5
- VERSION = "0.2.0"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphql-connections
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Misha Merkushin
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-07 00:00:00.000000000 Z
11
+ date: 2021-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -114,43 +114,43 @@ dependencies:
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: '4.0'
117
+ version: '5.0'
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: '4.0'
124
+ version: '5.0'
125
125
  - !ruby/object:Gem::Dependency
126
- name: rubocop
126
+ name: standard
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: '0.58'
131
+ version: '1.1'
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: '0.58'
138
+ version: '1.1'
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: test-prof
141
141
  requirement: !ruby/object:Gem::Requirement
142
142
  requirements:
143
143
  - - "~>"
144
144
  - !ruby/object:Gem::Version
145
- version: '0.11'
145
+ version: '1.0'
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - "~>"
151
151
  - !ruby/object:Gem::Version
152
- version: '0.11'
153
- description:
152
+ version: '1.0'
153
+ description:
154
154
  email:
155
155
  - merkushin.m.s@gmail.com
156
156
  executables: []
@@ -160,6 +160,11 @@ files:
160
160
  - LICENSE.txt
161
161
  - README.md
162
162
  - lib/graphql/connections.rb
163
+ - lib/graphql/connections/base.rb
164
+ - lib/graphql/connections/key_asc.rb
165
+ - lib/graphql/connections/keyset/asc.rb
166
+ - lib/graphql/connections/keyset/base.rb
167
+ - lib/graphql/connections/keyset/desc.rb
163
168
  - lib/graphql/connections/stable.rb
164
169
  - lib/graphql/connections/version.rb
165
170
  homepage: https://github.com/bibendi/graphql-connections
@@ -167,7 +172,7 @@ licenses:
167
172
  - MIT
168
173
  metadata:
169
174
  allowed_push_host: https://rubygems.org
170
- post_install_message:
175
+ post_install_message:
171
176
  rdoc_options: []
172
177
  require_paths:
173
178
  - lib
@@ -183,7 +188,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
188
  version: '0'
184
189
  requirements: []
185
190
  rubygems_version: 3.1.2
186
- signing_key:
191
+ signing_key:
187
192
  specification_version: 4
188
193
  summary: GraphQL cursor-based stable pagination to work with Active Record relations
189
194
  test_files: []