mongoid-scroll 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 [![Build Status](https://travis-ci.org/dblock/mongoid-scroll.png?branch=master)](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: []
|