graphql-connections 0.2.0 → 1.2.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 +4 -4
- data/README.md +51 -7
- data/lib/graphql/connections/base.rb +65 -0
- data/lib/graphql/connections/chewy.rb +37 -0
- data/lib/graphql/connections/key_asc.rb +69 -0
- data/lib/graphql/connections/keyset/asc.rb +84 -0
- data/lib/graphql/connections/keyset/base.rb +59 -0
- data/lib/graphql/connections/keyset/desc.rb +91 -0
- data/lib/graphql/connections/stable.rb +8 -86
- data/lib/graphql/connections/version.rb +1 -1
- data/lib/graphql/connections.rb +9 -0
- metadata +70 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1206b9dbdd7d135f43b52b0a62b4d28b4d5d8c93bb5e8a18800ef5abcc5a6157
|
|
4
|
+
data.tar.gz: 509ba8323078d1dee9095c18ec227b8416967ad0ce51bb4e3bd2806f09a28a57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 63f191221ff5b1e2133ad84bb8cb377941cad75de324310f18734729f5a519dae01f466a88539e895a155f32e39eccfab60f423271a220b4cf3fcfa33bfdbb3b
|
|
7
|
+
data.tar.gz: da4d990bcc8941fbce16049f0a4ea6aa642f73194a34bfb757c1c0080bd16da1736e4466f2ee7f2868dcc5ceebc8677b745a6c3ea43328561af8ad09f9c9e71f
|
data/README.md
CHANGED
|
@@ -3,10 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# GraphQL::Connections
|
|
5
5
|
|
|
6
|
-
|
|
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
|
|
@@ -37,6 +39,28 @@ Records are sorted by model's primary key by default. You can change this behavi
|
|
|
37
39
|
GraphQL::Connections::Stable.new(Message.all, primary_key: :created_at)
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
In case when you want records to be sorted by more than one field (i.e., _keyset pagination_), you can use `keys` param:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
GraphQL::Connections::Stable.new(Message.all, keys: %w[name id])
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When you pass only one key, a primary key will be added as a second one:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
GraphQL::Connections::Stable.new(Message.all, keys: [:name])
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**NOTE:** Currently we support maximum two keys in the keyset.
|
|
55
|
+
|
|
56
|
+
Also, you can pass the `:desc` option to reverse the relation:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
GraphQL::Connections::Stable.new(Message.all, keys: %w[name id], desc: true)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**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 🙂.
|
|
63
|
+
|
|
40
64
|
Also, you can disable opaque cursors by setting `opaque_cursor` param:
|
|
41
65
|
|
|
42
66
|
```ruby
|
|
@@ -47,13 +71,33 @@ Or you can apply a stable connection to all Active Record relations returning by
|
|
|
47
71
|
|
|
48
72
|
```ruby
|
|
49
73
|
class ApplicationSchema < GraphQL::Schema
|
|
50
|
-
use GraphQL::Pagination::Connections
|
|
51
|
-
|
|
52
74
|
connections.add(ActiveRecord::Relation, GraphQL::Connections::Stable)
|
|
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.
|
|
79
|
+
|
|
80
|
+
### Elasticsearch via Chewy
|
|
81
|
+
|
|
82
|
+
Register connection for all Chewy requests:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class ApplicationSchema < GraphQL::Schema
|
|
86
|
+
connections.add(Chewy::Search::Request, GraphQL::Connections::ChewyConnection)
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
And define field like below:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
field :messages, Types::Message.connection_type, null: false
|
|
94
|
+
|
|
95
|
+
def messages
|
|
96
|
+
CitiesIndex.query(match: {name: "Moscow"})
|
|
97
|
+
end
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**NOTE:** Using `first` and `last`arguments simultaneously is not supported yet.
|
|
57
101
|
|
|
58
102
|
## Development
|
|
59
103
|
|
|
@@ -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
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def has_next_page
|
|
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,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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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,84 @@
|
|
|
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
|
+
def has_previous_page
|
|
9
|
+
if last
|
|
10
|
+
nodes.any? &&
|
|
11
|
+
items.where(arel_table[field_key].eq(nodes.first[field_key]))
|
|
12
|
+
.where(arel_table[primary_key].lt(nodes.first[primary_key]))
|
|
13
|
+
.or(items.where(arel_table[field_key].lt(nodes.first[field_key])))
|
|
14
|
+
.exists?
|
|
15
|
+
elsif after
|
|
16
|
+
items
|
|
17
|
+
.where(arel_table[field_key].lt(after_cursor_date))
|
|
18
|
+
.or(
|
|
19
|
+
items.where(arel_table[field_key].eq(after_cursor_date))
|
|
20
|
+
.where(arel_table[primary_key].lt(after_cursor_primary_key))
|
|
21
|
+
).exists?
|
|
22
|
+
else
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def has_next_page
|
|
28
|
+
if first
|
|
29
|
+
nodes.any? &&
|
|
30
|
+
items.where(arel_table[field_key].eq(nodes.last[field_key]))
|
|
31
|
+
.where(arel_table[primary_key].gt(nodes.last[primary_key]))
|
|
32
|
+
.or(items.where(arel_table[field_key].gt(nodes.last[field_key])))
|
|
33
|
+
.exists?
|
|
34
|
+
elsif before
|
|
35
|
+
items
|
|
36
|
+
.where(arel_table[field_key].gt(before_cursor_date))
|
|
37
|
+
.or(
|
|
38
|
+
items.where(arel_table[field_key].eq(before_cursor_date))
|
|
39
|
+
.where(arel_table[primary_key].gt(before_cursor_primary_key))
|
|
40
|
+
).exists?
|
|
41
|
+
else
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
# rubocop:enable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def limited_relation
|
|
50
|
+
scope = sliced_relation
|
|
51
|
+
nodes = []
|
|
52
|
+
|
|
53
|
+
if first
|
|
54
|
+
nodes |= scope
|
|
55
|
+
.reorder(arel_table[field_key].asc, arel_table[primary_key].asc)
|
|
56
|
+
.limit(first).to_a
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if last
|
|
60
|
+
nodes |= scope
|
|
61
|
+
.reorder(arel_table[field_key].desc, arel_table[primary_key].desc)
|
|
62
|
+
.limit(last).to_a.reverse!
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
nodes
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sliced_relation_after(relation)
|
|
69
|
+
relation
|
|
70
|
+
.where(arel_table[field_key].eq(after_cursor_date))
|
|
71
|
+
.where(arel_table[primary_key].gt(after_cursor_primary_key))
|
|
72
|
+
.or(relation.where(arel_table[field_key].gt(after_cursor_date)))
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sliced_relation_before(relation)
|
|
76
|
+
relation
|
|
77
|
+
.where(arel_table[field_key].eq(before_cursor_date))
|
|
78
|
+
.where(arel_table[primary_key].lt(before_cursor_primary_key))
|
|
79
|
+
.or(relation.where(arel_table[field_key].lt(before_cursor_date)))
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
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,91 @@
|
|
|
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
|
+
def has_previous_page
|
|
9
|
+
if last
|
|
10
|
+
nodes.any? &&
|
|
11
|
+
items.where(arel_table[field_key].eq(nodes.first[field_key]))
|
|
12
|
+
.where(arel_table[primary_key].gt(nodes.first[primary_key]))
|
|
13
|
+
.or(items.where(arel_table[field_key].gt(nodes.first[field_key])))
|
|
14
|
+
.exists?
|
|
15
|
+
elsif after
|
|
16
|
+
items
|
|
17
|
+
.where(arel_table[field_key].gt(after_cursor_date))
|
|
18
|
+
.or(
|
|
19
|
+
items.where(arel_table[field_key].eq(after_cursor_date))
|
|
20
|
+
.where(arel_table[primary_key].gt(after_cursor_primary_key))
|
|
21
|
+
).exists?
|
|
22
|
+
else
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def has_next_page
|
|
28
|
+
if first
|
|
29
|
+
nodes.any? &&
|
|
30
|
+
items.where(arel_table[field_key].eq(nodes.last[field_key]))
|
|
31
|
+
.where(arel_table[primary_key].lt(nodes.last[primary_key]))
|
|
32
|
+
.or(items.where(arel_table[field_key].lt(nodes.last[field_key])))
|
|
33
|
+
.exists?
|
|
34
|
+
elsif before
|
|
35
|
+
items
|
|
36
|
+
.where(arel_table[field_key].lt(before_cursor_date))
|
|
37
|
+
.or(
|
|
38
|
+
items.where(arel_table[field_key].eq(before_cursor_date))
|
|
39
|
+
.where(arel_table[primary_key].lt(before_cursor_primary_key))
|
|
40
|
+
).exists?
|
|
41
|
+
else
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
# rubocop:enable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength
|
|
46
|
+
|
|
47
|
+
def cursor_for(item)
|
|
48
|
+
cursor = [item[field_key], item[primary_key]].map { |value| serialize(value) }.join(@separator)
|
|
49
|
+
cursor = encode(cursor) if opaque_cursor
|
|
50
|
+
cursor
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def limited_relation
|
|
56
|
+
scope = sliced_relation
|
|
57
|
+
nodes = []
|
|
58
|
+
|
|
59
|
+
if first
|
|
60
|
+
nodes |= scope
|
|
61
|
+
.reorder(arel_table[field_key].desc, arel_table[primary_key].desc)
|
|
62
|
+
.limit(first).to_a
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if last
|
|
66
|
+
nodes |= scope
|
|
67
|
+
.reorder(arel_table[field_key].asc, arel_table[primary_key].asc)
|
|
68
|
+
.limit(last)
|
|
69
|
+
.to_a.reverse!
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
nodes
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sliced_relation_after(relation)
|
|
76
|
+
relation
|
|
77
|
+
.where(arel_table[field_key].eq(after_cursor_date))
|
|
78
|
+
.where(arel_table[primary_key].lt(after_cursor_primary_key))
|
|
79
|
+
.or(relation.where(arel_table[field_key].lt(after_cursor_date)))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sliced_relation_before(relation)
|
|
83
|
+
relation
|
|
84
|
+
.where(arel_table[field_key].eq(before_cursor_date))
|
|
85
|
+
.where(arel_table[primary_key].gt(before_cursor_primary_key))
|
|
86
|
+
.or(relation.where(arel_table[field_key].gt(before_cursor_date)))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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
|
data/lib/graphql/connections.rb
CHANGED
|
@@ -9,3 +9,12 @@ 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"
|
|
19
|
+
|
|
20
|
+
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:
|
|
4
|
+
version: 1.2.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:
|
|
11
|
+
date: 2022-04-04 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
|
|
@@ -114,43 +162,43 @@ dependencies:
|
|
|
114
162
|
requirements:
|
|
115
163
|
- - "~>"
|
|
116
164
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: '
|
|
165
|
+
version: '5.0'
|
|
118
166
|
type: :development
|
|
119
167
|
prerelease: false
|
|
120
168
|
version_requirements: !ruby/object:Gem::Requirement
|
|
121
169
|
requirements:
|
|
122
170
|
- - "~>"
|
|
123
171
|
- !ruby/object:Gem::Version
|
|
124
|
-
version: '
|
|
172
|
+
version: '5.0'
|
|
125
173
|
- !ruby/object:Gem::Dependency
|
|
126
|
-
name:
|
|
174
|
+
name: standard
|
|
127
175
|
requirement: !ruby/object:Gem::Requirement
|
|
128
176
|
requirements:
|
|
129
177
|
- - "~>"
|
|
130
178
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '
|
|
179
|
+
version: '1.1'
|
|
132
180
|
type: :development
|
|
133
181
|
prerelease: false
|
|
134
182
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
183
|
requirements:
|
|
136
184
|
- - "~>"
|
|
137
185
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '
|
|
186
|
+
version: '1.1'
|
|
139
187
|
- !ruby/object:Gem::Dependency
|
|
140
188
|
name: test-prof
|
|
141
189
|
requirement: !ruby/object:Gem::Requirement
|
|
142
190
|
requirements:
|
|
143
191
|
- - "~>"
|
|
144
192
|
- !ruby/object:Gem::Version
|
|
145
|
-
version: '0
|
|
193
|
+
version: '1.0'
|
|
146
194
|
type: :development
|
|
147
195
|
prerelease: false
|
|
148
196
|
version_requirements: !ruby/object:Gem::Requirement
|
|
149
197
|
requirements:
|
|
150
198
|
- - "~>"
|
|
151
199
|
- !ruby/object:Gem::Version
|
|
152
|
-
version: '0
|
|
153
|
-
description:
|
|
200
|
+
version: '1.0'
|
|
201
|
+
description:
|
|
154
202
|
email:
|
|
155
203
|
- merkushin.m.s@gmail.com
|
|
156
204
|
executables: []
|
|
@@ -160,6 +208,12 @@ files:
|
|
|
160
208
|
- LICENSE.txt
|
|
161
209
|
- README.md
|
|
162
210
|
- lib/graphql/connections.rb
|
|
211
|
+
- lib/graphql/connections/base.rb
|
|
212
|
+
- lib/graphql/connections/chewy.rb
|
|
213
|
+
- lib/graphql/connections/key_asc.rb
|
|
214
|
+
- lib/graphql/connections/keyset/asc.rb
|
|
215
|
+
- lib/graphql/connections/keyset/base.rb
|
|
216
|
+
- lib/graphql/connections/keyset/desc.rb
|
|
163
217
|
- lib/graphql/connections/stable.rb
|
|
164
218
|
- lib/graphql/connections/version.rb
|
|
165
219
|
homepage: https://github.com/bibendi/graphql-connections
|
|
@@ -167,7 +221,7 @@ licenses:
|
|
|
167
221
|
- MIT
|
|
168
222
|
metadata:
|
|
169
223
|
allowed_push_host: https://rubygems.org
|
|
170
|
-
post_install_message:
|
|
224
|
+
post_install_message:
|
|
171
225
|
rdoc_options: []
|
|
172
226
|
require_paths:
|
|
173
227
|
- lib
|
|
@@ -182,8 +236,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
182
236
|
- !ruby/object:Gem::Version
|
|
183
237
|
version: '0'
|
|
184
238
|
requirements: []
|
|
185
|
-
rubygems_version: 3.
|
|
186
|
-
signing_key:
|
|
239
|
+
rubygems_version: 3.2.32
|
|
240
|
+
signing_key:
|
|
187
241
|
specification_version: 4
|
|
188
242
|
summary: GraphQL cursor-based stable pagination to work with Active Record relations
|
|
189
243
|
test_files: []
|