graphql-connections 1.0.0 → 1.3.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: 4f48d965da430fdbc88992199c0fef0e92e0bfe6f75f900735abd80d96edc098
4
- data.tar.gz: 3513296ea1afb7d5300c1c9737462bd5baf407f797cb45737e1fa97ccdc52bfb
3
+ metadata.gz: ed09e36d3d30a8e2cd939a1b9b39341168478d547e4b0e990798e612f6f83e49
4
+ data.tar.gz: bc35f6d38dcebf5a97ce0cf69d6540db2712046bddc15f5c1bca0c685115eac4
5
5
  SHA512:
6
- metadata.gz: 5f728d6e4fc47a7e0d7bb87cef013542e31370d33da2eb9289244dddb8a9dbd7ff80588726c437b1edc43fe569029c087a2016c76e62f80b30218f4b99a44060
7
- data.tar.gz: 1f294f11f5f1e24bc525d1893ecd1de64ba39aae1bcbc9fd72487bfbd3d55b31983627de6c36c97d109b1f989b215fa312615750a31bea307de9d781aa5f2812
6
+ metadata.gz: 2408beb9fb3346e5ce7e980b462dc34762d5507bb65eb9c1051672d1dc0b8d33c2807d5113b0837a704ad3311bcbc27baeddb5bd4bb1177e8e748d00ca0f2b98
7
+ data.tar.gz: 042f6ee93e8fbc8391122ac5546bf18c3be595b2c45504daeff6dbec16240278141ad3e9a5d145e30a14c84e70d37ef03fb242e282c110f41ffd2605ebca81c4
data/README.md CHANGED
@@ -3,10 +3,7 @@
3
3
 
4
4
  # GraphQL::Connections
5
5
 
