graphql-connections 0.2.0 → 1.0.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 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: []