-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from DmitryTsepelev/keyset-connections
Keyset connections
- Loading branch information
Showing
12 changed files
with
1,013 additions
and
259 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# frozen_string_literal: true | ||
|
||
module GraphQL | ||
module Connections | ||
# Base class for pagination implementations | ||
class Base < ::GraphQL::Pagination::Connection | ||
attr_reader :opaque_cursor | ||
|
||
delegate :arel_table, to: :items | ||
|
||
def initialize(*args, opaque_cursor: true, **kwargs) | ||
@opaque_cursor = opaque_cursor | ||
|
||
super(*args, **kwargs) | ||
end | ||
|
||
def primary_key | ||
@primary_key ||= items.model.primary_key | ||
end | ||
|
||
def nodes | ||
@nodes ||= limited_relation | ||
end | ||
|
||
def has_previous_page # rubocop:disable Naming/PredicateName | ||
raise NotImplementedError | ||
end | ||
|
||
def has_next_page # rubocop:disable Naming/PredicateName | ||
raise NotImplementedError | ||
end | ||
|
||
def cursor_for(item) | ||
raise NotImplementedError | ||
end | ||
|
||
private | ||
|
||
def serialize(cursor) | ||
case cursor | ||
when Time, DateTime, Date | ||
cursor.iso8601 | ||
else | ||
cursor.to_s | ||
end | ||
end | ||
|
||
def limited_relation | ||
raise NotImplementedError | ||
end | ||
|
||
def sliced_relation | ||
raise NotImplementedError | ||
end | ||
|
||
def after_cursor | ||
@after_cursor ||= opaque_cursor ? decode(after) : after | ||
end | ||
|
||
def before_cursor | ||
@before_cursor ||= opaque_cursor ? decode(before) : before | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
# frozen_string_literal: true | ||
|
||
module GraphQL | ||
module Connections | ||
# Implements pagination by one field with asc order | ||
class KeyAsc < ::GraphQL::Connections::Base | ||
def initialize(*args, primary_key: nil, **kwargs) | ||
@primary_key = primary_key | ||
|
||
super(*args, **kwargs) | ||
end | ||
|
||
def has_previous_page # rubocop:disable Naming/PredicateName, Metrics/AbcSize | ||
if last | ||
nodes.any? && items.where(arel_table[primary_key].lt(nodes.first[primary_key])).exists? | ||
elsif after | ||
items.where(arel_table[primary_key].lteq(after_cursor)).exists? | ||
else | ||
false | ||
end | ||
end | ||
|
||
def has_next_page # rubocop:disable Naming/PredicateName, Metrics/AbcSize | ||
if first | ||
nodes.any? && items.where(arel_table[primary_key].gt(nodes.last[primary_key])).exists? | ||
elsif before | ||
items.where(arel_table[primary_key].gteq(before_cursor)).exists? | ||
else | ||
false | ||
end | ||
end | ||
|
||
def cursor_for(item) | ||
cursor = serialize(item[primary_key]) | ||
cursor = encode(cursor) if opaque_cursor | ||
cursor | ||
end | ||
|
||
private | ||
|
||
def limited_relation # rubocop:disable Metrics/AbcSize, Metrics/MethodLength | ||
scope = sliced_relation | ||
nodes = [] | ||
|
||
if first | ||
nodes |= scope | ||
.reorder(arel_table[primary_key].asc) | ||
.limit(first) | ||
.to_a | ||
end | ||
|
||
if last | ||
nodes |= scope | ||
.reorder(arel_table[primary_key].desc) | ||
.limit(last) | ||
.to_a.reverse! | ||
end | ||
|
||
nodes | ||
end | ||
|
||
def sliced_relation # rubocop:disable Metrics/AbcSize | ||
items | ||
.yield_self { |s| after ? s.where(arel_table[primary_key].gt(after_cursor)) : s } | ||
.yield_self { |s| before ? s.where(arel_table[primary_key].lt(before_cursor)) : s } | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
# frozen_string_literal: true | ||
|
||
module GraphQL | ||
module Connections | ||
module Keyset | ||
# Implements keyset pagination by two fields with asc order | ||
class Asc < ::GraphQL::Connections::Keyset::Base | ||
# rubocop:disable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength | ||
def has_previous_page | ||
if last | ||
nodes.any? && | ||
items.where(arel_table[field_key].eq(nodes.first[field_key])) | ||
.where(arel_table[primary_key].lt(nodes.first[primary_key])) | ||
.or(items.where(arel_table[field_key].lt(nodes.first[field_key]))) | ||
.exists? | ||
elsif after | ||
items | ||
.where(arel_table[field_key].lt(after_cursor_date)) | ||
.or( | ||
items.where(arel_table[field_key].eq(after_cursor_date)) | ||
.where(arel_table[primary_key].lt(after_cursor_primary_key)) | ||
).exists? | ||
else | ||
false | ||
end | ||
end | ||
|
||
def has_next_page | ||
if first | ||
nodes.any? && | ||
items.where(arel_table[field_key].eq(nodes.last[field_key])) | ||
.where(arel_table[primary_key].gt(nodes.last[primary_key])) | ||
.or(items.where(arel_table[field_key].gt(nodes.last[field_key]))) | ||
.exists? | ||
elsif before | ||
items | ||
.where(arel_table[field_key].gt(before_cursor_date)) | ||
.or( | ||
items.where(arel_table[field_key].eq(before_cursor_date)) | ||
.where(arel_table[primary_key].gt(before_cursor_primary_key)) | ||
).exists? | ||
else | ||
false | ||
end | ||
end | ||
# rubocop:enable Naming/PredicateName, Metrics/AbcSize, Metrics/MethodLength | ||
|
||
private | ||
|
||
def limited_relation # rubocop:disable Metrics/AbcSize, Metrics/MethodLength | ||
scope = sliced_relation | ||
nodes = [] | ||
|
||
if first | ||
nodes |= scope | ||
.reorder(arel_table[field_key].asc, arel_table[primary_key].asc) | ||
.limit(first).to_a | ||
end | ||
|
||
if last | ||
nodes |= scope | ||
.reorder(arel_table[field_key].desc, arel_table[primary_key].desc) | ||
.limit(last).to_a.reverse! | ||
end | ||
|
||
nodes | ||
end | ||
|
||
def sliced_relation_after(relation) # rubocop:disable Metrics/AbcSize | ||
relation | ||
.where(arel_table[field_key].eq(after_cursor_date)) | ||
.where(arel_table[primary_key].gt(after_cursor_primary_key)) | ||
.or(relation.where(arel_table[field_key].gt(after_cursor_date))) | ||
end | ||
|
||
def sliced_relation_before(relation) # rubocop:disable Metrics/AbcSize | ||
relation | ||
.where(arel_table[field_key].eq(before_cursor_date)) | ||
.where(arel_table[primary_key].lt(before_cursor_primary_key)) | ||
.or(relation.where(arel_table[field_key].lt(before_cursor_date))) | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
# frozen_string_literal: true | ||
|
||
module GraphQL | ||
module Connections | ||
module Keyset | ||
# Base class for keyset pagination implementations | ||
class Base < ::GraphQL::Connections::Base | ||
attr_reader :field_key | ||
|
||
SEPARATOR = "/" | ||
|
||
def initialize(*args, keys:, separator: SEPARATOR, **kwargs) | ||
@field_key, @primary_key = keys | ||
@separator = separator | ||
|
||
super(*args, **kwargs) | ||
end | ||
|
||
def cursor_for(item) | ||
cursor = [item[field_key], item[primary_key]].map { |value| serialize(value) }.join(@separator) | ||
cursor = encode(cursor) if opaque_cursor | ||
cursor | ||
end | ||
|
||
private | ||
|
||
def sliced_relation | ||
items | ||
.yield_self { |s| after ? sliced_relation_after(s) : s } | ||
.yield_self { |s| before ? sliced_relation_before(s) : s } | ||
end | ||
|
||
def after_cursor_date | ||
@after_cursor_date ||= after_cursor.first | ||
end | ||
|
||
def after_cursor_primary_key | ||
@after_cursor_primary_key ||= after_cursor.last | ||
end | ||
|
||
def after_cursor | ||
@after_cursor ||= super.split(SEPARATOR) | ||
end | ||
|
||
def before_cursor_date | ||
@before_cursor_date ||= before_cursor.first | ||
end | ||
|
||
def before_cursor_primary_key | ||
@before_cursor_primary_key ||= before_cursor.last | ||
end | ||
|
||
def before_cursor | ||
@before_cursor ||= super.split(SEPARATOR) | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.