6
- Cursor-based pagination to work with `ActiveRecord::Relation`s.
7
-
8
- Implements [Relay specification](https://relay.dev/graphql/connections.htm) for serving stable connections based on column values.
9
- If objects are created or destroyed during pagination, the list of items won’t be disrupted.
6
+ Additional implementations of cursor-based paginations for [GraphQL Ruby](https://graphql-ruby.org/).
10
7
 
11
8
  <a href="https://evilmartians.com/?utm_source=graphql-connections">
12
9
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
@@ -21,6 +18,11 @@ gem "graphql-connections"
21
18
 
22
19
  ## Usage
23
20
 
21
+ ### ActiveRecord
22
+
23
+ Implements [Relay specification](https://relay.dev/graphql/connections.htm) for serving stable connections based on column values.
24
+ If objects are created or destroyed during pagination, the list of items won’t be disrupted.
25
+
24
26
  You can use a stable connection wrapper on a specific field:
25
27
 
26
28
  ```ruby
@@ -57,7 +59,9 @@ Also, you can pass the `:desc` option to reverse the relation:
57
59
  GraphQL::Connections::Stable.new(Message.all, keys: %w[name id], desc: true)
58
60
  ```
59
61
 
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 🙂.
62
+ ```ruby
63
+ GraphQL::Connections::Stable.new(Message.all, primary_key: :created_at, desc: true)
64
+ ```
61
65
 
62
66
  Also, you can disable opaque cursors by setting `opaque_cursor` param:
63
67
 
@@ -69,14 +73,34 @@ Or you can apply a stable connection to all Active Record relations returning by
69
73
 
70
74
  ```ruby
71
75
  class ApplicationSchema < GraphQL::Schema
72
- use GraphQL::Pagination::Connections
73
-
74
76
  connections.add(ActiveRecord::Relation, GraphQL::Connections::Stable)
75
77
  end
76
78
  ```
77
79
 
78
80
  **NOTE:** Don't use stable connections for relations whose ordering is too complicated for cursor generation.
79
81
 
82
+ ### Elasticsearch via Chewy
83
+
84
+ Register connection for all Chewy requests:
85
+
86
+ ```ruby
87
+ class ApplicationSchema < GraphQL::Schema
88
+ connections.add(Chewy::Search::Request, GraphQL::Connections::ChewyConnection)
89
+ end
90
+ ```
91
+
92
+ And define field like below:
93
+
94
+ ```ruby
95
+ field :messages, Types::Message.connection_type, null: false
96
+
97
+ def messages
98
+ CitiesIndex.query(match: {name: "Moscow"})
99
+ end
100
+ ```
101
+
102
+ **NOTE:** Using `first` and `last`arguments simultaneously is not supported yet.
103
+
80
104
  ## Development
81
105
 
82
106
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -22,11 +22,11 @@ module GraphQL
22
22
  @nodes ||= limited_relation
23
23
  end
24
24
 
25
- def has_previous_page # rubocop:disable Naming/PredicateName
25
+ def has_previous_page
26
26
  raise NotImplementedError
27
27
  end
28
28
 
29
- def has_next_page # rubocop:disable Naming/PredicateName
29
+ def has_next_page
30
30
  raise NotImplementedError
31
31
  end
32
32
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ class Chewy < ::GraphQL::Pagination::RelationConnection
6
+ private
7
+
8
+ def load_nodes
9
+ @nodes ||= limited_nodes.objects
10
+ end
11
+
12
+ def relation_count(relation)
13
+ offset = relation_offset(relation) || 0
14
+ limit = relation_limit(relation)
15
+ count = relation.count - offset
16
+
17
+ if limit.nil?
18
+ count
19
+ else
20
+ [count, limit].min
21
+ end
22
+ end
23
+
24
+ def relation_limit(relation)
25
+ relation.send(:raw_limit_value)&.to_i
26
+ end
27
+
28
+ def relation_offset(relation)
29
+ relation.send(:raw_offset_value)&.to_i
30
+ end
31
+
32
+ def null_relation(relation)
33
+ relation.none
34
+ end
35
+ end
36
+ end
37
+ end
@@ -5,7 +5,6 @@ module GraphQL
5
5
  module Keyset
6
6
  # Implements keyset pagination by two fields with asc order
7
7
  class Asc < ::GraphQL::Connections::Keyset::Base
8
- # rubocop:disable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength
9
8
  def has_previous_page
10
9
  if last
11
10
  nodes.any? &&
@@ -47,7 +46,7 @@ module GraphQL
47
46
 
48
47
  private
49
48
 
50
- def limited_relation # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
49
+ def limited_relation
51
50
  scope = sliced_relation
52
51
  nodes = []
53
52
 
@@ -66,14 +65,14 @@ module GraphQL
66
65
  nodes
67
66
  end
68
67
 
69
- def sliced_relation_after(relation) # rubocop:disable Metrics/AbcSize
68
+ def sliced_relation_after(relation)
70
69
  relation
71
70
  .where(arel_table[field_key].eq(after_cursor_date))
72
71
  .where(arel_table[primary_key].gt(after_cursor_primary_key))
73
72
  .or(relation.where(arel_table[field_key].gt(after_cursor_date)))
74
73
  end
75
74
 
76
- def sliced_relation_before(relation) # rubocop:disable Metrics/AbcSize
75
+ def sliced_relation_before(relation)
77
76
  relation
78
77
  .where(arel_table[field_key].eq(before_cursor_date))
79
78
  .where(arel_table[primary_key].lt(before_cursor_primary_key))
@@ -5,7 +5,6 @@ module GraphQL
5
5
  module Keyset
6
6
  # Implements keyset pagination by two fields with desc order
7
7
  class Desc < ::GraphQL::Connections::Keyset::Base
8
- # rubocop:disable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength
9
8
  def has_previous_page
10
9
  if last
11
10
  nodes.any? &&
@@ -53,7 +52,7 @@ module GraphQL
53
52
 
54
53
  private
55
54
 
56
- def limited_relation # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
55
+ def limited_relation
57
56
  scope = sliced_relation
58
57
  nodes = []
59
58
 
@@ -73,14 +72,14 @@ module GraphQL
73
72
  nodes
74
73
  end
75
74
 
76
- def sliced_relation_after(relation) # rubocop:disable Metrics/AbcSize
75
+ def sliced_relation_after(relation)
77
76
  relation
78
77
  .where(arel_table[field_key].eq(after_cursor_date))
79
78
  .where(arel_table[primary_key].lt(after_cursor_primary_key))
80
79
  .or(relation.where(arel_table[field_key].lt(after_cursor_date)))
81
80
  end
82
81
 
83
- def sliced_relation_before(relation) # rubocop:disable Metrics/AbcSize
82
+ def sliced_relation_before(relation)
84
83
  relation
85
84
  .where(arel_table[field_key].eq(before_cursor_date))
86
85
  .where(arel_table[primary_key].gt(before_cursor_primary_key))
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ # Implements pagination by one field with asc order
6
+ module PrimaryKey
7
+ class Asc < Base
8
+ PAGE_COMPARABLE_METHODS = {
9
+ previous: {query: :lt, cursor: :lteq},
10
+ next: {query: :gt, cursor: :gteq}
11
+ }
12
+
13
+ SLICED_COMPARABLE_METHODS = {
14
+ after: :gt,
15
+ before: :lt
16
+ }.freeze
17
+
18
+ private
19
+
20
+ def first_limited_sorted_table
21
+ arel_table[primary_key].asc
22
+ end
23
+
24
+ def last_limited_sorted_table
25
+ arel_table[primary_key].desc
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ module PrimaryKey
6
+ # Base class for PrimaryKey pagination implementations
7
+ class Base < ::GraphQL::Connections::Base
8
+ COMPARABLE_METHODS = %i[
9
+ gt lt lteq gteq
10
+ ].freeze
11
+
12
+ def initialize(*args, primary_key: nil, **kwargs)
13
+ @primary_key = primary_key
14
+
15
+ super(*args, **kwargs)
16
+ end
17
+
18
+ def has_previous_page
19
+ if last
20
+ nodes.any? && items_exist?(type: :query, search: nodes.first[primary_key], page_type: :previous)
21
+ elsif after
22
+ items_exist?(type: :cursor, search: after_cursor, page_type: :previous)
23
+ else
24
+ false
25
+ end
26
+ end
27
+
28
+ def has_next_page
29
+ if first
30
+ items_exist?(type: :query, search: nodes.last[primary_key], page_type: :next)
31
+ elsif before
32
+ items_exist?(type: :cursor, search: before_cursor, page_type: :next)
33
+ else
34
+ false
35
+ end
36
+ end
37
+
38
+ def cursor_for(item)
39
+ cursor = serialize(item[primary_key])
40
+ cursor = encode(cursor) if opaque_cursor
41
+ cursor
42
+ end
43
+
44
+ private
45
+
46
+ def page_comparable_method(query_type:, page_type:)
47
+ self.class::PAGE_COMPARABLE_METHODS.fetch(page_type).fetch(query_type)
48
+ end
49
+
50
+ def items_exist?(type:, search:, page_type:)
51
+ comparable_method = page_comparable_method(query_type: type, page_type: page_type)
52
+
53
+ if COMPARABLE_METHODS.exclude?(comparable_method)
54
+ raise ArgumentError.new("Unknown #{comparable_method} comparable type. Allowed #{COMPARABLE_METHODS.join(", ")}")
55
+ end
56
+
57
+ items.where(arel_table[primary_key].send(comparable_method, search)).exists?
58
+ end
59
+
60
+ def limited_relation
61
+ scope = sliced_relation
62
+ nodes = []
63
+
64
+ if first
65
+ nodes |=
66
+ scope
67
+ .reorder(first_limited_sorted_table)
68
+ .limit(first)
69
+ .to_a
70
+ end
71
+
72
+ if last
73
+ nodes |=
74
+ scope
75
+ .reorder(last_limited_sorted_table)
76
+ .limit(last)
77
+ .to_a.reverse!
78
+ end
79
+
80
+ nodes
81
+ end
82
+
83
+ def sliced_relation
84
+ items
85
+ .yield_self { |s| after ? sliced_items(items: s, cursor: after_cursor, type: :after) : s }
86
+ .yield_self { |s| before ? sliced_items(items: s, cursor: before_cursor, type: :before) : s }
87
+ end
88
+
89
+ def sliced_items(items:, cursor:, type:)
90
+ items.where(arel_table[primary_key].send(sliced_comparable_method(type), cursor))
91
+ end
92
+
93
+ def sliced_comparable_method(type)
94
+ self.class::SLICED_COMPARABLE_METHODS.fetch(type.to_sym)
95
+ end
96
+
97
+ def first_limited_sorted_table
98
+ raise NotImplementedError.new("method \"#{__method__}\" should be implemented in #{self.class.name} class")
99
+ end
100
+
101
+ def last_limited_sorted_table
102
+ raise NotImplementedError.new("method \"#{__method__}\" should be implemented in #{self.class.name} class")
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GraphQL
4
+ module Connections
5
+ # Implements pagination by one field with desc order
6
+ module PrimaryKey
7
+ class Desc < Base
8
+ PAGE_COMPARABLE_METHODS = {
9
+ previous: {query: :gt, cursor: :gteq},
10
+ next: {query: :lt, cursor: :lteq}
11
+ }.freeze
12
+
13
+ SLICED_COMPARABLE_METHODS = {
14
+ after: :lt,
15
+ before: :gt
16
+ }.freeze
17
+
18
+ private
19
+
20
+ def first_limited_sorted_table
21
+ arel_table[primary_key].desc
22
+ end
23
+
24
+ def last_limited_sorted_table
25
+ arel_table[primary_key].asc
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -14,9 +14,9 @@ module GraphQL
14
14
  module Stable
15
15
  def self.new(*args, desc: false, keys: nil, **kwargs)
16
16
  if kwargs[:primary_key] || keys.nil?
17
- raise NotImplementedError, "desc connection is not implemented yet" if desc
17
+ cls = desc ? GraphQL::Connections::PrimaryKey::Desc : GraphQL::Connections::PrimaryKey::Asc
18
18
 
19
- return GraphQL::Connections::KeyAsc.new(*args, **kwargs)
19
+ return cls.new(*args, **kwargs)
20
20
  end
21
21
 
22
22
  raise ArgumentError, "keyset for more that 2 keys is not implemented yet" if keys.length > 2
@@ -2,6 +2,6 @@
2
2
 
3
3
  module GraphQL
4
4
  module Paging
5
- VERSION = "1.0.0"
5
+ VERSION = "1.3.0"
6
6
  end
7
7
  end
@@ -11,8 +11,13 @@ end
11
11
  require "graphql/connections/stable"
12
12
 
13
13
  require "graphql/connections/base"
14
- require "graphql/connections/key_asc"
15
14
 
16
15
  require "graphql/connections/keyset/base"
17
16
  require "graphql/connections/keyset/asc"
18
17
  require "graphql/connections/keyset/desc"
18
+
19
+ require "graphql/connections/primary_key/base"
20
+ require "graphql/connections/primary_key/asc"
21
+ require "graphql/connections/primary_key/desc"
22
+
23
+ require "graphql/connections/chewy"
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: 1.0.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Misha Merkushin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-07-12 00:00:00.000000000 Z
11
+ date: 2022-05-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -28,16 +28,22 @@ dependencies:
28
28
  name: graphql
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.10'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '3.0'
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - "~>"
41
+ - - ">="
39
42
  - !ruby/object:Gem::Version
40
43
  version: '1.10'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: bundler
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +58,48 @@ dependencies:
52
58
  - - ">="
53
59
  - !ruby/object:Gem::Version
54
60
  version: '1.16'
61
+ - !ruby/object:Gem::Dependency
62
+ name: factory_bot_rails
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '6.2'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '6.2'
75
+ - !ruby/object:Gem::Dependency
76
+ name: faker
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '2.7'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.7'
89
+ - !ruby/object:Gem::Dependency
90
+ name: chewy
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '7.2'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '7.2'
55
103
  - !ruby/object:Gem::Dependency
56
104
  name: combustion
57
105
  requirement: !ruby/object:Gem::Requirement
@@ -161,10 +209,13 @@ files:
161
209
  - README.md
162
210
  - lib/graphql/connections.rb
163
211
  - lib/graphql/connections/base.rb
164
- - lib/graphql/connections/key_asc.rb
212
+ - lib/graphql/connections/chewy.rb
165
213
  - lib/graphql/connections/keyset/asc.rb
166
214
  - lib/graphql/connections/keyset/base.rb
167
215
  - lib/graphql/connections/keyset/desc.rb
216
+ - lib/graphql/connections/primary_key/asc.rb
217
+ - lib/graphql/connections/primary_key/base.rb
218
+ - lib/graphql/connections/primary_key/desc.rb
168
219
  - lib/graphql/connections/stable.rb
169
220
  - lib/graphql/connections/version.rb
170
221
  homepage: https://github.com/bibendi/graphql-connections
@@ -187,7 +238,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
187
238
  - !ruby/object:Gem::Version
188
239
  version: '0'
189
240
  requirements: []
190
- rubygems_version: 3.1.2
241
+ rubygems_version: 3.2.32
191
242
  signing_key:
192
243
  specification_version: 4
193
244
  summary: GraphQL cursor-based stable pagination to work with Active Record relations
@@ -1,69 +0,0 @@
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