mongoid-cached-json 1.5.3 → 1.7.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/CONTRIBUTING.md CHANGED
@@ -1,16 +1,16 @@
1
1
  Contributing to Mongoid::CachedJson
2
2
  ===================================
3
3
 
4
- Mongoid::CachedJson is work of [many of contributors](https://github.com/dblock/mongoid-cached-json/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/dblock/mongoid-cached-json/pulls), [propose features and discuss issues](https://github.com/dblock/mongoid-cached-json/issues). When in doubt, ask a question in the [Google Group](http://groups.google.com/group/mongoid-cached-json).
4
+ Mongoid::CachedJson is work of [many of contributors](https://github.com/mongoid/mongoid-cached-json/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/mongoid/mongoid-cached-json/pulls), [propose features and discuss issues](https://github.com/mongoid/mongoid-cached-json/issues). When in doubt, ask a question in the [Google Group](http://groups.google.com/group/mongoid-cached-json).
5
5
 
6
6
  #### Fork the Project
7
7
 
8
- Fork the [project on Github](https://github.com/dblock/mongoid-cached-json) and check out your copy.
8
+ Fork the [project on Github](https://github.com/mongoid/mongoid-cached-json) and check out your copy.
9
9
 
10
10
  ```
11
11
  git clone https://github.com/contributor/mongoid-cached-json.git
12
12
  cd mongoid-cached-json
13
- git remote add upstream https://github.com/dblock/mongoid-cached-json.git
13
+ git remote add upstream https://github.com/mongoid/mongoid-cached-json.git
14
14
  ```
15
15
 
16
16
  #### Create a Topic Branch
@@ -63,6 +63,8 @@ Document any external behavior in the [README](README.md).
63
63
 
64
64
  Add a line to [CHANGELOG](CHANGELOG.md) under *Next Release*. Make it look like every other line, including your name and link to your Github account.
65
65
 
66
+ Danger checks pull requests with `danger-changelog` and reports missing changelog entries in a PR comment. Include the pull request number in your entry once it is available.
67
+
66
68
  #### Commit Changes
67
69
 
68
70
  Make sure git knows your name and email address:
@@ -104,7 +106,7 @@ git push origin my-feature-branch -f
104
106
  Update the [CHANGELOG](CHANGELOG.md) with the pull request number. A typical entry looks as follows.
105
107
 
106
108
  ```
107
- * [#123](https://github.com/dblock/mongoid-cached-json/pull/123): Reticulated splines - [@contributor](https://github.com/contributor).
109
+ * [#123](https://github.com/mongoid/mongoid-cached-json/pull/123): Reticulated splines - [@contributor](https://github.com/contributor).
108
110
  ```
109
111
 
110
112
  Amend your previous commit and force push the changes.
data/Dangerfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ danger.import_dangerfile(gem: 'danger-pr-comment')
4
+
5
+ changelog.check!
data/Gemfile CHANGED
@@ -1,27 +1,27 @@
1
- source 'http://rubygems.org'
1
+ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- case version = ENV['MONGOID_VERSION'] || '~> 5.0'
6
- when /5/
7
- gem 'mongoid', '~> 5.0'
8
- when /4/
9
- gem 'mongoid', '~> 4.0'
10
- when /3/
11
- gem 'mongoid', '~> 3.1'
12
- else
13
- gem 'mongoid', version
14
- end
5
+ version = ENV['MONGOID_VERSION'] || '9'
6
+ gem 'bigdecimal' if RUBY_VERSION >= '3.4'
7
+ gem 'logger' if RUBY_VERSION >= '3.4'
8
+ gem 'mongoid', version =~ /\A\d+\z/ ? "~> #{version}.0" : version
15
9
 
16
10
  group :development do
11
+ if RUBY_VERSION >= '2.7'
12
+ gem 'danger'
13
+ gem 'danger-changelog', require: false
14
+ gem 'danger-pr-comment', require: false
15
+ end
16
+ gem 'dalli', RUBY_VERSION >= '3.0' ? '~> 3.0' : '~> 2.6'
17
17
  gem 'rake'
18
- gem 'bundler', '~> 1.0'
18
+ gem 'rdoc', RUBY_VERSION >= '3.0' ? '>= 6' : '~> 4.2'
19
+ gem 'rubocop', ENV.fetch('RUBOCOP_VERSION', '>= 0')
19
20
  gem 'yard', '~> 0.6'
20
- gem 'dalli', '~> 2.6'
21
- gem 'rubocop', '0.33.0'
22
21
  end
23
22
 
24
23
  group :test do
25
24
  gem 'mongoid-compatibility'
26
25
  gem 'rspec', '~> 3.1'
26
+ gem 'simplecov', '~> 0.22' if ENV['COVERAGE'] == 'true'
27
27
  end
data/README.md CHANGED
@@ -2,9 +2,10 @@ Mongoid::CachedJson
2
2
  ===================
3
3
 
4
4
  [![Gem Version](https://badge.fury.io/rb/mongoid-cached-json.svg)](http://badge.fury.io/rb/mongoid-cached-json)
5
- [![Build Status](https://travis-ci.org/dblock/mongoid-cached-json.svg?branch=master)](https://travis-ci.org/dblock/mongoid-cached-json)
6
- [![Dependency Status](https://gemnasium.com/dblock/mongoid-cached-json.svg)](https://gemnasium.com/dblock/mongoid-cached-json)
7
- [![Code Climate](https://codeclimate.com/github/dblock/mongoid-cached-json.svg)](https://codeclimate.com/github/dblock/mongoid-cached-json)
5
+ [![Test](https://github.com/mongoid/mongoid-cached-json/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/mongoid/mongoid-cached-json/actions/workflows/test.yml?query=branch%3Amaster)
6
+ [![Coverage Status](https://coveralls.io/repos/github/mongoid/mongoid-cached-json/badge.svg?branch=master)](https://coveralls.io/github/mongoid/mongoid-cached-json?branch=master)
7
+
8
+ The Ruby 4.0 CI job measures library line coverage with SimpleCov and submits `coverage/.resultset.json` to Coveralls using GitHub's automatic token. Other matrix jobs do not submit duplicate reports. Run `COVERAGE=true bundle install` followed by `COVERAGE=true bundle exec rake spec` for a local report. The default-branch badge requires Coveralls to accept an upstream `master` build; configuring this workflow does not activate the external service.
8
9
 
9
10
  Typical `as_json` definitions may involve lots of database point queries and method calls. When returning collections of objects, a single call may yield hundreds of database queries that can take seconds. This library mitigates the problem by implementing a module called `CachedJson`.
10
11
 
@@ -12,16 +13,12 @@ Typical `as_json` definitions may involve lots of database point queries and met
12
13
 
13
14
  Using `Mongoid::CachedJson` we were able to cut our JSON API average response time by about a factor of 10.
14
15
 
15
- Compatibility
16
- -------------
17
-
18
- This gem is compatible with Mongoid 3, 4 and 5.
19
-
20
16
  Resources
21
17
  ---------
22
18
 
23
19
  * [Need Help? Google Group](http://groups.google.com/group/mongoid-cached-json)
24
- * [Source Code](http://github.com/dblock/mongoid-cached-json)
20
+ * [Source Code](http://github.com/mongoid/mongoid-cached-json)
21
+ * [Upgrading](UPGRADING.md)
25
22
 
26
23
  Quickstart
27
24
  ----------
@@ -102,7 +99,7 @@ Defining Fields
102
99
 
103
100
  `Mongoid::CachedJson` supports the following options:
104
101
 
105
- * `:hide_as_child_json_when` is an optional function that hides the child JSON from `as_json` parent objects, eg. `cached_json hide_as_child_json_when: lambda { |instance| ! instance.secret? }`
102
+ * `:hide_as_child_json_when` is an optional function that omits the child key from `as_json` parent objects when it returns true, eg. `cached_json hide_as_child_json_when: lambda { |instance| ! instance.secret? }`. A missing relationship remains a `nil` value.
106
103
 
107
104
  `Mongoid::CachedJson` field definitions support the following options:
108
105
 
@@ -111,8 +108,66 @@ Defining Fields
111
108
  * `:properties` can be one of `:short`, `:public`, `:all`, in this order
112
109
  * `:version` can be a single version for this field to appear in
113
110
  * `:versions` can be an array of versions for this field to appear in
111
+ * `:cache` defaults to `true`; set it to `false` to recompute a field on every serialization (see below)
114
112
  * `:reference_properties` can be one of `:short`, `:public`, `:all`, default will select the reference properties format dynamically (see below)
115
113
 
114
+ Per-Field Caching
115
+ -----------------
116
+
117
+ Use `cache: false` for a field whose value should be evaluated on every `as_json` call, even when the rest of the object's JSON comes from the cache. The field is excluded from the cached fragment and merged into the response after that fragment is fetched. Its `definition`, property/version filters, and configured transformations still apply.
118
+
119
+ ``` ruby
120
+ class Product
121
+ include Mongoid::Document
122
+ include Mongoid::CachedJson
123
+
124
+ field :name
125
+ field :stock, type: Integer
126
+
127
+ json_fields \
128
+ name: {},
129
+ stock: { cache: false }
130
+ end
131
+
132
+ product = Product.create!(name: 'Widget', stock: 10)
133
+ product.as_json # => { name: 'Widget', stock: 10 }
134
+
135
+ # Unsaved changes illustrate which fields are recomputed on a cache hit.
136
+ product.name = 'Renamed widget'
137
+ product.stock = 9
138
+ product.as_json # => { name: 'Widget', stock: 9 }
139
+ ```
140
+
141
+ The cached `name` stays unchanged until its cache entry expires or is invalidated (for example, by saving the product); `stock` is read from the current model instance each time. This option does not reload the model from MongoDB: reload a stale instance yourself if another process has changed its stored values.
142
+
143
+ For a `type: :reference` field, `cache: false` bypasses reads and writes of the referenced object's own JSON fragment through that relationship. The parent's other fields remain cached:
144
+
145
+ ``` ruby
146
+ class Order
147
+ include Mongoid::Document
148
+ include Mongoid::CachedJson
149
+
150
+ field :number
151
+ belongs_to :product
152
+
153
+ json_fields \
154
+ number: {},
155
+ product: { type: :reference, cache: false }
156
+ end
157
+
158
+ order = Order.create!(number: '123', product: product)
159
+ order.as_json # => { number: '123', product: { name: 'Renamed widget', stock: 9 } }
160
+ ```
161
+
162
+ The `product` reference is recomputed even if the product has a cached JSON fragment from another serialization. This does not disable caching on the `Product` model itself: `product.as_json` and other relationships to that product still use their normal cache policies. References nested inside the product also retain their own cache policies; the bypass is not recursive.
163
+
164
+ All Stored Fields
165
+ -----------------
166
+
167
+ Use `json_fields :all, display_name: { definition: :display_name }` to expand every Mongoid field declared so far (including `_id`) into an ordinary JSON field, then merge explicit definitions. An explicit definition overrides the generated definition for the same symbol key; for example, `json_fields :all, secret: { properties: :all }` restricts that field to the `:all` representation.
168
+
169
+ Declare Mongoid fields before `json_fields :all`. Later declarations, dynamic attributes, and associations are not added automatically; declare references explicitly with `type: :reference`. This is opt-in and may expose sensitive stored data or foreign keys, so prefer an explicit allowlist for public APIs. Runtime `as_json(methods: ...)` selection remains unsupported: declare computed methods in `json_fields`.
170
+
116
171
  Reference Properties
117
172
  --------------------
118
173
 
@@ -120,7 +175,7 @@ When calling `as_json` on a model that contains references to other models the v
120
175
 
121
176
  * Use the value of the `:reference_properties` option, if specified.
122
177
  * For `:short` JSON, use `:short`.
123
- * For `:public` JSON, use `:short`.
178
+ * For `:public` JSON, use `:public`.
124
179
  * For `:all` JSON, use `:all`.
125
180
 
126
181
  The dynamic selection where `:public` generates `:short` references allows to return smaller embedded collections, while `:all` allows to fetch deep data. Another way of looking at this is to say that a field in a `:short` JSON appears in collections, a field declared in the `:public` JSON appears for all users and the field declared in the `:all` JSON appears for object owners only.
@@ -178,6 +233,13 @@ person.as_json(version: :v2) # { first: 'John', middle: 'F.', last: 'Kennedy', n
178
233
  person.as_json(version: :v3) # { first: 'John', middle: 'F.', last: 'Kennedy', name: 'John F. Kennedy', born: 'May 29, 1917' }
179
234
  ```
180
235
 
236
+ Schema Cache Versions
237
+ ---------------------
238
+
239
+ Declare `json_fields cache_version: 2, name: {}` to add a model-specific schema namespace to its cache keys. Increment it when changing fields, computed values, or transformations in a deployment. This is independent of the API `version:` passed to `as_json`; callers need not change. Omitting `cache_version` preserves existing keys.
240
+
241
+ New code stops reading old fragments, including referenced fragments of that model. Old entries are not deleted and remain until eviction or a cache-store TTL. The gem does not set a default TTL; configure `expires_in` on your cache store if needed. Updates expire only the current namespace. For rolling deployments or rollbacks, do not reuse a namespace containing stale values: coordinate invalidation or choose another version.
242
+
181
243
  Transformations
182
244
  ---------------
183
245
 
@@ -214,6 +276,28 @@ Turning Caching Off
214
276
 
215
277
  You can set `Mongoid::CachedJson.config.disable_caching = true`. It may be a good idea to set it to `ENV['DISABLE_JSON_CACHING']`, in case this turns out not to be The Solution To All Of Your Performance Problems (TM).
216
278
 
279
+ To bypass shared-cache reads and writes for one model while retaining its JSON fields, versions, and property hierarchy, declare `json_fields disable_caching: true, name: {}`. This applies when the model is serialized directly, in collections, or as a reference. Other models, including its parent and referenced children, keep their own caching policy. Changes to an uncached child are visible even when its parent's scalar fields are cached.
280
+
281
+ Cache Invalidation Boundaries
282
+ -----------------------------
283
+
284
+ Normal model updates and destroys expire that document's JSON fragments through Mongoid callbacks. Bulk or raw collection updates that bypass model callbacks cannot invalidate the cache automatically. Explicitly expire each affected document after such a write:
285
+
286
+ ``` ruby
287
+ User.where(id: user_id).update_all(status: 'active')
288
+ User.find(user_id).expire_cached_json
289
+ ```
290
+
291
+ Concurrent writes have a separate boundary: an older in-memory instance can repopulate a fragment after another instance has saved newer values and expired the cache. Reload stale instances before serializing them, and expire their fragments if a stale writer may already have repopulated the cache:
292
+
293
+ ``` ruby
294
+ user.reload
295
+ user.expire_cached_json
296
+ user.as_json
297
+ ```
298
+
299
+ Reloading and expiring are not an atomic cross-process operation. This gem does not provide a distributed revision or compare-and-set protocol for cache stores. Applications requiring strict consistency across concurrent writers should coordinate persistence and cache invalidation themselves or disable caching for the affected data.
300
+
217
301
  Testing JSON
218
302
  ------------
219
303
 
@@ -293,6 +377,6 @@ See [CONTRIBUTING](CONTRIBUTING.md).
293
377
  Copyright and License
294
378
  ---------------------
295
379
 
296
- MIT License, see [LICENSE](https://github.com/dblock/mongoid-cached-json/blob/master/LICENSE.md) for details.
380
+ MIT License, see [LICENSE](https://github.com/mongoid/mongoid-cached-json/blob/master/LICENSE.md) for details.
297
381
 
298
382
  (c) 2012-2014 [Artsy](https://artsy.net) and [Contributors](CHANGELOG.md)
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'bundler'
3
3
 
4
- require File.expand_path('../lib/mongoid-cached-json/version', __FILE__)
4
+ require File.expand_path('lib/mongoid-cached-json/version', __dir__)
5
5
 
6
6
  Bundler.setup(:default, :development)
7
7
  Bundler::GemHelper.install_tasks
data/UPGRADING.md ADDED
@@ -0,0 +1,59 @@
1
+ # Upgrading
2
+
3
+ ## Upgrading to 1.7.0
4
+
5
+ ### Hidden child references
6
+
7
+ [#35](https://github.com/mongoid/mongoid-cached-json/pull/35) changes how a single child reference is serialized when the child's `hide_as_child_json_when` predicate returns `true`. Previously, an existing but hidden child and a missing child both appeared as `child: nil` (`"child": null` in JSON), so clients could not distinguish them. Now, an existing but hidden child's key is omitted; a missing child still appears as `child: nil`.
8
+
9
+ Here, `parent` is a `SecretParent` document with a `has_one` relationship to a `SometimesSecret` document. The parent exposes that relationship as the JSON field `child`; the hiding predicate is defined on the child model:
10
+
11
+ ```ruby
12
+ class SecretParent
13
+ include Mongoid::Document
14
+ include Mongoid::CachedJson
15
+
16
+ field :name
17
+ has_one :sometimes_secret
18
+
19
+ json_fields \
20
+ name: {},
21
+ child: { definition: :sometimes_secret, type: :reference }
22
+ end
23
+
24
+ class SometimesSecret
25
+ include Mongoid::Document
26
+ include Mongoid::CachedJson
27
+
28
+ field :secret
29
+ field :should_tell_secret, type: Boolean
30
+ belongs_to :secret_parent
31
+
32
+ json_fields \
33
+ hide_as_child_json_when: lambda { |child| !child.should_tell_secret },
34
+ secret: {}
35
+ end
36
+
37
+ parent = SecretParent.create!(name: 'Parent')
38
+ parent.create_sometimes_secret!(secret: 'Afraid of the dark', should_tell_secret: false)
39
+
40
+ # The child exists, but its hiding predicate returns true.
41
+ # Before 1.7.0:
42
+ parent.as_json(properties: :short)
43
+ # => { name: 'Parent', child: nil }
44
+
45
+ # From 1.7.0:
46
+ parent.as_json(properties: :short)
47
+ # => { name: 'Parent' }
48
+
49
+ # By contrast, a parent with no child still includes an explicit nil.
50
+ parent_without_child = SecretParent.create!(name: 'No child')
51
+ parent_without_child.as_json(properties: :short)
52
+ # => { name: 'No child', child: nil }
53
+ ```
54
+
55
+ A genuinely missing relationship still includes `child: nil`. Requests using `properties: :all` and serialization of the child itself are unchanged.
56
+
57
+ Update clients and response schemas that require the child key to allow its absence. In Ruby, `json[:child]` still returns `nil` for either case; use `json.key?(:child)` to distinguish an omitted key from an explicit `nil`.
58
+
59
+ Invalidate existing cached JSON fragments when deploying this change so cached results use the new representation. If the cache is shared with other applications, invalidate only this application's cached JSON entries rather than clearing the entire store.
@@ -1,14 +1,19 @@
1
1
  # encoding: utf-8
2
2
  module Mongoid
3
3
  module CachedJson
4
+ HIDDEN_JSON = :__mongoid_cached_json_hidden__
5
+
4
6
  extend ActiveSupport::Concern
5
7
 
6
8
  included do
7
9
  class_attribute :all_json_properties
8
10
  class_attribute :all_json_versions
9
11
  class_attribute :cached_json_field_defs
12
+ class_attribute :cached_json_uncached_field_defs
10
13
  class_attribute :cached_json_reference_defs
11
14
  class_attribute :hide_as_child_json_when
15
+ class_attribute :cached_json_cache_version
16
+ class_attribute :cached_json_disable_caching
12
17
  end
13
18
 
14
19
  module ClassMethods
@@ -17,11 +22,17 @@ module Mongoid
17
22
  # @param [ hash ] defs JSON field definition.
18
23
  #
19
24
  # @since 1.0
20
- def json_fields(defs)
25
+ def json_fields(defs, additions = {})
26
+ if defs == :all
27
+ defs = Hash[fields.keys.map { |field| [field.to_sym, {}] }].merge(additions)
28
+ end
29
+ self.cached_json_cache_version = defs.delete(:cache_version)
30
+ self.cached_json_disable_caching = defs.delete(:disable_caching)
21
31
  self.hide_as_child_json_when = defs.delete(:hide_as_child_json_when) || lambda { |_a| false }
22
32
  self.all_json_properties = [:short, :public, :all]
23
33
  cached_json_defs = Hash[defs.map { |k, v| [k, { type: :callable, properties: :short, definition: k }.merge(v)] }]
24
34
  self.cached_json_field_defs = {}
35
+ self.cached_json_uncached_field_defs = {}
25
36
  self.cached_json_reference_defs = {}
26
37
  # Collect all versions for clearing cache
27
38
  self.all_json_versions = cached_json_defs.map do |_field, definition|
@@ -29,7 +40,10 @@ module Mongoid
29
40
  end.flatten.compact.uniq
30
41
  all_json_properties.each_with_index do |property, i|
31
42
  cached_json_field_defs[property] = Hash[cached_json_defs.find_all do |_field, definition|
32
- all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :callable
43
+ all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :callable && definition[:cache] != false
44
+ end]
45
+ cached_json_uncached_field_defs[property] = Hash[cached_json_defs.find_all do |_field, definition|
46
+ all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :callable && definition[:cache] == false
33
47
  end]
34
48
  cached_json_reference_defs[property] = Hash[cached_json_defs.find_all do |_field, definition|
35
49
  all_json_properties.find_index(definition[:properties]) <= i && definition[:type] == :reference
@@ -49,8 +63,10 @@ module Mongoid
49
63
  def materialize_cached_json(clazz, id, object_reference, options)
50
64
  is_top_level_json = options[:is_top_level_json] || false
51
65
  object_reference = clazz.where(_id: id).first unless object_reference
52
- if !object_reference || (!is_top_level_json && options[:properties] != :all && clazz.hide_as_child_json_when.call(object_reference))
66
+ if !object_reference
53
67
  nil
68
+ elsif !is_top_level_json && options[:properties] != :all && clazz.hide_as_child_json_when.call(object_reference)
69
+ HIDDEN_JSON
54
70
  else
55
71
  Hash[clazz.cached_json_field_defs[options[:properties]].map do |field, definition|
56
72
  # version match
@@ -65,6 +81,22 @@ module Mongoid
65
81
  end
66
82
  end
67
83
 
84
+ # Materialize JSON fields that opt out of shared caching.
85
+ def materialize_uncached_json(clazz, id, object_reference, options)
86
+ object_reference = clazz.where(_id: id).first unless object_reference
87
+ return {} unless object_reference
88
+
89
+ Hash[clazz.cached_json_uncached_field_defs[options[:properties]].map do |field, definition|
90
+ versions = ([definition[:version]] | Array(definition[:versions])).compact
91
+ next unless versions.empty? || versions.include?(options[:version])
92
+ json_value = (definition[:definition].is_a?(Symbol) ? object_reference.send(definition[:definition]) : definition[:definition].call(object_reference))
93
+ Mongoid::CachedJson.config.transform.each do |t|
94
+ json_value = t.call(field, definition, json_value)
95
+ end
96
+ [field, json_value]
97
+ end.compact]
98
+ end
99
+
68
100
  # Given an object definition in the form of either an object or a class, id pair,
69
101
  # grab the as_json representation from the cache if possible, otherwise create
70
102
  # the as_json representation by loading the object from the database. For any
@@ -84,7 +116,8 @@ module Mongoid
84
116
  id = object_def[:id]
85
117
  end
86
118
  key = cached_json_key(options, clazz, id)
87
- json = { _ref: { _clazz: self, _key: key, _materialize_cached_json: [clazz, id, object_reference, options] } }
119
+ json = { _ref: { _clazz: self, _key: key, _disable_caching: clazz.cached_json_disable_caching,
120
+ _materialize_cached_json: [clazz, id, object_reference, options] } }
88
121
  keys = KeyReferences.new
89
122
  keys.set_and_add(key, json)
90
123
  reference_defs = clazz.cached_json_reference_defs[options[:properties]]
@@ -98,6 +131,12 @@ module Mongoid
98
131
  ref[:_parent] = json
99
132
  ref[:_field] = field
100
133
  end
134
+ if definition[:cache] == false
135
+ references = reference.is_a?(Array) ? reference : [reference]
136
+ references.each do |item|
137
+ item[:_ref][:_disable_caching] = true if item.is_a?(Hash) && item[:_ref]
138
+ end
139
+ end
101
140
  keys.merge_set(reference_keys)
102
141
  [field, reference]
103
142
  end])
@@ -109,7 +148,8 @@ module Mongoid
109
148
  # Cache key.
110
149
  def cached_json_key(options, cached_class, cached_id)
111
150
  base_class_name = cached_class.collection_name.to_s.singularize.camelize
112
- "as_json/#{options[:version]}/#{base_class_name}/#{cached_id}/#{options[:properties]}/#{!!options[:is_top_level_json]}"
151
+ key = "as_json/#{options[:version]}/#{base_class_name}/#{cached_id}/#{options[:properties]}/#{!!options[:is_top_level_json]}"
152
+ cached_class.cached_json_cache_version.nil? ? key : "#{key}/schema/#{cached_class.cached_json_cache_version}"
113
153
  end
114
154
 
115
155
  # If the reference is a symbol, we may be lucky and be able to figure out the as_json
@@ -126,7 +166,13 @@ module Mongoid
126
166
  else
127
167
  clazz = reference_def[:metadata].class_name.constantize
128
168
  end
129
- if reference_def[:metadata].relation == Mongoid::Relations::Referenced::ManyToMany
169
+ relation_class = if Mongoid::Compatibility::Version.mongoid7_or_newer?
170
+ Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy
171
+ else
172
+ Mongoid::Relations::Referenced::ManyToMany
173
+ end
174
+
175
+ if reference_def[:metadata].relation == relation_class
130
176
  object_ids = object.send(key)
131
177
  if object_ids
132
178
  reference_json = object_ids.map do |id|
@@ -145,13 +191,11 @@ module Mongoid
145
191
  reference_def_definition = reference_def[:definition]
146
192
  reference = reference_def_definition.is_a?(Symbol) ? object.send(reference_def_definition) : reference_def_definition.call(object)
147
193
  reference_json = nil
148
- if reference
149
- if reference.respond_to?(:as_json_partial)
150
- reference_keys, reference_json = reference.as_json_partial(options)
151
- keys = keys ? keys.merge_set(reference_keys) : reference_keys
152
- else
153
- reference_json = reference.as_json(options)
154
- end
194
+ if reference.respond_to?(:as_json_partial)
195
+ reference_keys, reference_json = reference.as_json_partial(options)
196
+ keys = keys ? keys.merge_set(reference_keys) : reference_keys
197
+ else
198
+ reference_json = reference.as_json(options)
155
199
  end
156
200
  end
157
201
  [keys, reference_json]
@@ -160,8 +204,10 @@ module Mongoid
160
204
 
161
205
  # Check whether the cache supports :read_multi and prefetch the data if it does.
162
206
  def self.materialize_json_references_with_read_multi(key_refs, partial_json)
163
- unfrozen_keys = key_refs.keys.to_a.map(&:dup) if key_refs # see https://github.com/mperham/dalli/pull/320
164
- read_multi = unfrozen_keys && Mongoid::CachedJson.config.cache.respond_to?(:read_multi)
207
+ if key_refs
208
+ unfrozen_keys = key_refs.select { |_key, refs| refs.any? { |ref| !ref[:_ref][:_disable_caching] } }.keys.map(&:dup)
209
+ end
210
+ read_multi = unfrozen_keys && !unfrozen_keys.empty? && Mongoid::CachedJson.config.cache.respond_to?(:read_multi)
165
211
  local_cache = read_multi ? Mongoid::CachedJson.config.cache.read_multi(*unfrozen_keys) : {}
166
212
  Mongoid::CachedJson.materialize_json_references(key_refs, local_cache, read_multi) if key_refs
167
213
  partial_json
@@ -174,7 +220,9 @@ module Mongoid
174
220
  _ref = ref.delete(:_ref)
175
221
  key = _ref[:_key]
176
222
  fetched_json = local_cache[key] if local_cache.key?(key)
177
- unless fetched_json
223
+ if _ref[:_disable_caching]
224
+ fetched_json = _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json])
225
+ elsif !fetched_json
178
226
  if read_multi
179
227
  # no value in cache, materialize and write
180
228
  fetched_json = (local_cache[key] = _ref[:_clazz].materialize_cached_json(* _ref[:_materialize_cached_json]))
@@ -186,8 +234,11 @@ module Mongoid
186
234
  end)
187
235
  end
188
236
  end
189
- if fetched_json
237
+ if fetched_json == HIDDEN_JSON
238
+ _ref[:_parent].delete(_ref[:_field]) if _ref[:_parent]
239
+ elsif fetched_json
190
240
  ref.merge! fetched_json
241
+ ref.merge! _ref[:_clazz].materialize_uncached_json(* _ref[:_materialize_cached_json])
191
242
  elsif _ref[:_parent]
192
243
  # a single _ref that resolved to a nil
193
244
  _ref[:_parent][_ref[:_field]] = nil
@@ -3,7 +3,6 @@ module Mongoid
3
3
  module CachedJson
4
4
  module Config
5
5
  extend self
6
- include ActiveSupport::Callbacks
7
6
 
8
7
  # Current configuration settings.
9
8
  attr_accessor :settings
@@ -1,5 +1,5 @@
1
1
  module Mongoid
2
2
  module CachedJson
3
- VERSION = '1.5.3'
3
+ VERSION = '1.7.0'
4
4
  end
5
5
  end
@@ -1,4 +1,4 @@
1
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
1
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
2
2
  require 'mongoid-cached-json/version'
3
3
 
4
4
  Gem::Specification.new do |s|
@@ -10,7 +10,7 @@ Gem::Specification.new do |s|
10
10
  s.required_rubygems_version = '>= 1.3.6'
11
11
  s.files = `git ls-files`.split("\n")
12
12
  s.require_paths = ['lib']
13
- s.homepage = 'http://github.com/dblock/mongoid-cached-json'
13
+ s.homepage = 'http://github.com/mongoid/mongoid-cached-json'
14
14
  s.licenses = ['MIT']
15
15
  s.summary = 'Cached-json is a DSL for describing JSON representations of Mongoid models.'
16
16
  s.add_dependency 'mongoid', '>= 3.0'
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Declaring all stored fields' do
4
+ before do
5
+ class AllFieldsDocument
6
+ include Mongoid::Document
7
+ include Mongoid::CachedJson
8
+ field :name
9
+ field :secret
10
+
11
+ def display_name
12
+ name.upcase
13
+ end
14
+
15
+ json_fields :all, secret: { properties: :all }, display_name: {}
16
+ field :added_later
17
+ end
18
+ end
19
+
20
+ after do
21
+ AllFieldsDocument.delete_all
22
+ Object.send(:remove_const, :AllFieldsDocument)
23
+ end
24
+
25
+ it 'expands fields at declaration time and adds computed fields' do
26
+ document = AllFieldsDocument.create!(name: 'example', secret: 'hidden', added_later: 'not declared')
27
+ expected = { _id: document.id, name: 'example', display_name: 'EXAMPLE' }
28
+ expect(document.as_json).to eq(expected)
29
+ expect([document].as_json).to eq([expected])
30
+ expect(document.as_json(properties: :all)).to eq(expected.merge(secret: 'hidden'))
31
+ end
32
+
33
+ it 'retains the normal field cache and invalidation behavior' do
34
+ document = AllFieldsDocument.create!(name: 'example')
35
+ document.as_json
36
+ document.name = 'unsaved'
37
+ expect(document.as_json[:name]).to eq('example')
38
+ document.save!
39
+ expect(document.as_json[:name]).to eq('unsaved')
40
+ end
41
+
42
+ it 'does not expose extra fields for ordinary explicit declarations' do
43
+ document = JsonFoobar.create!(foo: 'visible', bar: 'private')
44
+ expect(document.as_json).not_to have_key(:bar)
45
+ expect(document.as_json).not_to have_key(:_id)
46
+ end
47
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Schema cache versions' do
4
+ before do
5
+ class VersionedDocument
6
+ include Mongoid::Document
7
+ include Mongoid::CachedJson
8
+ field :value
9
+ json_fields cache_version: 1, value: {}
10
+ end
11
+ end
12
+
13
+ after do
14
+ VersionedDocument.delete_all
15
+ Object.send(:remove_const, :VersionedDocument)
16
+ end
17
+
18
+ [true, false].each do |bulk|
19
+ context "with bulk reads #{bulk}" do
20
+ before do
21
+ @store = ActiveSupport::Cache::MemoryStore.new
22
+ @store.singleton_class.send(:undef_method, :read_multi) unless bulk
23
+ Mongoid::CachedJson.config.cache = @store
24
+ end
25
+
26
+ it 'separates new schema fragments without changing the API version' do
27
+ document = VersionedDocument.create!(value: 'old')
28
+ expect(document.as_json).to eq(value: 'old')
29
+ document.value = 'new'
30
+ expect(document.as_json).to eq(value: 'old')
31
+ VersionedDocument.cached_json_cache_version = 2
32
+ expect(document.as_json).to eq(value: 'new')
33
+ VersionedDocument.cached_json_cache_version = 1
34
+ expect(document.as_json).to eq(value: 'old')
35
+ end
36
+
37
+ it 'invalidates the current namespace on a model update' do
38
+ document = VersionedDocument.create!(value: 'old')
39
+ [1, 2].each do |version|
40
+ VersionedDocument.cached_json_cache_version = version
41
+ document.as_json
42
+ end
43
+ document.update_attributes!(value: 'new')
44
+ expect(document.as_json).to eq(value: 'new')
45
+ VersionedDocument.cached_json_cache_version = 1
46
+ expect(document.as_json).to eq(value: 'old')
47
+ end
48
+
49
+ it 'uses the schema version for child fragments too' do
50
+ document = VersionedDocument.create!(value: 'old')
51
+ options = { properties: :short, version: :unspecified, is_top_level_json: false }
52
+ key = VersionedDocument.cached_json_key(options, VersionedDocument, document.id)
53
+ expect(key).to end_with('/false/schema/1')
54
+ expect([document].as_json).to eq([{ value: 'old' }])
55
+ end
56
+ end
57
+ end
58
+ end