graphql-connections 1.0.0 → 1.3.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: 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