mongoid-scroll 0.1.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.
- data/.gitignore +4 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +9 -0
- data/LICENSE.md +22 -0
- data/README.md +87 -0
- data/Rakefile +24 -0
- data/lib/config/locales/en.yml +22 -0
- data/lib/mongoid/criterion/scrollable.rb +33 -0
- data/lib/mongoid/scroll/cursor.rb +78 -0
- data/lib/mongoid/scroll/errors/base.rb +81 -0
- data/lib/mongoid/scroll/errors/invalid_cursor_error.rb +13 -0
- data/lib/mongoid/scroll/errors/multiple_sort_fields_error.rb +13 -0
- data/lib/mongoid/scroll/errors/no_such_field_error.rb +13 -0
- data/lib/mongoid/scroll/errors/unsupported_field_type_error.rb +13 -0
- data/lib/mongoid/scroll/errors.rb +5 -0
- data/lib/mongoid/scroll/version.rb +5 -0
- data/lib/mongoid-scroll.rb +11 -0
- data/lib/mongoid_scroll.rb +2 -0
- data/mongoid-scroll.gemspec +20 -0
- data/spec/mongoid/criteria_spec.rb +98 -0
- data/spec/mongoid/scroll_cursor_spec.rb +106 -0
- data/spec/mongoid/scroll_spec.rb +6 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/support/feed/item.rb +13 -0
- metadata +107 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2012 Daniel Doubrovkine, Artsy Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
Mongoid::Scroll [](https://travis-ci.org/dblock/mongoid-scroll)
|
2
|
+
===============
|
3
|
+
|
4
|
+
Mongoid extension that enable infinite scrolling.
|
5
|
+
|
6
|
+
The Problem
|
7
|
+
-----------
|
8
|
+
|
9
|
+
Traditional pagination does not work when data changes between paginated requests, which makes it unsuitable for infinite scroll behaviors.
|
10
|
+
|
11
|
+
* If a record is inserted before the current page limit, the collection will shift to the right, and the returned result will include a duplicate from a previous page.
|
12
|
+
* If a record is removed before the current page limit, the collection will shift to the left, and the returned result will be missing a record.
|
13
|
+
|
14
|
+
The solution implemented by the `scroll` extension paginates data using a cursor, giving you the ability to restart pagination where you left it off. This is a non-trivial problem when combined with sorting over non-unique record fields, such as timestamps.
|
15
|
+
|
16
|
+
Usage
|
17
|
+
-----
|
18
|
+
|
19
|
+
Add `mongoid-scroll` to Gemfile.
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
gem 'mongoid-scroll'
|
23
|
+
```
|
24
|
+
|
25
|
+
A sample model.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
module Feed
|
29
|
+
class Item
|
30
|
+
include Mongoid::Document
|
31
|
+
field :content, type: String
|
32
|
+
field :created_at, type: DateTime
|
33
|
+
end
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
Scroll and save a cursor to the last item.
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
saved_cursor = nil
|
41
|
+
Feed::Item.desc(:created_at).limit(5).scroll do |record, next_cursor|
|
42
|
+
# each record, one-by-one
|
43
|
+
saved_cursor = next_cursor
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
Resume iterating using the previously saved cursor.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
Feed::Item.desc(:created_at).limit(5).scroll(saved_cursor) do |record, next_cursor|
|
51
|
+
# each record, one-by-one
|
52
|
+
saved_cursor = next_cursor
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
The iteration finishes when no more records are available. You can also finish iterating over the remaining records by omitting the query limit.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
Feed::Item.desc(:created_at).scroll(saved_cursor) do |record, next_cursor|
|
60
|
+
# each record, one-by-one
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
Cursors
|
65
|
+
-------
|
66
|
+
|
67
|
+
You can use `Mongoid::Scroll::Cursor.from_record` to generate a cursor. This can be useful when you just want to return a collection of results and the cursor pointing to after the last item.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
record = Feed::Item.desc(:created_at).limit(3).last
|
71
|
+
cursor = Mongoid::Scroll::Cursor.from_record(record, { field: Feed::Item.fields["created_at"] })
|
72
|
+
# cursor or cursor.to_s can be returned to a client and passed into .scroll(cursor)
|
73
|
+
```
|
74
|
+
|
75
|
+
Note that unlike MongoDB cursors, `Mongoid::Scroll::Cursor` values don't expire.
|
76
|
+
|
77
|
+
Contributing
|
78
|
+
------------
|
79
|
+
|
80
|
+
Fork the project. Make your feature addition or bug fix with tests. Send a pull request. Bonus points for topic branches.
|
81
|
+
|
82
|
+
Copyright and License
|
83
|
+
---------------------
|
84
|
+
|
85
|
+
MIT License, see [LICENSE](http://github.com/dblock/mongoid-scroll/raw/master/LICENSE.md) for details.
|
86
|
+
|
87
|
+
(c) 2013 [Daniel Doubrovkine](http://github.com/dblock), based on code by [Frank Macreery](http://github.com/macreery), [Artsy Inc.](http://artsy.net)
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
|
4
|
+
require File.expand_path('../lib/mongoid/scroll/version', __FILE__)
|
5
|
+
|
6
|
+
begin
|
7
|
+
Bundler.setup(:default, :development)
|
8
|
+
rescue Bundler::BundlerError => e
|
9
|
+
$stderr.puts e.message
|
10
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
+
exit e.status_code
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rake'
|
15
|
+
|
16
|
+
require 'rspec/core'
|
17
|
+
require 'rspec/core/rake_task'
|
18
|
+
|
19
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
20
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
21
|
+
end
|
22
|
+
|
23
|
+
task :default => :spec
|
24
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
en:
|
2
|
+
mongoid:
|
3
|
+
scroll:
|
4
|
+
errors:
|
5
|
+
messages:
|
6
|
+
multiple_sort_fields:
|
7
|
+
message: "Scrolling over a criteria with multiple fields is not supported."
|
8
|
+
summary: "You're attempting to scroll over data with a sort order that includes multiple fields: %{sort}."
|
9
|
+
resolution: "Simplify the sort order to a single field."
|
10
|
+
invalid_cursor:
|
11
|
+
message: "The cursor supplied is invalid."
|
12
|
+
summary: "The cursor supplied is invalid: %{cursor}."
|
13
|
+
resolution: "Cursors must be in the form 'value:tiebreak_id'."
|
14
|
+
no_such_field:
|
15
|
+
message: "Invalid field."
|
16
|
+
summary: "The field supplied in the cursor does not exist: %{field}."
|
17
|
+
resolution: "Has the model changed or are you not sorting the criteria by the right field."
|
18
|
+
unsupported_field_type:
|
19
|
+
message: "Unsupported field type."
|
20
|
+
summary: "The type of the field '%{field}' is not supported: %{type}."
|
21
|
+
resolution: "Please open a feature request in https://github.com/dblock/mongoid-scroll."
|
22
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Criterion
|
3
|
+
module Scrollable
|
4
|
+
|
5
|
+
def scroll(cursor = nil, &block)
|
6
|
+
c = self
|
7
|
+
# we don't support scrolling over a criteria with multiple fields
|
8
|
+
if c.options[:sort] && c.options[:sort].keys.count != 1
|
9
|
+
sort = c.options[:sort].keys.join(", ")
|
10
|
+
raise Mongoid::Scroll::Errors::MultipleSortFieldsError.new(sort: sort)
|
11
|
+
end
|
12
|
+
# introduce a default sort order if there's none
|
13
|
+
c = c.asc(:_id) if (! c.options[:sort]) || c.options[:sort].empty?
|
14
|
+
# scroll field and direction
|
15
|
+
scroll_field = c.options[:sort].keys.first
|
16
|
+
scroll_direction = c.options[:sort].values.first.to_i == 1 ? '$gt' : '$lt'
|
17
|
+
# scroll cursor from the parameter, with value and tiebreak_id
|
18
|
+
field = c.klass.fields[scroll_field.to_s]
|
19
|
+
cursor_options = { field: field, direction: scroll_direction }
|
20
|
+
cursor = cursor.is_a?(Mongoid::Scroll::Cursor) ? cursor : Mongoid::Scroll::Cursor.new(cursor, cursor_options)
|
21
|
+
# scroll
|
22
|
+
if block_given?
|
23
|
+
c.where(cursor.criteria).each do |record|
|
24
|
+
yield record, Mongoid::Scroll::Cursor.from_record(record, cursor_options)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
c
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Scroll
|
3
|
+
class Cursor
|
4
|
+
|
5
|
+
attr_accessor :value, :tiebreak_id, :field, :direction
|
6
|
+
|
7
|
+
def initialize(value = nil, options = {})
|
8
|
+
unless options && (@field = options[:field])
|
9
|
+
raise ArgumentError.new "Missing options[:field]."
|
10
|
+
end
|
11
|
+
@direction = options[:direction] || '$gt'
|
12
|
+
@value, @tiebreak_id = Mongoid::Scroll::Cursor.parse(value, options)
|
13
|
+
end
|
14
|
+
|
15
|
+
def criteria
|
16
|
+
cursor_criteria = { field.name => { direction => value } } if value
|
17
|
+
tiebreak_criteria = { field.name => value, :_id => { '$gt' => tiebreak_id } } if value && tiebreak_id
|
18
|
+
(cursor_criteria || tiebreak_criteria) ? { '$or' => [ cursor_criteria, tiebreak_criteria].compact } : {}
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
def from_record(record, options)
|
23
|
+
unless options && (field = options[:field])
|
24
|
+
raise ArgumentError.new "Missing options[:field]."
|
25
|
+
end
|
26
|
+
Mongoid::Scroll::Cursor.new("#{transform_field_value(field, record.send(field.name))}:#{record.id}", options)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
tiebreak_id ? [ Mongoid::Scroll::Cursor.transform_field_value(field, value), tiebreak_id ].join(":") : nil
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
class << self
|
37
|
+
|
38
|
+
def parse(value, options)
|
39
|
+
return [ nil, nil ] unless value
|
40
|
+
parts = value.split(":")
|
41
|
+
unless parts.length >= 2
|
42
|
+
raise Mongoid::Scroll::Errors::InvalidCursorError.new({ cursor: value })
|
43
|
+
end
|
44
|
+
id = parts[-1]
|
45
|
+
value = parts[0...-1].join(":")
|
46
|
+
[ parse_field_value(options[:field], value), Moped::BSON::ObjectId(id) ]
|
47
|
+
end
|
48
|
+
|
49
|
+
def parse_field_value(field, value)
|
50
|
+
case field.type.to_s
|
51
|
+
when "String" then value.to_s
|
52
|
+
when "DateTime" then Time.at(value.to_i).to_datetime
|
53
|
+
when "Time" then Time.at(value.to_i)
|
54
|
+
when "Date" then Time.at(value.to_i).utc.to_date
|
55
|
+
when "Float" then value.to_f
|
56
|
+
when "Integer" then value.to_i
|
57
|
+
else
|
58
|
+
raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field.name, type: field.type)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def transform_field_value(field, value)
|
63
|
+
case field.type.to_s
|
64
|
+
when "String" then value.to_s
|
65
|
+
when "Date" then value.to_time(:utc).to_i
|
66
|
+
when "DateTime", "Time" then value.to_i
|
67
|
+
when "Float" then value.to_f
|
68
|
+
when "Integer" then value.to_i
|
69
|
+
else
|
70
|
+
raise Mongoid::Scroll::Errors::UnsupportedFieldTypeError.new(field: field.name, type: field.type)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Scroll
|
3
|
+
module Errors
|
4
|
+
class Base < StandardError
|
5
|
+
|
6
|
+
# Problem occurred.
|
7
|
+
attr_reader :problem
|
8
|
+
|
9
|
+
# Summary of the problem.
|
10
|
+
attr_reader :summary
|
11
|
+
|
12
|
+
# Suggested problem resolution.
|
13
|
+
attr_reader :resolution
|
14
|
+
|
15
|
+
# Compose the message.
|
16
|
+
# === Parameters
|
17
|
+
# [key] Lookup key in the translation table.
|
18
|
+
# [attributes] The objects to pass to create the message.
|
19
|
+
def compose_message(key, attributes = {})
|
20
|
+
@problem = create_problem(key, attributes)
|
21
|
+
@summary = create_summary(key, attributes)
|
22
|
+
@resolution = create_resolution(key, attributes)
|
23
|
+
|
24
|
+
"\nProblem:\n #{@problem}"+
|
25
|
+
"\nSummary:\n #{@summary}"+
|
26
|
+
"\nResolution:\n #{@resolution}"
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
BASE_KEY = "mongoid.scroll.errors.messages" #:nodoc:
|
32
|
+
|
33
|
+
# Given the key of the specific error and the options hash, translate the
|
34
|
+
# message.
|
35
|
+
#
|
36
|
+
# === Parameters
|
37
|
+
# [key] The key of the error in the locales.
|
38
|
+
# [options] The objects to pass to create the message.
|
39
|
+
#
|
40
|
+
# Returns a localized error message string.
|
41
|
+
def translate(key, options)
|
42
|
+
::I18n.translate("#{BASE_KEY}.#{key}", { :locale => :en }.merge(options)).strip
|
43
|
+
end
|
44
|
+
|
45
|
+
# Create the problem.
|
46
|
+
#
|
47
|
+
# === Parameters
|
48
|
+
# [key] The error key.
|
49
|
+
# [attributes] The attributes to interpolate.
|
50
|
+
#
|
51
|
+
# Returns the problem.
|
52
|
+
def create_problem(key, attributes)
|
53
|
+
translate("#{key}.message", attributes)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Create the summary.
|
57
|
+
#
|
58
|
+
# === Parameters
|
59
|
+
# [key] The error key.
|
60
|
+
# [attributes] The attributes to interpolate.
|
61
|
+
#
|
62
|
+
# Returns the summary.
|
63
|
+
def create_summary(key, attributes)
|
64
|
+
translate("#{key}.summary", attributes)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create the resolution.
|
68
|
+
#
|
69
|
+
# === Parameters
|
70
|
+
# [key] The error key.
|
71
|
+
# [attributes] The attributes to interpolate.
|
72
|
+
#
|
73
|
+
# Returns the resolution.
|
74
|
+
def create_resolution(key, attributes)
|
75
|
+
translate("#{key}.resolution", attributes)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
I18n.load_path << File.join(File.dirname(__FILE__), "config", "locales", "en.yml")
|
4
|
+
|
5
|
+
require 'mongoid'
|
6
|
+
require 'mongoid/scroll/version'
|
7
|
+
require 'mongoid/scroll/errors'
|
8
|
+
require 'mongoid/scroll/cursor'
|
9
|
+
require 'mongoid/criterion/scrollable'
|
10
|
+
|
11
|
+
Mongoid::Criteria.send(:include, Mongoid::Criterion::Scrollable)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
require "mongoid/scroll/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "mongoid-scroll"
|
6
|
+
s.version = Mongoid::Scroll::VERSION
|
7
|
+
s.authors = [ "Daniel Doubrovkine", "Frank Macreery" ]
|
8
|
+
s.email = "dblock@dblock.org"
|
9
|
+
s.platform = Gem::Platform::RUBY
|
10
|
+
s.required_rubygems_version = '>= 1.3.6'
|
11
|
+
s.files = `git ls-files`.split("\n")
|
12
|
+
s.require_paths = [ "lib" ]
|
13
|
+
s.homepage = "http://github.com/dblock/mongoid-scroll"
|
14
|
+
s.licenses = [ "MIT" ]
|
15
|
+
s.summary = "Mongoid extensions to enable infinite scroll."
|
16
|
+
s.add_dependency "mongoid", ">= 3.0"
|
17
|
+
s.add_dependency "i18n"
|
18
|
+
end
|
19
|
+
|
20
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Criteria do
|
4
|
+
context "scrollable" do
|
5
|
+
subject do
|
6
|
+
Feed::Item
|
7
|
+
end
|
8
|
+
it ":scroll" do
|
9
|
+
subject.should.respond_to? :scroll
|
10
|
+
end
|
11
|
+
end
|
12
|
+
context "with multiple sort fields" do
|
13
|
+
subject do
|
14
|
+
Feed::Item.desc(:name).asc(:value)
|
15
|
+
end
|
16
|
+
it "raises Mongoid::Scroll::Errors::MultipleSortFieldsError" do
|
17
|
+
expect { subject.scroll }.to raise_error Mongoid::Scroll::Errors::MultipleSortFieldsError,
|
18
|
+
/You're attempting to scroll over data with a sort order that includes multiple fields: name, value./
|
19
|
+
end
|
20
|
+
end
|
21
|
+
context "with no sort" do
|
22
|
+
subject do
|
23
|
+
Feed::Item.all
|
24
|
+
end
|
25
|
+
it "adds a default sort by _id" do
|
26
|
+
subject.scroll.options[:sort].should == { "_id" => 1 }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
context "with data" do
|
30
|
+
before :each do
|
31
|
+
10.times do |i|
|
32
|
+
Feed::Item.create!(
|
33
|
+
a_string: i.to_s,
|
34
|
+
a_integer: i,
|
35
|
+
a_datetime: DateTime.new(2013, i + 1, 21, 1, 42, 3)
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
context "integer" do
|
40
|
+
it "scrolls all with a block" do
|
41
|
+
records = []
|
42
|
+
Feed::Item.asc(:a_integer).scroll do |record, next_cursor|
|
43
|
+
records << record
|
44
|
+
end
|
45
|
+
records.size.should == 10
|
46
|
+
records.should eq Feed::Item.all.to_a
|
47
|
+
end
|
48
|
+
it "scrolls all with a break" do
|
49
|
+
records = []
|
50
|
+
cursor = nil
|
51
|
+
Feed::Item.asc(:a_integer).limit(5).scroll do |record, next_cursor|
|
52
|
+
records << record
|
53
|
+
cursor = next_cursor
|
54
|
+
end
|
55
|
+
records.size.should == 5
|
56
|
+
Feed::Item.asc(:a_integer).scroll(cursor) do |record, next_cursor|
|
57
|
+
records << record
|
58
|
+
cursor = next_cursor
|
59
|
+
end
|
60
|
+
records.size.should == 10
|
61
|
+
records.should eq Feed::Item.all.to_a
|
62
|
+
end
|
63
|
+
it "scrolls in descending order" do
|
64
|
+
records = []
|
65
|
+
Feed::Item.desc(:a_integer).limit(3).scroll do |record, next_cursor|
|
66
|
+
records << record
|
67
|
+
end
|
68
|
+
records.size.should == 3
|
69
|
+
records.should eq Feed::Item.desc(:a_integer).limit(3).to_a
|
70
|
+
end
|
71
|
+
it "map" do
|
72
|
+
record = Feed::Item.desc(:a_integer).limit(3).scroll.map { |record, cursor| record }.last
|
73
|
+
cursor = Mongoid::Scroll::Cursor.from_record(record, { field: Feed::Item.fields["a_integer"] })
|
74
|
+
cursor.should_not be_nil
|
75
|
+
cursor.to_s.split(":").should == [ record.a_integer.to_s, record.id.to_s ]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
context "with overlapping data" do
|
80
|
+
before :each do
|
81
|
+
3.times { Feed::Item.create! a_integer: 5 }
|
82
|
+
end
|
83
|
+
it "scrolls" do
|
84
|
+
records = []
|
85
|
+
cursor = nil
|
86
|
+
Feed::Item.desc(:a_integer).limit(2).scroll do |record, next_cursor|
|
87
|
+
records << record
|
88
|
+
cursor = next_cursor
|
89
|
+
end
|
90
|
+
records.size.should == 2
|
91
|
+
Feed::Item.desc(:a_integer).scroll(cursor) do |record, next_cursor|
|
92
|
+
records << record
|
93
|
+
end
|
94
|
+
records.size.should == 3
|
95
|
+
records.should eq Feed::Item.all.to_a
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mongoid::Scroll::Cursor do
|
4
|
+
context "an empty cursor" do
|
5
|
+
subject do
|
6
|
+
Mongoid::Scroll::Cursor.new(nil, { field: Feed::Item.fields["a_string"]})
|
7
|
+
end
|
8
|
+
its(:tiebreak_id) { should be_nil }
|
9
|
+
its(:value) { should be_nil }
|
10
|
+
its(:criteria) { should eq({}) }
|
11
|
+
end
|
12
|
+
context "an invalid cursor" do
|
13
|
+
it "raises InvalidCursorError" do
|
14
|
+
expect { Mongoid::Scroll::Cursor.new "invalid", field: Feed::Item.fields["a_string"] }.to raise_error Mongoid::Scroll::Errors::InvalidCursorError,
|
15
|
+
/The cursor supplied is invalid: invalid./
|
16
|
+
end
|
17
|
+
end
|
18
|
+
context "a string field cursor" do
|
19
|
+
let(:feed_item) { Feed::Item.create!(a_string: "astring") }
|
20
|
+
subject do
|
21
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_string}:#{feed_item.id}", field: Feed::Item.fields["a_string"]
|
22
|
+
end
|
23
|
+
its(:value) { should eq feed_item.a_string }
|
24
|
+
its(:tiebreak_id) { should eq feed_item.id }
|
25
|
+
its(:criteria) {
|
26
|
+
should eq({ "$or" => [
|
27
|
+
{ "a_string" => { "$gt" => feed_item.a_string }},
|
28
|
+
{ "a_string" => feed_item.a_string, :_id => { "$gt" => feed_item.id }}
|
29
|
+
]})
|
30
|
+
}
|
31
|
+
end
|
32
|
+
context "an integer field cursor" do
|
33
|
+
let(:feed_item) { Feed::Item.create!(a_integer: 10) }
|
34
|
+
subject do
|
35
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_integer}:#{feed_item.id}", field: Feed::Item.fields["a_integer"]
|
36
|
+
end
|
37
|
+
its(:value) { should eq feed_item.a_integer }
|
38
|
+
its(:tiebreak_id) { should eq feed_item.id }
|
39
|
+
its(:criteria) {
|
40
|
+
should eq({ "$or" => [
|
41
|
+
{ "a_integer" => { "$gt" => feed_item.a_integer }},
|
42
|
+
{ "a_integer" => feed_item.a_integer, :_id => { "$gt" => feed_item.id }}
|
43
|
+
]})
|
44
|
+
}
|
45
|
+
end
|
46
|
+
context "a date/time field cursor" do
|
47
|
+
let(:feed_item) { Feed::Item.create!(a_datetime: DateTime.new(2013, 12, 21, 1, 42, 3)) }
|
48
|
+
subject do
|
49
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_datetime.to_i}:#{feed_item.id}", field: Feed::Item.fields["a_datetime"]
|
50
|
+
end
|
51
|
+
its(:value) { should eq feed_item.a_datetime }
|
52
|
+
its(:tiebreak_id) { should eq feed_item.id }
|
53
|
+
its(:to_s) { should eq "#{feed_item.a_datetime.to_i}:#{feed_item.id}" }
|
54
|
+
its(:criteria) {
|
55
|
+
should eq({ "$or" => [
|
56
|
+
{ "a_datetime" => { "$gt" => feed_item.a_datetime }},
|
57
|
+
{ "a_datetime" => feed_item.a_datetime, :_id => { "$gt" => feed_item.id }}
|
58
|
+
]})
|
59
|
+
}
|
60
|
+
end
|
61
|
+
context "a date field cursor" do
|
62
|
+
let(:feed_item) { Feed::Item.create!(a_date: Date.new(2013, 12, 21)) }
|
63
|
+
subject do
|
64
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_date.to_datetime.to_i}:#{feed_item.id}", field: Feed::Item.fields["a_date"]
|
65
|
+
end
|
66
|
+
its(:value) { should eq feed_item.a_date }
|
67
|
+
its(:tiebreak_id) { should eq feed_item.id }
|
68
|
+
its(:to_s) { should eq "#{feed_item.a_date.to_datetime.to_i}:#{feed_item.id}" }
|
69
|
+
its(:criteria) {
|
70
|
+
should eq({ "$or" => [
|
71
|
+
{ "a_date" => { "$gt" => feed_item.a_date.to_datetime }},
|
72
|
+
{ "a_date" => feed_item.a_date.to_datetime, :_id => { "$gt" => feed_item.id }}
|
73
|
+
]})
|
74
|
+
}
|
75
|
+
end
|
76
|
+
context "a time field cursor" do
|
77
|
+
let(:feed_item) { Feed::Item.create!(a_time: Time.new(2013, 12, 21, 1, 2, 3)) }
|
78
|
+
subject do
|
79
|
+
Mongoid::Scroll::Cursor.new "#{feed_item.a_time.to_i}:#{feed_item.id}", field: Feed::Item.fields["a_time"]
|
80
|
+
end
|
81
|
+
its(:value) { should eq feed_item.a_time }
|
82
|
+
its(:tiebreak_id) { should eq feed_item.id }
|
83
|
+
its(:to_s) { should eq "#{feed_item.a_time.to_i}:#{feed_item.id}" }
|
84
|
+
its(:criteria) {
|
85
|
+
should eq({ "$or" => [
|
86
|
+
{ "a_time" => { "$gt" => feed_item.a_time }},
|
87
|
+
{ "a_time" => feed_item.a_time, :_id => { "$gt" => feed_item.id }}
|
88
|
+
]})
|
89
|
+
}
|
90
|
+
end
|
91
|
+
context "an array field cursor" do
|
92
|
+
let(:feed_item) { Feed::Item.create!(a_array: [ "x", "y" ]) }
|
93
|
+
it "is not supported" do
|
94
|
+
expect {
|
95
|
+
Mongoid::Scroll::Cursor.from_record(feed_item, field: Feed::Item.fields["a_array"])
|
96
|
+
}.to raise_error Mongoid::Scroll::Errors::UnsupportedFieldTypeError, /The type of the field 'a_array' is not supported: Array./
|
97
|
+
end
|
98
|
+
end
|
99
|
+
context "an invalid field cursor" do
|
100
|
+
it "raises ArgumentError" do
|
101
|
+
expect {
|
102
|
+
Mongoid::Scroll::Cursor.new "invalid:whatever", {}
|
103
|
+
}.to raise_error ArgumentError
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'rspec'
|
6
|
+
require 'mongoid-scroll'
|
7
|
+
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each do |f|
|
9
|
+
require f
|
10
|
+
end
|
11
|
+
|
12
|
+
Mongoid.configure do |config|
|
13
|
+
config.connect_to 'mongoid_scroll_test'
|
14
|
+
end
|
15
|
+
|
16
|
+
RSpec.configure do |config|
|
17
|
+
config.before :each do
|
18
|
+
Mongoid.purge!
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Feed
|
2
|
+
class Item
|
3
|
+
include Mongoid::Document
|
4
|
+
|
5
|
+
field :a_field
|
6
|
+
field :a_integer, type: Integer
|
7
|
+
field :a_string, type: String
|
8
|
+
field :a_datetime, type: DateTime
|
9
|
+
field :a_date, type: Date
|
10
|
+
field :a_time, type: Time
|
11
|
+
field :a_array, type: Array
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mongoid-scroll
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Daniel Doubrovkine
|
9
|
+
- Frank Macreery
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-02-14 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mongoid
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '3.0'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '3.0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: i18n
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :runtime
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
description:
|
48
|
+
email: dblock@dblock.org
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- .gitignore
|
54
|
+
- .rspec
|
55
|
+
- .travis.yml
|
56
|
+
- CHANGELOG.md
|
57
|
+
- Gemfile
|
58
|
+
- LICENSE.md
|
59
|
+
- README.md
|
60
|
+
- Rakefile
|
61
|
+
- lib/config/locales/en.yml
|
62
|
+
- lib/mongoid-scroll.rb
|
63
|
+
- lib/mongoid/criterion/scrollable.rb
|
64
|
+
- lib/mongoid/scroll/cursor.rb
|
65
|
+
- lib/mongoid/scroll/errors.rb
|
66
|
+
- lib/mongoid/scroll/errors/base.rb
|
67
|
+
- lib/mongoid/scroll/errors/invalid_cursor_error.rb
|
68
|
+
- lib/mongoid/scroll/errors/multiple_sort_fields_error.rb
|
69
|
+
- lib/mongoid/scroll/errors/no_such_field_error.rb
|
70
|
+
- lib/mongoid/scroll/errors/unsupported_field_type_error.rb
|
71
|
+
- lib/mongoid/scroll/version.rb
|
72
|
+
- lib/mongoid_scroll.rb
|
73
|
+
- mongoid-scroll.gemspec
|
74
|
+
- spec/mongoid/criteria_spec.rb
|
75
|
+
- spec/mongoid/scroll_cursor_spec.rb
|
76
|
+
- spec/mongoid/scroll_spec.rb
|
77
|
+
- spec/spec_helper.rb
|
78
|
+
- spec/support/feed/item.rb
|
79
|
+
homepage: http://github.com/dblock/mongoid-scroll
|
80
|
+
licenses:
|
81
|
+
- MIT
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
hash: 1370092332583330753
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: 1.3.6
|
101
|
+
requirements: []
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 1.8.25
|
104
|
+
signing_key:
|
105
|
+
specification_version: 3
|
106
|
+
summary: Mongoid extensions to enable infinite scroll.
|
107
|
+
test_files: []
|