forest_liana 9.22.8 → 9.22.10
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.
- checksums.yaml +4 -4
- data/app/serializers/forest_liana/serializer_factory.rb +7 -7
- data/app/services/forest_liana/ability/apimap_index.rb +44 -0
- data/app/services/forest_liana/ability/permission.rb +24 -6
- data/app/services/forest_liana/schema_utils.rb +12 -4
- data/lib/forest_liana/lookup_cache.rb +37 -0
- data/lib/forest_liana/version.rb +1 -1
- data/lib/forest_liana.rb +7 -4
- data/spec/lib/forest_liana/lookup_cache_spec.rb +68 -0
- data/spec/rails_helper.rb +1 -0
- data/spec/requests/projection_header_spec.rb +13 -0
- data/spec/services/forest_liana/ability/assert_can_read_query_fields_spec.rb +12 -0
- data/spec/services/forest_liana/ability/redact_fields_spec.rb +14 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23a1adeed07bf166b64953093122fc2e9c8c442d1ab52aec8227bec80f83f17d
|
|
4
|
+
data.tar.gz: 3cbea9923bf9350fbd8ad11c0a11479504c3a7b876b1c96e2c79a0cf602c88a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d41d2034547eb686df156ac01a72cd9e85f3aa10fcaf272831013dcf0827f7bd1b216b35828b22cc699d88e6ed1541ddaae5769b76e16cf6caf6aace4a49edef
|
|
7
|
+
data.tar.gz: f26b8e3fb84c18d3ee198b0f474e2bf189254b0c47a32365ee775ffcb1652baee2c6eea641e22cdb753d3f552ef5cc360db7b6c76a3df3f5eef8357adec0ede0
|
|
@@ -137,7 +137,7 @@ module ForestLiana
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def type
|
|
140
|
-
ForestLiana.name_for(object.class).demodulize
|
|
140
|
+
@type ||= ForestLiana.name_for(object.class).demodulize
|
|
141
141
|
end
|
|
142
142
|
|
|
143
143
|
def format_name(attribute_name)
|
|
@@ -155,8 +155,9 @@ module ForestLiana
|
|
|
155
155
|
def relationship_related_link(attribute_name)
|
|
156
156
|
ret = {}
|
|
157
157
|
|
|
158
|
-
# Has many smart field
|
|
159
|
-
|
|
158
|
+
# Has many smart field. Read off the class rather than has_many_relationships, which rebuilds
|
|
159
|
+
# every to-many on each call: once per serialized relation, so quadratic in their number.
|
|
160
|
+
current = self.class.to_many_associations&.[](attribute_name)
|
|
160
161
|
if current.try(:[], :options).try(:[], :name) == attribute_name
|
|
161
162
|
ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/#{attribute_name}"
|
|
162
163
|
return ret
|
|
@@ -195,10 +196,9 @@ module ForestLiana
|
|
|
195
196
|
|
|
196
197
|
if ret[:href].blank?
|
|
197
198
|
begin
|
|
198
|
-
SchemaUtils.many_associations(object.class).
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
end
|
|
199
|
+
@many_association_names ||= SchemaUtils.many_associations(object.class).map(&:name)
|
|
200
|
+
if @many_association_names.include?(attribute_name)
|
|
201
|
+
ret[:href] = "/forest/#{ForestLiana.name_for(object.class)}/#{object.id}/relationships/#{attribute_name}"
|
|
202
202
|
end
|
|
203
203
|
rescue TypeError, ActiveRecord::StatementInvalid, NoMethodError => exception
|
|
204
204
|
FOREST_LOGGER.warn "Cannot load the association #{attribute_name} on #{object.class.name} #{object.id}.\n#{exception&.backtrace&.join("\n\t")}"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module ForestLiana
|
|
2
|
+
module Ability
|
|
3
|
+
# The apimap lookups a field-read check repeats once per requested field, made once per check
|
|
4
|
+
# instead: a wide collection on a large schema otherwise rescans every collection, and every
|
|
5
|
+
# field of the root one, for each field a page displays.
|
|
6
|
+
class ApimapIndex
|
|
7
|
+
def initialize(apimap)
|
|
8
|
+
@collections_by_name = apimap.each_with_object({}) { |collection, index| index[collection.name.to_s] ||= collection }
|
|
9
|
+
@collections_by_model = {}
|
|
10
|
+
@fields_by_name = {}
|
|
11
|
+
@smart_belongs_to_by_name = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def collection_for(model)
|
|
15
|
+
@collections_by_model.fetch(model) do
|
|
16
|
+
@collections_by_model[model] = @collections_by_name[ForestLiana.name_for(model)]
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def exposed?(collection_name)
|
|
21
|
+
@collections_by_name.key?(collection_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fields_named(model, field_name)
|
|
25
|
+
@fields_by_name[model] ||= first_by_name(collection_for(model)&.fields.to_a, all: true)
|
|
26
|
+
@fields_by_name[model][field_name] || []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def smart_belongs_to(model, field_name)
|
|
30
|
+
@smart_belongs_to_by_name[model] ||= first_by_name(collection_for(model)&.fields_smart_belongs_to.to_a)
|
|
31
|
+
@smart_belongs_to_by_name[model][field_name]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def first_by_name(fields, all: false)
|
|
37
|
+
fields.each_with_object({}) do |field, index|
|
|
38
|
+
name = field[:field].to_s
|
|
39
|
+
all ? (index[name] ||= []) << field : index[name] ||= field
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -86,6 +86,10 @@ module ForestLiana
|
|
|
86
86
|
def redact_fields(user, root_model, fields_hash, named_collections:)
|
|
87
87
|
return fields_hash if fields_hash.nil?
|
|
88
88
|
|
|
89
|
+
with_apimap_index { redact_indexed_fields(user, root_model, fields_hash, named_collections) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def redact_indexed_fields(user, root_model, fields_hash, named_collections)
|
|
89
93
|
root_name = ForestLiana.name_for(root_model)
|
|
90
94
|
|
|
91
95
|
resolved = fields_hash.each_with_object({}) do |(collection_key, csv), acc|
|
|
@@ -131,6 +135,10 @@ module ForestLiana
|
|
|
131
135
|
# caller cannot read. +root_model+ is pinned readable — +browse+/+read+ already gate it
|
|
132
136
|
# upstream — so it is never itself a refusal.
|
|
133
137
|
def assert_can_read_query_fields(user, root_model, filter_paths: [], sort_paths: [], search_paths: [])
|
|
138
|
+
with_apimap_index { assert_indexed_query_fields(user, root_model, filter_paths, sort_paths, search_paths) }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def assert_indexed_query_fields(user, root_model, filter_paths, sort_paths, search_paths)
|
|
134
142
|
root_name = ForestLiana.name_for(root_model)
|
|
135
143
|
|
|
136
144
|
usages = filter_paths.map { |path| query_usage('filter on', root_model, path) } +
|
|
@@ -352,29 +360,39 @@ module ForestLiana
|
|
|
352
360
|
end
|
|
353
361
|
|
|
354
362
|
def smart_belongs_to_field(model, field_name)
|
|
355
|
-
|
|
363
|
+
apimap_index.smart_belongs_to(model, field_name)
|
|
356
364
|
end
|
|
357
365
|
|
|
358
366
|
# SchemaAdapter drops a relation to an excluded model, but keeps one whose target is missing from
|
|
359
367
|
# the apimap otherwise (a table absent at boot, a secondary database unreachable): skipped alike.
|
|
360
368
|
# A polymorphic relation's reference names the association, never one of its targets, so it stays.
|
|
361
369
|
def only_computed_smart_field?(model, field_name, unexposed_targets)
|
|
362
|
-
fields =
|
|
363
|
-
fields = fields.reject do |field|
|
|
370
|
+
fields = apimap_index.fields_named(model, field_name).reject do |field|
|
|
364
371
|
!field[:is_virtual] && unexposed_targets.include?(field[:reference].to_s.split('.').first)
|
|
365
372
|
end
|
|
366
373
|
|
|
367
374
|
fields.any? && fields.all? { |field| field[:is_virtual] && field[:reference].nil? && field[:integration].nil? }
|
|
368
375
|
end
|
|
369
376
|
|
|
370
|
-
def
|
|
371
|
-
|
|
377
|
+
def apimap_index
|
|
378
|
+
@apimap_index || ApimapIndex.new(ForestLiana.apimap)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def with_apimap_index
|
|
382
|
+
return yield if @apimap_index
|
|
383
|
+
|
|
384
|
+
@apimap_index = ApimapIndex.new(ForestLiana.apimap)
|
|
385
|
+
begin
|
|
386
|
+
yield
|
|
387
|
+
ensure
|
|
388
|
+
@apimap_index = nil
|
|
389
|
+
end
|
|
372
390
|
end
|
|
373
391
|
|
|
374
392
|
# No role can ever be granted `read` on a collection absent from the apimap — a denial
|
|
375
393
|
# message naming it as unreadable would point at a permission nobody can grant.
|
|
376
394
|
def collection_exposed?(collection_name)
|
|
377
|
-
|
|
395
|
+
apimap_index.exposed?(collection_name)
|
|
378
396
|
end
|
|
379
397
|
|
|
380
398
|
# unexposed/also_denied (each present iff non-empty) tell UnauthorizedFieldsError which of
|
|
@@ -31,6 +31,18 @@ module ForestLiana
|
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def self.find_model_from_collection_name(collection_name, logs = false)
|
|
34
|
+
model_found = LookupCache.fetch(:model_from_collection_name, collection_name) do
|
|
35
|
+
scan_models_for_collection_name(collection_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if logs && model_found.nil?
|
|
39
|
+
FOREST_LOGGER.warn "No model found for collection #{collection_name}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
model_found
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.scan_models_for_collection_name(collection_name)
|
|
34
46
|
model_found = nil
|
|
35
47
|
ForestLiana.models.each do |model|
|
|
36
48
|
if model.abstract_class?
|
|
@@ -42,10 +54,6 @@ module ForestLiana
|
|
|
42
54
|
break if model_found
|
|
43
55
|
end
|
|
44
56
|
|
|
45
|
-
if logs && model_found.nil?
|
|
46
|
-
FOREST_LOGGER.warn "No model found for collection #{collection_name}"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
57
|
model_found
|
|
50
58
|
end
|
|
51
59
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module ForestLiana
|
|
2
|
+
# Remembers lookups by name over ForestLiana.models, .apimap and .names_overriden: on a large
|
|
3
|
+
# schema each one is a linear scan, schema_for_resource a scan of scans, and every request runs
|
|
4
|
+
# several. Any of the three being replaced or growing (the bootstrapper, a Forest collection
|
|
5
|
+
# file, a code reload) drops everything remembered, so an answer never outlives what it read.
|
|
6
|
+
module LookupCache
|
|
7
|
+
# A miss is not remembered: the router resolves a request's collection before authenticating
|
|
8
|
+
# it, so caching unknown names would let any caller grow this cache without bound.
|
|
9
|
+
def self.fetch(namespace, key)
|
|
10
|
+
reset_if_stale
|
|
11
|
+
store = (@stores[namespace] ||= {})
|
|
12
|
+
return store[key] if store.key?(key)
|
|
13
|
+
|
|
14
|
+
value = yield
|
|
15
|
+
store[key] = value unless value.nil?
|
|
16
|
+
value
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.reset!
|
|
20
|
+
@stores = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.reset_if_stale
|
|
24
|
+
models = ForestLiana.models
|
|
25
|
+
apimap = ForestLiana.apimap
|
|
26
|
+
names = ForestLiana.names_overriden
|
|
27
|
+
return if @stores && models.equal?(@models) && models.size == @models_size &&
|
|
28
|
+
apimap.equal?(@apimap) && apimap.size == @apimap_size &&
|
|
29
|
+
names.equal?(@names) && names.size == @names_size
|
|
30
|
+
|
|
31
|
+
@models, @models_size = models, models.size
|
|
32
|
+
@apimap, @apimap_size = apimap, apimap.size
|
|
33
|
+
@names, @names_size = names, names.size
|
|
34
|
+
@stores = {}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/forest_liana/version.rb
CHANGED
data/lib/forest_liana.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'forest_liana/engine'
|
|
2
|
+
require 'forest_liana/lookup_cache'
|
|
2
3
|
|
|
3
4
|
module Forest
|
|
4
5
|
end
|
|
@@ -69,14 +70,16 @@ module ForestLiana
|
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def self.schema_for_resource resource
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
73
|
+
LookupCache.fetch(:schema_for_resource, resource.name) do
|
|
74
|
+
self.apimap.find do |collection|
|
|
75
|
+
SchemaUtils.find_model_from_collection_name(collection.name)
|
|
76
|
+
.try(:name) == resource.name
|
|
77
|
+
end
|
|
75
78
|
end
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
def self.name_for(model)
|
|
79
|
-
self.names_overriden[model] || model.try(:name).gsub('::', '__')
|
|
82
|
+
LookupCache.fetch(:name_for, model) { self.names_overriden[model] || model.try(:name).gsub('::', '__') }
|
|
80
83
|
end
|
|
81
84
|
|
|
82
85
|
# TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Lookups by name over the schema' do
|
|
4
|
+
describe 'ForestLiana.name_for' do
|
|
5
|
+
after { ForestLiana.names_overriden.delete(Tree) }
|
|
6
|
+
|
|
7
|
+
it 'answers an override registered after a first lookup' do
|
|
8
|
+
expect(ForestLiana.name_for(Tree)).to eq 'Tree'
|
|
9
|
+
|
|
10
|
+
ForestLiana.names_overriden[Tree] = 'Orchard'
|
|
11
|
+
|
|
12
|
+
expect(ForestLiana.name_for(Tree)).to eq 'Orchard'
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe 'ForestLiana::SchemaUtils.find_model_from_collection_name' do
|
|
17
|
+
let(:ghost) { Class.new(ActiveRecord::Base) { self.table_name = 'trees' } }
|
|
18
|
+
|
|
19
|
+
after do
|
|
20
|
+
ForestLiana.models.delete(ghost)
|
|
21
|
+
ForestLiana.names_overriden.delete(ghost)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'scans the models once for repeated lookups of a name' do
|
|
25
|
+
expect(ForestLiana::SchemaUtils).to receive(:scan_models_for_collection_name).with('Tree').once.and_call_original
|
|
26
|
+
|
|
27
|
+
3.times { expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Tree')).to eq Tree }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'does not remember an unknown name, which any caller can send before authenticating' do
|
|
31
|
+
expect(ForestLiana::SchemaUtils).to receive(:scan_models_for_collection_name).with('Ghost').twice.and_call_original
|
|
32
|
+
|
|
33
|
+
2.times { expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Ghost')).to be_nil }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'still warns on each lookup of an unknown name' do
|
|
37
|
+
expect(FOREST_LOGGER).to receive(:warn).with('No model found for collection Ghost').twice
|
|
38
|
+
|
|
39
|
+
2.times { expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Ghost', true)).to be_nil }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'finds a model added after a first lookup missed it' do
|
|
43
|
+
expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Ghost')).to be_nil
|
|
44
|
+
|
|
45
|
+
ForestLiana.names_overriden[ghost] = 'Ghost'
|
|
46
|
+
ForestLiana.models << ghost
|
|
47
|
+
|
|
48
|
+
expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Ghost')).to eq ghost
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe 'ForestLiana.schema_for_resource' do
|
|
53
|
+
it 'resolves the collection of a resource once for repeated lookups' do
|
|
54
|
+
expect(ForestLiana.schema_for_resource(Tree).name).to eq 'Tree'
|
|
55
|
+
expect(ForestLiana::SchemaUtils).not_to receive(:find_model_from_collection_name)
|
|
56
|
+
|
|
57
|
+
expect(ForestLiana.schema_for_resource(Tree).name).to eq 'Tree'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'answers from the apimap in place when it is replaced' do
|
|
61
|
+
expect(ForestLiana.schema_for_resource(Tree).name).to eq 'Tree'
|
|
62
|
+
replacement = ForestLiana::Model::Collection.new(name: 'Tree', fields: [])
|
|
63
|
+
allow(ForestLiana).to receive(:apimap).and_return([replacement])
|
|
64
|
+
|
|
65
|
+
expect(ForestLiana.schema_for_resource(Tree)).to equal replacement
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
data/spec/rails_helper.rb
CHANGED
|
@@ -65,6 +65,7 @@ RSpec.configure do |config|
|
|
|
65
65
|
# own comment) — cleared before every example so one spec's warning can't silence another's,
|
|
66
66
|
# regardless of run order or which file exercises the valve.
|
|
67
67
|
config.before { ForestLiana::MissingAttributeValve::WARNED_ONCE.clear }
|
|
68
|
+
config.before { ForestLiana::LookupCache.reset! }
|
|
68
69
|
|
|
69
70
|
# Filter lines from Rails gems in backtraces.
|
|
70
71
|
config.filter_rails_from_backtrace!
|
|
@@ -225,6 +225,19 @@ describe 'Requesting resources with the Forest-Projection header', :type => :req
|
|
|
225
225
|
expect(body['data']['relationships'].values.map(&:keys).uniq).to eq [%w[links]]
|
|
226
226
|
end
|
|
227
227
|
|
|
228
|
+
# has_many_relationships rebuilds every to-many of the record. Called once per link it is
|
|
229
|
+
# quadratic in the to-many count, which on a wide collection outweighs the whole get-one.
|
|
230
|
+
it 'builds the to-many relationships once, however many links the record carries' do
|
|
231
|
+
builds = 0
|
|
232
|
+
allow_any_instance_of(ForestLiana::UserSpace::UserSerializer).to receive(:has_many_relationships)
|
|
233
|
+
.and_wrap_original { |original| builds += 1; original.call }
|
|
234
|
+
|
|
235
|
+
get "/forest/User/#{@user.id}", headers: projecting('id,name')
|
|
236
|
+
|
|
237
|
+
expect(body['data']['relationships'].keys.size).to eq 7
|
|
238
|
+
expect(builds).to eq 1
|
|
239
|
+
end
|
|
240
|
+
|
|
228
241
|
# selects_from anchors on the query's own FROM, which a `selects_of('users')` filter would
|
|
229
242
|
# not: a to-many preload reads FROM "trees" and names "trees_users", never `"users"`, so it
|
|
230
243
|
# would be dropped before any assertion could see it.
|
|
@@ -67,6 +67,18 @@ module ForestLiana
|
|
|
67
67
|
expect { dummy_class.assert_can_read_query_fields(user, Tree) }.not_to raise_error
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
it 'indexes the apimap once, however many paths are checked' do
|
|
71
|
+
write_permissions('Tree' => true, 'User' => true, 'Island' => true)
|
|
72
|
+
expect(ForestLiana::Ability::ApimapIndex).to receive(:new).with(ForestLiana.apimap).once.and_call_original
|
|
73
|
+
|
|
74
|
+
expect do
|
|
75
|
+
dummy_class.assert_can_read_query_fields(
|
|
76
|
+
user, Tree, filter_paths: %w[owner:name island:name], sort_paths: %w[cutter:name name],
|
|
77
|
+
search_paths: %w[owner:title]
|
|
78
|
+
)
|
|
79
|
+
end.not_to raise_error
|
|
80
|
+
end
|
|
81
|
+
|
|
70
82
|
it 'serves a filter, a sort and a search on an unreadable collection once the option is on' do
|
|
71
83
|
write_permissions('Tree' => true, 'Island' => false)
|
|
72
84
|
ForestLiana.skip_relation_read_permissions = true
|
|
@@ -167,6 +167,20 @@ module ForestLiana
|
|
|
167
167
|
.to eq('Tree' => 'id,name,island', 'Island' => 'name')
|
|
168
168
|
end
|
|
169
169
|
|
|
170
|
+
# Resolving each field against the apimap rescans every collection: on a wide collection and
|
|
171
|
+
# a large schema, that cost outweighs the request it guards.
|
|
172
|
+
it 'indexes the apimap once, however many fields are requested' do
|
|
173
|
+
write_permissions('Tree' => true, 'User' => true, 'Island' => true)
|
|
174
|
+
expect(ForestLiana::Ability::ApimapIndex).to receive(:new).with(ForestLiana.apimap).once.and_call_original
|
|
175
|
+
|
|
176
|
+
redacted = dummy_class.redact_fields(
|
|
177
|
+
user, Tree, { 'Tree' => 'id,name,age,owner,cutter,island', 'owner' => 'name', 'island' => 'name' },
|
|
178
|
+
named_collections: %w[Tree owner island]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
expect(redacted).to eq('Tree' => 'id,name,age,owner,cutter,island', 'owner' => 'name', 'island' => 'name')
|
|
182
|
+
end
|
|
183
|
+
|
|
170
184
|
it 'checks nothing at all when only root collection fields are requested' do
|
|
171
185
|
write_permissions({})
|
|
172
186
|
expect_any_instance_of(ForestLiana::Ability::Fetch).not_to receive(:get_permissions)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_liana
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 9.22.
|
|
4
|
+
version: 9.22.10
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Munda
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -276,6 +276,7 @@ files:
|
|
|
276
276
|
- app/serializers/forest_liana/stripe_payment_serializer.rb
|
|
277
277
|
- app/serializers/forest_liana/stripe_subscription_serializer.rb
|
|
278
278
|
- app/services/forest_liana/ability.rb
|
|
279
|
+
- app/services/forest_liana/ability/apimap_index.rb
|
|
279
280
|
- app/services/forest_liana/ability/exceptions/access_denied.rb
|
|
280
281
|
- app/services/forest_liana/ability/exceptions/action_condition_error.rb
|
|
281
282
|
- app/services/forest_liana/ability/exceptions/approval_selection_too_large.rb
|
|
@@ -364,6 +365,7 @@ files:
|
|
|
364
365
|
- lib/forest_liana/collection.rb
|
|
365
366
|
- lib/forest_liana/engine.rb
|
|
366
367
|
- lib/forest_liana/json_printer.rb
|
|
368
|
+
- lib/forest_liana/lookup_cache.rb
|
|
367
369
|
- lib/forest_liana/mixpanel_event.rb
|
|
368
370
|
- lib/forest_liana/schema_file_updater.rb
|
|
369
371
|
- lib/forest_liana/version.rb
|
|
@@ -470,6 +472,7 @@ files:
|
|
|
470
472
|
- spec/lib/forest_liana/bootstrapper_spec.rb
|
|
471
473
|
- spec/lib/forest_liana/collection_spec.rb
|
|
472
474
|
- spec/lib/forest_liana/engine_spec.rb
|
|
475
|
+
- spec/lib/forest_liana/lookup_cache_spec.rb
|
|
473
476
|
- spec/lib/forest_liana/schema_file_updater_spec.rb
|
|
474
477
|
- spec/models/forest_liana/model/action_spec.rb
|
|
475
478
|
- spec/models/forest_liana/model/collection_spec.rb
|
|
@@ -828,6 +831,7 @@ test_files:
|
|
|
828
831
|
- spec/lib/forest_liana/bootstrapper_spec.rb
|
|
829
832
|
- spec/lib/forest_liana/collection_spec.rb
|
|
830
833
|
- spec/lib/forest_liana/engine_spec.rb
|
|
834
|
+
- spec/lib/forest_liana/lookup_cache_spec.rb
|
|
831
835
|
- spec/lib/forest_liana/schema_file_updater_spec.rb
|
|
832
836
|
- spec/models/forest_liana/model/action_spec.rb
|
|
833
837
|
- spec/models/forest_liana/model/collection_spec.rb
|