mongoid-collection-snapshot 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fdda543f7183d461cf4b7068b97c7c9bba6523b9
4
+ data.tar.gz: a83c53f21498ad03d49089382dd9e519a9451d79
5
+ SHA512:
6
+ metadata.gz: de91fdde1b8c8b6457cdcecb4e91eec67f4eb3fddf0f96702092ef3a8df9e4747401571d1475478e1f82e28b33133287df422fce178ac245c238c6474de2be93
7
+ data.tar.gz: c9682df34fa35b1e444f6f10ccbf28a15517ca141c397d689493aca9de664c0c524cebb11c1a58f7a3abafd15fca855ac233fa3c1d538e408c3fc494392558c4
@@ -0,0 +1,54 @@
1
+ # rcov generated
2
+ coverage
3
+
4
+ # rdoc generated
5
+ rdoc
6
+
7
+ # yard generated
8
+ doc
9
+ .yardoc
10
+
11
+ # bundler
12
+ .bundle
13
+
14
+ # bundler
15
+ pkg
16
+
17
+ # Gemfile lock
18
+ Gemfile.lock
19
+
20
+ # rvm
21
+ .rvmrc
22
+
23
+ # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
24
+ #
25
+ # * Create a file at ~/.gitignore
26
+ # * Include files you want ignored
27
+ # * Run: git config --global core.excludesfile ~/.gitignore
28
+ #
29
+ # After doing this, these files will be ignored in all your git projects,
30
+ # saving you from having to 'pollute' every project you touch with them
31
+ #
32
+ # Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
33
+ #
34
+ # For MacOS:
35
+ #
36
+ #.DS_Store
37
+
38
+ # For TextMate
39
+ *.tmproj
40
+ tmtags
41
+
42
+ # For emacs:
43
+ *~
44
+ \#*
45
+ .\#*
46
+
47
+ # For vim:
48
+ *.swp
49
+
50
+ # For redcar:
51
+ .redcar
52
+
53
+ # For rubinius:
54
+ *.rbc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=documentation
@@ -0,0 +1 @@
1
+ inherit_from: .rubocop_todo.yml
@@ -0,0 +1,72 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2017-01-20 11:17:06 -0500 using RuboCop version 0.47.1.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 4
10
+ # Configuration parameters: Include.
11
+ # Include: **/Gemfile, **/gems.rb
12
+ Bundler/DuplicatedGem:
13
+ Exclude:
14
+ - 'Gemfile'
15
+
16
+ # Offense count: 1
17
+ Lint/IneffectiveAccessModifier:
18
+ Exclude:
19
+ - 'spec/models/custom_connection_snapshot.rb'
20
+
21
+ # Offense count: 1
22
+ # Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
23
+ Lint/UselessAccessModifier:
24
+ Exclude:
25
+ - 'spec/models/custom_connection_snapshot.rb'
26
+
27
+ # Offense count: 3
28
+ Metrics/AbcSize:
29
+ Max: 34
30
+
31
+ # Offense count: 5
32
+ # Configuration parameters: CountComments, ExcludedMethods.
33
+ Metrics/BlockLength:
34
+ Max: 145
35
+
36
+ # Offense count: 1
37
+ Metrics/CyclomaticComplexity:
38
+ Max: 8
39
+
40
+ # Offense count: 42
41
+ # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
42
+ # URISchemes: http, https
43
+ Metrics/LineLength:
44
+ Max: 192
45
+
46
+ # Offense count: 3
47
+ # Configuration parameters: CountComments.
48
+ Metrics/MethodLength:
49
+ Max: 31
50
+
51
+ # Offense count: 1
52
+ # Configuration parameters: CountComments.
53
+ Metrics/ModuleLength:
54
+ Max: 147
55
+
56
+ # Offense count: 1
57
+ Metrics/PerceivedComplexity:
58
+ Max: 10
59
+
60
+ # Offense count: 2
61
+ Style/Documentation:
62
+ Exclude:
63
+ - 'spec/**/*'
64
+ - 'test/**/*'
65
+ - 'lib/mongoid-collection-snapshot.rb'
66
+
67
+ # Offense count: 1
68
+ # Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms.
69
+ # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS
70
+ Style/FileName:
71
+ Exclude:
72
+ - 'lib/mongoid-collection-snapshot.rb'
@@ -0,0 +1,27 @@
1
+ sudo: false
2
+
3
+ matrix:
4
+ include:
5
+ - rvm: 2.3.1
6
+ env:
7
+ - MONGOID_VERSION=6
8
+ before_script:
9
+ - bundle exec danger
10
+ - rvm: 2.3.1
11
+ env:
12
+ - MONGOID_VERSION=5
13
+ - rvm: 2.2
14
+ env:
15
+ - MONGOID_VERSION=4
16
+ - rvm: 2.1
17
+ env:
18
+ - MONGOID_VERSION=3
19
+
20
+ services: mongodb
21
+
22
+ addons:
23
+ apt:
24
+ sources:
25
+ - mongodb-3.2-precise
26
+ packages:
27
+ - mongodb-org-server
@@ -0,0 +1,35 @@
1
+ ### 1.3.0 (1/20/2017)
2
+
3
+ * [#2](https://github.com/mongoid/mongoid-collection-snapshot/pull/2): Support for Mongoid 6 - [@dblock](https://github.com/dblock).
4
+ * [#1](https://github.com/mongoid/mongoid-collection-snapshot/pull/1): Gem renamed to mongoid-collection-snapshot and forked to the [mongoid org](https://github.com/mongoid) - [@dblock](https://github.com/dblock).
5
+ * [#18](https://github.com/aaw/mongoid_collection_snapshot/pull/18): Fix: classes that include `Mongoid::CollectionSnapshot` can be inherited - [@dblock](https://github.com/dblock).
6
+ * [#3](https://github.com/mongoid/mongoid-collection-snapshot/pull/3): Added Danger, PR linter - [@dblock](https://github.com/dblock).
7
+ * [#4](https://github.com/mongoid/mongoid-collection-snapshot/pull/4): Added RuboCop - [@dblock](https://github.com/dblock).
8
+
9
+ ### 1.2.0
10
+
11
+ * [#14](https://github.com/aaw/mongoid_collection_snapshot/pull/14): Compatibility with Mongoid 5 - [@dblock](https://github.com/dblock).
12
+
13
+ ### 1.1.0
14
+
15
+ * [#11](https://github.com/aaw/mongoid_collection_snapshot/pull/10): Added support for accessing snapshot collection documents via Mongoid::CollectionSnapshot#documents - [@dblock](https://github.com/dblock).
16
+ * [#11](https://github.com/aaw/mongoid_collection_snapshot/pull/11): Upgraded RSpec - [@dblock](https://github.com/dblock).
17
+
18
+ ### 1.0.1
19
+
20
+ * [#8](https://github.com/aaw/mongoid_collection_snapshot/pull/8): Fixed .gemspec for compatibility with Mongoid 4.x - [@dblock](https://github.com/dblock).
21
+
22
+ ### 1.0.0
23
+
24
+ * Expose `snapshot_session` for custom snapshot storage - [@joeyAghion](https://github.com/joeyAghion).
25
+ * Compatibility with Mongoid 4.x - [@dblock](https://github.com/dblock).
26
+
27
+ ### 0.2.0
28
+
29
+ * Added ability to maintain a snapshot of multiple collections atomically - [@aaw](https://github.com/aaw).
30
+ * Added support for [Mongoid 3.0](https://github.com/mongoid/mongoid) - [@dblock](https://github.com/dblock).
31
+ * Relaxed version limitations of [mongoid_slug](https://github.com/digitalplaywright/mongoid-slug) - [@dblock](https://github.com/dblock).
32
+
33
+ ### 0.1.0
34
+
35
+ * Initial public release - [@aaw](https://github.com/aaw).
@@ -0,0 +1 @@
1
+ danger.import_dangerfile(gem: "mongoid-danger")
data/Gemfile ADDED
@@ -0,0 +1,24 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ case version = ENV['MONGOID_VERSION'] || '6.0'
6
+ when /^6/
7
+ gem 'mongoid', '~> 6.0'
8
+ when /^5/
9
+ gem 'mongoid', '~> 5.0'
10
+ when /^4/
11
+ gem 'mongoid', '~> 4.0'
12
+ when /^3/
13
+ gem 'mongoid', '~> 3.1'
14
+ else
15
+ gem 'mongoid', version
16
+ end
17
+
18
+ group :development, :test do
19
+ gem 'mongoid-danger', '~> 0.1.1'
20
+ gem 'rake'
21
+ gem 'rspec', '~> 3.1'
22
+ gem 'rubocop', '0.47.1'
23
+ gem 'timecop'
24
+ end
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011-2017 Art.sy Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,223 @@
1
+ Mongoid Collection Snapshot
2
+ ===========================
3
+
4
+ Easy maintenance of collections of processed data in MongoDB with Mongoid 3, 4, 5 and 6.
5
+
6
+ This is a forked, renamed, maintained and supported version of [mongoid_collection_snapshot](https://github.com/aaw/mongoid_collection_snapshot).
7
+
8
+ [![Gem Version](https://badge.fury.io/rb/mongoid-collection-snapshot.svg)](https://badge.fury.io/rb/mongoid-collection-snapshot)
9
+ [![Build Status](https://travis-ci.org/mongoid/mongoid-collection-snapshot.svg)](https://travis-ci.org/mongoid/mongoid-collection-snapshot)
10
+
11
+ Quick example:
12
+ --------------
13
+
14
+ Suppose that you have a Mongoid model called `Artwork`, stored in a MongoDB collection called `artworks` and the underlying documents look something like:
15
+
16
+ { name: 'Flowers', artist: 'Andy Warhol', price: 3000000 }
17
+
18
+ From time to time, your system runs a map/reduce job to compute the average price of each artist's works, resulting in a collection called `artist_average_price` that contains documents that look like:
19
+
20
+ { _id: { artist: 'Andy Warhol' }, value: { price: 1500000 } }
21
+
22
+ If your system wants to maintain and use this average price data, it has to do so at the level of raw MongoDB operations, since map/reduce result documents don't map well to models in Mongoid.
23
+ Furthermore, even though map/reduce jobs can take some time to run, you probably want the entire `artist_average_price` collection populated atomically from the point of view of your system, since otherwise you don't ever know the state of the data in the collection - you could access it in the middle of a map/reduce and get partial, incorrect results.
24
+
25
+ A mongoid-collection-snapshot solves this problem by providing an atomic view of collections of data like map/reduce results that live outside of Mongoid.
26
+
27
+ In the example above, we'd set up our average artist price collection like:
28
+
29
+ ``` ruby
30
+ class AverageArtistPrice
31
+ include Mongoid::CollectionSnapshot
32
+
33
+ def build
34
+
35
+ map = <<-EOS
36
+ function() {
37
+ emit({ artist_id: this['artist_id']}, { count: 1, sum: this['price'] })
38
+ }
39
+ EOS
40
+
41
+ reduce = <<-EOS
42
+ function(key, values) {
43
+ var sum = 0;
44
+ var count = 0;
45
+ values.forEach(function(value) {
46
+ sum += value['sum'];
47
+ count += value['count'];
48
+ });
49
+ return({ count: count, sum: sum });
50
+ }
51
+ EOS
52
+
53
+ Artwork.map_reduce(map, reduce).out(inline: 1).each do |doc|
54
+ collection_snapshot.insert_one(
55
+ artist_id: doc['_id']['artist_id'],
56
+ count: doc['value']['count'],
57
+ sum: doc['value']['sum']
58
+ )
59
+ end
60
+ end
61
+ end
62
+
63
+ ```
64
+
65
+ Now, if you want to schedule a recomputation, just call `AverageArtistPrice.create`. You can define other methods on collection snapshots.
66
+
67
+ ```ruby
68
+ class AverageArtistPrice
69
+ ...
70
+
71
+ def average_price(artist_name)
72
+ artist = Artist.where(name: artist_name).first
73
+ doc = collection_snapshot.where(artist_id: artist.id).first
74
+ doc['sum'] / doc['count']
75
+ end
76
+ end
77
+ ```
78
+
79
+ The latest snapshot is always available as `AverageArtistPrice.latest`, so you can write code like:
80
+
81
+ ```ruby
82
+ warhol_expected_price = AverageArtistPrice.latest.average_price('Andy Warhol')
83
+ ```
84
+
85
+ And always be sure that you'll never be looking at partial results. The only thing you need to do to hook into mongoid-collection-snapshot is implement the method `build`, which populates the collection snapshot and any indexes you need.
86
+
87
+ By default, mongoid-collection-snapshot maintains the most recent two snapshots computed any given time.
88
+
89
+ ery Snapshot Data with Mongoid
90
+ --------------------------------
91
+
92
+ You can do better than the average price example above and define first-class models for your collection snapshot data, then access them as any other Mongoid collection via collection snapshot's `.documents` method.
93
+
94
+ ```ruby
95
+ class AverageArtistPrice
96
+ document do
97
+ belongs_to :artist, inverse_of: nil
98
+ field :sum, type: Integer
99
+ field :count, type: Integer
100
+ end
101
+
102
+ def average_price(artist_name)
103
+ artist = Artist.where(name: artist_name).first
104
+ doc = documents.where(artist: artist).first
105
+ doc.sum / doc.count
106
+ end
107
+ end
108
+ ```
109
+
110
+ Another example iterates through all latest artist price averages.
111
+
112
+ ```ruby
113
+ AverageArtistPrice.latest.documents.each do |doc|
114
+ puts "#{doc.artist.name}: #{doc.sum / doc.count}"
115
+ end
116
+ ```
117
+
118
+ Multi-collection snapshots
119
+ --------------------------
120
+
121
+ You can maintain multiple collections atomically within the same snapshot by passing unique collection identifiers to `collection_snaphot` when you call it in your build or query methods:
122
+
123
+ ``` ruby
124
+ class ArtistStats
125
+ include Mongoid::CollectionSnapshot
126
+
127
+ def build
128
+ # ...
129
+ # define map/reduce for average and max aggregations
130
+ # ...
131
+ Mongoid.default_session.command('mapreduce' => 'artworks', map: map_avg, reduce: reduce_avg, out: collection_snapshot('average'))
132
+ Mongoid.default_session.command('mapreduce' => 'artworks', map: map_max, reduce: reduce_max, out: collection_snapshot('max'))
133
+ end
134
+
135
+ def average_price(artist)
136
+ doc = collection_snapshot('average').find('_id.artist' => artist).first
137
+ doc['value']['sum'] / doc['value']['count']
138
+ end
139
+
140
+ def max_price(artist)
141
+ doc = collection_snapshot('max').find('_id.artist' => artist).first
142
+ doc['value']['max']
143
+ end
144
+ end
145
+ ```
146
+
147
+ Specify the name of the collection to define first class Mongoid models.
148
+
149
+ ```ruby
150
+ class ArtistStats
151
+ document('average') do
152
+ field :value, type: Hash
153
+ end
154
+
155
+ document('max') do
156
+ field :value, type: Hash
157
+ end
158
+ end
159
+ ```
160
+
161
+ Access these by name.
162
+
163
+ ```ruby
164
+ ArtistStats.latest.documents('average')
165
+ ArtistStats.latest.documents('max')
166
+ ```
167
+
168
+ If fields across multiple collection snapshots are identical, a single default `document` is sufficient.
169
+
170
+ ```ruby
171
+ class ArtistStats
172
+ document do
173
+ field :value, type: Hash
174
+ end
175
+ end
176
+ ```
177
+
178
+ Custom database connections
179
+ ---------------------------
180
+
181
+ Your class can specify a custom database for storage of collection snapshots by overriding the `snapshot_session` instance method. In this example, we memoize the connection at the class level to avoid creating many separate connection instances.
182
+
183
+ ```ruby
184
+ class ArtistStats
185
+ include Mongoid::CollectionSnapshot
186
+
187
+ def build
188
+ # ...
189
+ end
190
+
191
+ def snapshot_session
192
+ self.class.snapshot_session
193
+ end
194
+
195
+ def self.snapshot_session
196
+ @@snapshot_session ||= Mongo::Client.new('mongodb://localhost:27017').tap do |c|
197
+ c.use :alternate_db
198
+ end
199
+ end
200
+ end
201
+ ```
202
+
203
+ Another common way of configuring this is through mongoid.yml.
204
+
205
+ ```yaml
206
+ development:
207
+ sessions:
208
+ default:
209
+ database: dev_data
210
+ imports:
211
+ database: dev_imports
212
+ ```
213
+
214
+ ```ruby
215
+ def snapshot_session
216
+ Mongoid.session('imports')
217
+ end
218
+ ```
219
+
220
+ License
221
+ =======
222
+
223
+ MIT License, see [LICENSE.txt](LICENSE.txt) for details.
@@ -0,0 +1,27 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ require 'bundler'
6
+ require 'bundler/gem_tasks'
7
+
8
+ begin
9
+ Bundler.setup(:default, :development)
10
+ rescue Bundler::BundlerError => e
11
+ $stderr.puts e.message
12
+ $stderr.puts 'Run `bundle install` to install missing gems'
13
+ exit e.status_code
14
+ end
15
+
16
+ require 'rake'
17
+
18
+ require 'rspec/core'
19
+ require 'rspec/core/rake_task'
20
+ RSpec::Core::RakeTask.new(:spec) do |spec|
21
+ spec.pattern = FileList['spec/**/*_spec.rb']
22
+ end
23
+
24
+ require 'rubocop/rake_task'
25
+ RuboCop::RakeTask.new(:rubocop)
26
+
27
+ task default: [:rubocop, :spec]
@@ -0,0 +1,8 @@
1
+ ### Upgrading from 0.1.0 to >= 0.2.0
2
+
3
+ When upgrading from 0.1.0 (pre-Mongoid 3.0) to 0.2.0 (Mongoid 3.x), you'll need to upgrade any existing snapshots created by mongoid_collection_snapshot 0.1.0 in your database before they're usable. You can do this by renaming the 'workspace_slug' attribute to 'slug' in MongoDB after upgrading. For example, to upgrade the snapshot class "MySnapshot", you'd enter the following at the mongo shell.
4
+
5
+ ```
6
+ db.my_snapshot.rename({ 'workspace_slug' : { '$exists' : true } }, {'$rename' : {'workspace_slug' : 'slug' } })
7
+ ```
8
+
@@ -0,0 +1,100 @@
1
+ require 'mongoid-collection-snapshot/version'
2
+
3
+ module Mongoid
4
+ module CollectionSnapshot
5
+ extend ActiveSupport::Concern
6
+
7
+ DEFAULT_COLLECTION_KEY_NAME = '*'.freeze
8
+
9
+ included do
10
+ require 'mongoid_slug'
11
+
12
+ include Mongoid::Document
13
+ include Mongoid::Timestamps::Created
14
+ include Mongoid::Slug
15
+
16
+ field :workspace_basename, default: 'snapshot'
17
+ slug :workspace_basename
18
+
19
+ field :max_collection_snapshot_instances, default: 2
20
+
21
+ before_create :build
22
+ after_create :ensure_at_most_two_instances_exist
23
+ before_destroy :drop_snapshot_collections
24
+
25
+ class_attribute :document_blocks
26
+ class_attribute :document_classes
27
+
28
+ # Mongoid documents on this snapshot.
29
+ def documents(name = nil)
30
+ self.class.document_classes ||= {}
31
+ class_name = "#{self.class.name}#{id}#{name}".underscore.camelize
32
+ key = "#{class_name}-#{name || DEFAULT_COLLECTION_KEY_NAME}"
33
+ self.class.document_classes[key] ||= begin
34
+ document_block = document_blocks[name || DEFAULT_COLLECTION_KEY_NAME] if document_blocks
35
+ collection_name = collection_snapshot(name).name
36
+ klass = Class.new do
37
+ include Mongoid::Document
38
+ if Mongoid::Compatibility::Version.mongoid5?
39
+ cattr_accessor :mongo_client
40
+ else
41
+ cattr_accessor :mongo_session
42
+ end
43
+ instance_eval(&document_block) if document_block
44
+ store_in collection: collection_name
45
+ end
46
+ if Mongoid::Compatibility::Version.mongoid6?
47
+ PersistenceContext.set(klass, database: snapshot_session.database.name)
48
+ elsif Mongoid::Compatibility::Version.mongoid5?
49
+ klass.mongo_client = snapshot_session
50
+ else
51
+ klass.mongo_session = snapshot_session
52
+ end
53
+ Object.const_set(class_name, klass)
54
+ klass
55
+ end
56
+ end
57
+ end
58
+
59
+ module ClassMethods
60
+ def latest
61
+ order_by([[:created_at, :desc]]).first
62
+ end
63
+
64
+ def document(name = nil, &block)
65
+ self.document_blocks ||= {}
66
+ self.document_blocks[name || DEFAULT_COLLECTION_KEY_NAME] = block
67
+ end
68
+ end
69
+
70
+ def collection_snapshot(name = nil)
71
+ if name
72
+ snapshot_session["#{collection.name}.#{name}.#{slug}"]
73
+ else
74
+ snapshot_session["#{collection.name}.#{slug}"]
75
+ end
76
+ end
77
+
78
+ def drop_snapshot_collections
79
+ collections = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? snapshot_session.database.collections : snapshot_session.collections
80
+ collections.each do |collection|
81
+ collection.drop if collection.name =~ /^#{self.collection.name}\.([^\.]+\.)?#{slug}$/
82
+ end
83
+ end
84
+
85
+ # Since we should always be using the latest instance of this class, this method is
86
+ # called after each save - making sure only at most two instances exists should be
87
+ # sufficient to ensure that this data can be rebuilt live without corrupting any
88
+ # existing computations that might have a handle to the previous "latest" instance.
89
+ def ensure_at_most_two_instances_exist
90
+ all_instances = self.class.order_by([[:created_at, :desc]]).to_a
91
+ return unless all_instances.length > max_collection_snapshot_instances
92
+ all_instances[max_collection_snapshot_instances..-1].each(&:destroy)
93
+ end
94
+
95
+ # Override to supply custom database connection for snapshots
96
+ def snapshot_session
97
+ Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client : Mongoid.default_session
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,5 @@
1
+ module Mongoid
2
+ module CollectionSnapshot
3
+ VERSION = '1.3.0'.freeze
4
+ end
5
+ end
@@ -0,0 +1,19 @@
1
+ $LOAD_PATH.push File.expand_path('../lib', __FILE__)
2
+ require 'mongoid-collection-snapshot/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'mongoid-collection-snapshot'
6
+ s.version = Mongoid::CollectionSnapshot::VERSION
7
+ s.authors = ['Aaron Windsor']
8
+ s.email = 'aaron.windsor@gmail.com'
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/mongoid/mongoid-collection-snapshot'
14
+ s.licenses = ['MIT']
15
+ s.summary = 'Easy maintenence of collections of processed data in MongoDB with the Mongoid ODM.'
16
+ s.add_dependency 'mongoid', '>= 3.0'
17
+ s.add_dependency 'mongoid-compatibility'
18
+ s.add_dependency 'mongoid-slug'
19
+ end
@@ -0,0 +1,7 @@
1
+ class Artist
2
+ include Mongoid::Document
3
+
4
+ field :name
5
+
6
+ has_many :artworks
7
+ end
@@ -0,0 +1,9 @@
1
+ class Artwork
2
+ include Mongoid::Document
3
+
4
+ field :name
5
+ field :price
6
+
7
+ belongs_to :artist
8
+ belongs_to :partner
9
+ end
@@ -0,0 +1,107 @@
1
+ class AveragePrice
2
+ include Mongoid::CollectionSnapshot
3
+ end
4
+
5
+ class AverageArtistPrice < AveragePrice
6
+ document do
7
+ belongs_to :artist, inverse_of: nil
8
+ field :sum, type: Integer
9
+ field :count, type: Integer
10
+ end
11
+
12
+ def build
13
+ map = <<-EOS
14
+ function() {
15
+ emit({ artist_id: this['artist_id']}, { count: 1, sum: this['price'] })
16
+ }
17
+ EOS
18
+
19
+ reduce = <<-EOS
20
+ function(key, values) {
21
+ var sum = 0;
22
+ var count = 0;
23
+ values.forEach(function(value) {
24
+ sum += value['sum'];
25
+ count += value['count'];
26
+ });
27
+ return({ count: count, sum: sum });
28
+ }
29
+ EOS
30
+
31
+ Artwork.map_reduce(map, reduce).out(inline: 1).each do |doc|
32
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
33
+ collection_snapshot.insert_one(
34
+ artist_id: doc['_id']['artist_id'],
35
+ count: doc['value']['count'],
36
+ sum: doc['value']['sum']
37
+ )
38
+ else
39
+ collection_snapshot.insert(
40
+ artist_id: doc['_id']['artist_id'],
41
+ count: doc['value']['count'],
42
+ sum: doc['value']['sum']
43
+ )
44
+ end
45
+ end
46
+ end
47
+
48
+ def average_price(artist_name)
49
+ artist = Artist.where(name: artist_name).first
50
+ raise 'missing artist' unless artist
51
+ doc = documents.where(artist: artist).first
52
+ raise 'missing record' unless doc
53
+ doc.sum / doc.count
54
+ end
55
+ end
56
+
57
+ class AveragePartnerPrice < AveragePrice
58
+ document do
59
+ belongs_to :partner, inverse_of: nil
60
+ field :sum, type: Integer
61
+ field :count, type: Integer
62
+ end
63
+
64
+ def build
65
+ map = <<-EOS
66
+ function() {
67
+ emit({ partner_id: this['partner_id']}, { count: 1, sum: this['price'] })
68
+ }
69
+ EOS
70
+
71
+ reduce = <<-EOS
72
+ function(key, values) {
73
+ var sum = 0;
74
+ var count = 0;
75
+ values.forEach(function(value) {
76
+ sum += value['sum'];
77
+ count += value['count'];
78
+ });
79
+ return({ count: count, sum: sum });
80
+ }
81
+ EOS
82
+
83
+ Artwork.map_reduce(map, reduce).out(inline: 1).each do |doc|
84
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
85
+ collection_snapshot.insert_one(
86
+ partner_id: doc['_id']['partner_id'],
87
+ count: doc['value']['count'],
88
+ sum: doc['value']['sum']
89
+ )
90
+ else
91
+ collection_snapshot.insert(
92
+ partner_id: doc['_id']['partner_id'],
93
+ count: doc['value']['count'],
94
+ sum: doc['value']['sum']
95
+ )
96
+ end
97
+ end
98
+ end
99
+
100
+ def average_price(partner_name)
101
+ partner = Partner.where(name: partner_name).first
102
+ raise 'missing partner' unless partner
103
+ doc = documents.where(partner: partner).first
104
+ raise 'missing record' unless doc
105
+ doc.sum / doc.count
106
+ end
107
+ end
@@ -0,0 +1,33 @@
1
+ class CustomConnectionSnapshot
2
+ include Mongoid::CollectionSnapshot
3
+
4
+ def self.snapshot_session
5
+ @snapshot_session ||= new_snapshot_session
6
+ end
7
+
8
+ def snapshot_session
9
+ self.class.snapshot_session
10
+ end
11
+
12
+ def build
13
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
14
+ collection_snapshot.insert_one('name' => 'foo')
15
+ collection_snapshot('foo').insert_one('name' => 'bar')
16
+ else
17
+ collection_snapshot.insert('name' => 'foo')
18
+ collection_snapshot('foo').insert('name' => 'bar')
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def self.new_snapshot_session
25
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
26
+ Mongo::Client.new('mongodb://localhost:27017/snapshot_test')
27
+ else
28
+ Moped::Session.new(['127.0.0.1:27017']).tap do |session|
29
+ session.use :snapshot_test
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ class MultiCollectionSnapshot
2
+ include Mongoid::CollectionSnapshot
3
+
4
+ document('foo') do
5
+ field :name, type: String
6
+ field :count, type: Integer
7
+ end
8
+
9
+ document('bar') do
10
+ field :name, type: String
11
+ field :number, type: Integer
12
+ end
13
+
14
+ document('baz') do
15
+ field :name, type: String
16
+ field :digit, type: Integer
17
+ end
18
+
19
+ def build
20
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
21
+ collection_snapshot('foo').insert_one('name' => 'foo!', count: 1)
22
+ collection_snapshot('bar').insert_one('name' => 'bar!', number: 2)
23
+ collection_snapshot('baz').insert_one('name' => 'baz!', digit: 3)
24
+ else
25
+ collection_snapshot('foo').insert('name' => 'foo!', count: 1)
26
+ collection_snapshot('bar').insert('name' => 'bar!', number: 2)
27
+ collection_snapshot('baz').insert('name' => 'baz!', digit: 3)
28
+ end
29
+ end
30
+
31
+ def names
32
+ %w(foo bar baz).map { |x| collection_snapshot(x).find.first['name'] }.join('')
33
+ end
34
+ end
@@ -0,0 +1,7 @@
1
+ class Partner
2
+ include Mongoid::Document
3
+
4
+ field :name
5
+
6
+ has_many :artworks
7
+ end
@@ -0,0 +1,173 @@
1
+ require 'spec_helper'
2
+
3
+ module Mongoid
4
+ describe CollectionSnapshot do
5
+ it 'has a version' do
6
+ expect(Mongoid::CollectionSnapshot::VERSION).not_to be_nil
7
+ end
8
+
9
+ context 'creating a basic snapshot' do
10
+ let!(:gagosian) { Partner.create!(name: 'Gagosian') }
11
+ let!(:pace) { Partner.create!(name: 'Pace') }
12
+ let!(:andy_warhol) { Artist.create!(name: 'Andy Warhol') }
13
+ let!(:damien_hirst) { Artist.create!(name: 'Damien Hirst') }
14
+ let!(:flowers) { Artwork.create!(name: 'Flowers', partner: gagosian, artist: andy_warhol, price: 3_000_000) }
15
+ let!(:guns) { Artwork.create!(name: 'Guns', partner: pace, artist: andy_warhol, price: 1_000_000) }
16
+ let!(:vinblastine) { Artwork.create!(name: 'Vinblastine', partner: gagosian, artist: damien_hirst, price: 1_500_000) }
17
+
18
+ it 'returns nil if no snapshot has been created' do
19
+ expect(AverageArtistPrice.latest).to be_nil
20
+ expect(AveragePartnerPrice.latest).to be_nil
21
+ end
22
+
23
+ it 'runs the build method on creation for average artist price' do
24
+ snapshot = AverageArtistPrice.create
25
+ expect(snapshot.average_price('Andy Warhol')).to eq(2_000_000)
26
+ expect(snapshot.average_price('Damien Hirst')).to eq(1_500_000)
27
+ end
28
+
29
+ it 'runs the build method on creation for average partner price' do
30
+ snapshot = AveragePartnerPrice.create
31
+ expect(snapshot.average_price('Gagosian')).to eq(2_250_000)
32
+ expect(snapshot.average_price('Pace')).to eq(1_000_000)
33
+ end
34
+
35
+ it 'returns the most recent snapshot through the latest methods' do
36
+ first = AverageArtistPrice.create
37
+ expect(first).to eq(AverageArtistPrice.latest)
38
+ # "latest" only works up to a resolution of 1 second since it relies on Mongoid::Timestamp. But this
39
+ # module is meant to snapshot long-running collection creation, so if you need a resolution of less
40
+ # than a second for "latest" then you're probably using the wrong gem. In tests, sleeping for a second
41
+ # makes sure we get what we expect.
42
+ Timecop.travel(1.second.from_now)
43
+ second = AverageArtistPrice.create
44
+ expect(AverageArtistPrice.latest).to eq(second)
45
+ Timecop.travel(1.second.from_now)
46
+ third = AverageArtistPrice.create
47
+ expect(AverageArtistPrice.latest).to eq(third)
48
+ end
49
+
50
+ it 'maintains at most two of the latest snapshots to support its calculations' do
51
+ AverageArtistPrice.create
52
+ 10.times do
53
+ AverageArtistPrice.create
54
+ expect(AverageArtistPrice.count).to eq(2)
55
+ end
56
+ end
57
+
58
+ context '#documents' do
59
+ it 'provides access to a Mongoid collection' do
60
+ snapshot = AverageArtistPrice.create
61
+ expect(snapshot.documents.count).to eq 2
62
+ document = snapshot.documents.where(artist: andy_warhol).first
63
+ expect(document.artist).to eq andy_warhol
64
+ expect(document.count).to eq 2
65
+ expect(document.sum).to eq 4_000_000
66
+
67
+ snapshot = AveragePartnerPrice.create
68
+ expect(snapshot.documents.count).to eq 2
69
+ document = snapshot.documents.where(partner: pace).first
70
+ expect(document.partner).to eq pace
71
+ expect(document.count).to eq 1
72
+ expect(document.sum).to eq 1_000_000
73
+ end
74
+
75
+ it 'only creates one global class reference' do
76
+ 3.times do
77
+ index = AverageArtistPrice.create
78
+ 2.times { expect(index.documents.count).to eq 2 }
79
+ index = AveragePartnerPrice.create
80
+ 2.times { expect(index.documents.count).to eq 2 }
81
+ end
82
+ expect(AveragePrice.document_classes).to be nil
83
+ expect(AverageArtistPrice.document_classes.count).to be >= 3
84
+ expect(AveragePartnerPrice.document_classes.count).to be >= 3
85
+ end
86
+ end
87
+ end
88
+
89
+ context 'creating a snapshot containing multiple collections' do
90
+ it 'populates several collections and allows them to be queried' do
91
+ expect(MultiCollectionSnapshot.latest).to be_nil
92
+ 10.times { MultiCollectionSnapshot.create }
93
+ expect(MultiCollectionSnapshot.latest.names).to eq('foo!bar!baz!')
94
+ end
95
+
96
+ it 'safely cleans up all collections used by the snapshot' do
97
+ # Create some collections with names close to the snapshots we'll create
98
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
99
+ Mongoid.default_client["#{MultiCollectionSnapshot.collection.name}.do.not_delete"].insert_one('a' => 1)
100
+ Mongoid.default_client["#{MultiCollectionSnapshot.collection.name}.snapshorty"].insert_one('a' => 1)
101
+ Mongoid.default_client["#{MultiCollectionSnapshot.collection.name}.hello.1"].insert_one('a' => 1)
102
+ else
103
+ Mongoid.default_session["#{MultiCollectionSnapshot.collection.name}.do.not_delete"].insert('a' => 1)
104
+ Mongoid.default_session["#{MultiCollectionSnapshot.collection.name}.snapshorty"].insert('a' => 1)
105
+ Mongoid.default_session["#{MultiCollectionSnapshot.collection.name}.hello.1"].insert('a' => 1)
106
+ end
107
+
108
+ MultiCollectionSnapshot.create
109
+ collections = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client.database.collections : Mongoid.default_session.collections
110
+ before_create = collections.map(&:name)
111
+ expect(before_create.length).to be > 0
112
+
113
+ Timecop.travel(1.second.from_now)
114
+ MultiCollectionSnapshot.create
115
+ collections = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client.database.collections : Mongoid.default_session.collections
116
+ after_create = collections.map(&:name)
117
+ collections_created = (after_create - before_create).sort
118
+ expect(collections_created.length).to eq(3)
119
+
120
+ MultiCollectionSnapshot.latest.destroy
121
+ collections = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client.database.collections : Mongoid.default_session.collections
122
+ after_destroy = collections.map(&:name)
123
+ collections_destroyed = (after_create - after_destroy).sort
124
+ expect(collections_created).to eq(collections_destroyed)
125
+ end
126
+ end
127
+
128
+ context 'with a custom snapshot connection' do
129
+ around(:each) do |example|
130
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
131
+ CustomConnectionSnapshot.snapshot_session.database.drop
132
+ else
133
+ CustomConnectionSnapshot.snapshot_session.drop
134
+ end
135
+ example.run
136
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
137
+ CustomConnectionSnapshot.snapshot_session.database.drop
138
+ else
139
+ CustomConnectionSnapshot.snapshot_session.drop
140
+ end
141
+ end
142
+
143
+ it 'builds snapshot in custom database' do
144
+ snapshot = CustomConnectionSnapshot.create
145
+ [
146
+ "#{CustomConnectionSnapshot.collection.name}.foo.#{snapshot.slug}",
147
+ "#{CustomConnectionSnapshot.collection.name}.#{snapshot.slug}"
148
+ ].each do |collection_name|
149
+ session = Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6? ? Mongoid.default_client : Mongoid.default_session
150
+ expect(session[collection_name].find.count).to eq(0)
151
+ expect(CustomConnectionSnapshot.snapshot_session[collection_name].find.count).to eq(1)
152
+ end
153
+ end
154
+
155
+ context '#documents' do
156
+ it 'uses the custom session' do
157
+ if Mongoid::Compatibility::Version.mongoid6?
158
+ expect(CustomConnectionSnapshot.new.documents.database_name).to eq :snapshot_test
159
+ elsif Mongoid::Compatibility::Version.mongoid5?
160
+ expect(CustomConnectionSnapshot.new.documents.mongo_client).to eq CustomConnectionSnapshot.snapshot_session
161
+ else
162
+ expect(CustomConnectionSnapshot.new.documents.mongo_session).to eq CustomConnectionSnapshot.snapshot_session
163
+ end
164
+ end
165
+ it 'provides access to a Mongoid collection' do
166
+ snapshot = CustomConnectionSnapshot.create
167
+ expect(snapshot.collection_snapshot.find.count).to eq 1
168
+ expect(snapshot.documents.count).to eq 1
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,34 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'rspec'
4
+
5
+ require 'mongoid'
6
+ require 'timecop'
7
+
8
+ Mongoid.configure do |config|
9
+ config.connect_to('mongoid-collection-snapshot_test')
10
+ end
11
+
12
+ require File.expand_path('../../lib/mongoid-collection-snapshot', __FILE__)
13
+ Dir["#{File.dirname(__FILE__)}/models/**/*.rb"].each { |f| require f }
14
+
15
+ require 'mongoid-compatibility'
16
+
17
+ RSpec.configure do |c|
18
+ c.before(:all) do
19
+ Mongoid.logger.level = Logger::INFO
20
+ Mongo::Logger.logger.level = Logger::INFO if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
21
+ end
22
+ c.before(:each) do
23
+ Mongoid.purge!
24
+ end
25
+ c.after(:all) do
26
+ if Mongoid::Compatibility::Version.mongoid5? || Mongoid::Compatibility::Version.mongoid6?
27
+ Mongoid.default_client.database.drop
28
+ else
29
+ Mongoid.default_session.drop
30
+ end
31
+ end
32
+ end
33
+
34
+ RSpec.configure(&:raise_errors_for_deprecations!)
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid-collection-snapshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Windsor
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-01-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mongoid
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mongoid-compatibility
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: mongoid-slug
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description:
56
+ email: aaron.windsor@gmail.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - ".gitignore"
62
+ - ".rspec"
63
+ - ".rubocop.yml"
64
+ - ".rubocop_todo.yml"
65
+ - ".travis.yml"
66
+ - CHANGELOG.md
67
+ - Dangerfile
68
+ - Gemfile
69
+ - LICENSE.txt
70
+ - README.md
71
+ - Rakefile
72
+ - UPGRADING.md
73
+ - lib/mongoid-collection-snapshot.rb
74
+ - lib/mongoid-collection-snapshot/version.rb
75
+ - mongoid-collection-snapshot.gemspec
76
+ - spec/models/artist.rb
77
+ - spec/models/artwork.rb
78
+ - spec/models/average_price.rb
79
+ - spec/models/custom_connection_snapshot.rb
80
+ - spec/models/multi_collection_snapshot.rb
81
+ - spec/models/partner.rb
82
+ - spec/mongoid/collection_snapshot_spec.rb
83
+ - spec/spec_helper.rb
84
+ homepage: http://github.com/mongoid/mongoid-collection-snapshot
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 1.3.6
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.5.1
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Easy maintenence of collections of processed data in MongoDB with the Mongoid
108
+ ODM.
109
+ test_files: []