forest_liana 9.20.13 → 9.22.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.
- checksums.yaml +4 -4
- data/app/controllers/forest_liana/application_controller.rb +111 -5
- data/app/controllers/forest_liana/associations_controller.rb +108 -7
- data/app/controllers/forest_liana/base_controller.rb +24 -5
- data/app/controllers/forest_liana/capabilities_controller.rb +13 -0
- data/app/controllers/forest_liana/resources_controller.rb +135 -22
- data/app/controllers/forest_liana/smart_actions_controller.rb +3 -0
- data/app/deserializers/forest_liana/resource_deserializer.rb +5 -1
- data/app/models/forest_liana/model/collection.rb +47 -1
- data/app/serializers/forest_liana/missing_attribute_valve.rb +84 -0
- data/app/serializers/forest_liana/serializer_factory.rb +3 -4
- data/app/services/forest_liana/ability/exceptions/approval_selection_too_large.rb +17 -0
- data/app/services/forest_liana/ability/exceptions/require_approval.rb +3 -2
- data/app/services/forest_liana/ability/exceptions/unauthorized_fields_error.rb +44 -0
- data/app/services/forest_liana/ability/exceptions/unauthorized_query_field_error.rb +21 -0
- data/app/services/forest_liana/ability/exceptions/unexposed_query_collection_error.rb +28 -0
- data/app/services/forest_liana/ability/fetch.rb +1 -1
- data/app/services/forest_liana/ability/permission/smart_action_checker.rb +39 -2
- data/app/services/forest_liana/ability/permission.rb +248 -21
- data/app/services/forest_liana/base_getter.rb +476 -1
- data/app/services/forest_liana/belongs_to_updater.rb +8 -0
- data/app/services/forest_liana/capabilities_getter.rb +162 -0
- data/app/services/forest_liana/field_path.rb +39 -0
- data/app/services/forest_liana/filters_parser.rb +38 -1
- data/app/services/forest_liana/has_many_associator.rb +9 -0
- data/app/services/forest_liana/has_many_dissociator.rb +19 -0
- data/app/services/forest_liana/has_many_getter.rb +62 -7
- data/app/services/forest_liana/projection_parser.rb +68 -0
- data/app/services/forest_liana/resource_getter.rb +48 -2
- data/app/services/forest_liana/resources_getter.rb +88 -212
- data/app/services/forest_liana/schema_adapter.rb +2 -3
- data/app/services/forest_liana/scope_manager.rb +12 -0
- data/app/services/forest_liana/search_query_builder.rb +173 -45
- data/app/services/forest_liana/smart_field_dependencies.rb +82 -0
- data/app/services/forest_liana/utils/context_variables_injector.rb +9 -1
- data/config/initializers/errors.rb +16 -0
- data/config/routes.rb +3 -0
- data/lib/forest_liana/bootstrapper.rb +22 -0
- data/lib/forest_liana/collection.rb +47 -0
- data/lib/forest_liana/engine.rb +4 -2
- data/lib/forest_liana/version.rb +1 -1
- data/lib/forest_liana.rb +9 -0
- data/spec/deserializers/forest_liana/resource_deserializer_spec.rb +43 -0
- data/spec/dummy/app/models/article.rb +3 -0
- data/spec/dummy/app/models/flag.rb +3 -0
- data/spec/dummy/app/models/island.rb +3 -0
- data/spec/dummy/app/models/membership.rb +4 -0
- data/spec/dummy/app/models/owner.rb +9 -0
- data/spec/dummy/app/models/town.rb +5 -0
- data/spec/dummy/app/models/user.rb +9 -1
- data/spec/dummy/config/database.yml +14 -9
- data/spec/dummy/config/environments/test.rb +3 -2
- data/spec/dummy/config/initializers/forest_liana.rb +4 -0
- data/spec/dummy/db/garage_schema.rb +107 -0
- data/spec/dummy/db/migrate/20220719094127_create_cars.rb +6 -0
- data/spec/dummy/db/migrate/20220719094450_create_drivers.rb +3 -0
- data/spec/dummy/db/migrate/20260909120000_create_memberships.rb +10 -0
- data/spec/dummy/db/migrate/20260909130000_create_flags.rb +10 -0
- data/spec/dummy/db/migrate/20260914120000_create_articles_and_taggings.rb +34 -0
- data/spec/dummy/db/migrate/20260922120000_create_trees_users.rb +7 -0
- data/spec/dummy/db/schema.rb +56 -1
- data/spec/dummy/db/user_schema.rb +105 -0
- data/spec/dummy/lib/forest_liana/collections/address.rb +1 -1
- data/spec/dummy/lib/forest_liana/collections/location.rb +7 -0
- data/spec/dummy/lib/forest_liana/collections/owner.rb +18 -0
- data/spec/dummy/lib/forest_liana/collections/product.rb +5 -0
- data/spec/dummy/lib/forest_liana/collections/tree.rb +46 -0
- data/spec/dummy/lib/forest_liana/collections/user.rb +20 -1
- data/spec/lib/forest_liana/bootstrapper_spec.rb +60 -3
- data/spec/lib/forest_liana/collection_spec.rb +86 -1
- data/spec/models/forest_liana/model/collection_spec.rb +79 -0
- data/spec/rails_helper.rb +13 -1
- data/spec/requests/actions_controller_spec.rb +5 -1
- data/spec/requests/associations_spec.rb +669 -0
- data/spec/requests/capabilities_spec.rb +301 -0
- data/spec/requests/cors_spec.rb +6 -0
- data/spec/requests/count_spec.rb +82 -0
- data/spec/requests/missing_attribute_valve_spec.rb +161 -0
- data/spec/requests/projection_header_spec.rb +602 -0
- data/spec/requests/projection_inherited_loads_spec.rb +140 -0
- data/spec/requests/query_footprint_spec.rb +549 -0
- data/spec/requests/resources_spec.rb +549 -13
- data/spec/requests/sti_record_conversion_spec.rb +79 -0
- data/spec/serializers/forest_liana/missing_attribute_valve_spec.rb +26 -0
- data/spec/services/forest_liana/ability/assert_can_read_query_fields_spec.rb +391 -0
- data/spec/services/forest_liana/ability/permission/smart_action_checker_spec.rb +132 -0
- data/spec/services/forest_liana/ability/permission_spec.rb +45 -2
- data/spec/services/forest_liana/ability/redact_fields_spec.rb +495 -0
- data/spec/services/forest_liana/apimap_sorter_spec.rb +20 -0
- data/spec/services/forest_liana/base_getter_spec.rb +227 -0
- data/spec/services/forest_liana/belongs_to_updater_spec.rb +35 -0
- data/spec/services/forest_liana/field_path_spec.rb +142 -0
- data/spec/services/forest_liana/filters_parser_spec.rb +103 -0
- data/spec/services/forest_liana/has_many_associator_spec.rb +26 -0
- data/spec/services/forest_liana/has_many_dissociator_spec.rb +76 -0
- data/spec/services/forest_liana/has_many_getter_pagination_spec.rb +39 -0
- data/spec/services/forest_liana/has_many_getter_spec.rb +49 -0
- data/spec/services/forest_liana/projection_parser_spec.rb +82 -0
- data/spec/services/forest_liana/resources_getter_spec.rb +128 -4
- data/spec/services/forest_liana/schema_adapter_default_values_spec.rb +8 -2
- data/spec/services/forest_liana/schema_adapter_spec.rb +9 -3
- data/spec/services/forest_liana/search_query_builder_spec.rb +573 -1
- data/spec/services/forest_liana/smart_field_dependencies_spec.rb +147 -0
- data/spec/support/forest_liana/query_capture.rb +85 -0
- data/spec/support/forest_liana/query_capture_spec.rb +119 -0
- data/test/dummy/app/models/has_one_field.rb +6 -1
- data/test/dummy/app/models/serialize_field.rb +6 -1
- data/test/dummy/config/environments/test.rb +3 -2
- data/test/test_helper.rb +4 -1
- metadata +78 -8
- data/spec/dummy/app/config/routes.rb +0 -3
- data/spec/dummy/config/initializers/assets.rb +0 -11
- data/test/dummy/config/initializers/assets.rb +0 -11
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6042deb02adc0ae0d73c97cac99e2abdf0807cdf759cfd0cde116bf94fdc3dff
|
|
4
|
+
data.tar.gz: 351c37c6fd6cfc17e9c4de3c8f760b84527ca7c1d7a0e2df6976b67b8d549329
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a952e5b05d2c1491f74b77a53e16012d89419af19040e6168488e000afba0651ebea4f3d7cb21b1ef3045598be8317c30930f7a1e6a681fd5458b8ce956672f
|
|
7
|
+
data.tar.gz: 9884f0447c34ccb867abf7a2b41326229391e6a3c2689ad9884a15d09ac0ad067ba3a7ebfb30df13cab9502524ef29cd69de941779bfd3185cd385a893261960
|
|
@@ -4,10 +4,24 @@ require 'csv'
|
|
|
4
4
|
module ForestLiana
|
|
5
5
|
class ApplicationController < ForestLiana::BaseController
|
|
6
6
|
rescue_from ForestLiana::Ability::Exceptions::AccessDenied, with: :render_error
|
|
7
|
+
rescue_from ForestLiana::Errors::HTTP400Error, with: :render_error
|
|
7
8
|
rescue_from ForestLiana::Errors::HTTP403Error, with: :render_error
|
|
8
9
|
rescue_from ForestLiana::Errors::HTTP422Error, with: :render_error
|
|
10
|
+
rescue_from ForestLiana::Errors::PermissionsUnavailableError, with: :render_error
|
|
9
11
|
rescue_from ForestLiana::Ability::Exceptions::ActionConditionError, with: :render_error
|
|
10
12
|
rescue_from ForestLiana::Ability::Exceptions::UnknownCollection, with: :render_error
|
|
13
|
+
rescue_from ForestLiana::Ability::Exceptions::UnauthorizedFieldsError, with: :render_error
|
|
14
|
+
rescue_from ForestLiana::Ability::Exceptions::UnauthorizedQueryFieldError, with: :render_error
|
|
15
|
+
rescue_from ForestLiana::Ability::Exceptions::UnexposedQueryCollectionError, with: :render_error
|
|
16
|
+
|
|
17
|
+
# `render_error` above already gives these their 403 + name/data payload — every list here is
|
|
18
|
+
# for an action whose own `begin/rescue` would otherwise let a bare `rescue => error` swallow
|
|
19
|
+
# them into a 500 first. Those actions catch this list and re-`raise`, letting it reach here.
|
|
20
|
+
QUERY_PERMISSION_ERRORS = [
|
|
21
|
+
ForestLiana::Ability::Exceptions::UnauthorizedFieldsError,
|
|
22
|
+
ForestLiana::Ability::Exceptions::UnauthorizedQueryFieldError,
|
|
23
|
+
ForestLiana::Ability::Exceptions::UnexposedQueryCollectionError,
|
|
24
|
+
].freeze
|
|
11
25
|
|
|
12
26
|
def self.papertrail?
|
|
13
27
|
Object.const_get('PaperTrail::Version').is_a?(Class) rescue false
|
|
@@ -40,7 +54,7 @@ module ForestLiana
|
|
|
40
54
|
|
|
41
55
|
def serialize_model(record, options = {})
|
|
42
56
|
options[:is_collection] = false
|
|
43
|
-
options[:context] = { unoptimized:
|
|
57
|
+
options[:context] = { unoptimized: options[:fields].nil? }.merge(options[:context] || {})
|
|
44
58
|
|
|
45
59
|
json = ForestAdmin::JSONAPI::Serializer.serialize(record, options)
|
|
46
60
|
|
|
@@ -49,7 +63,7 @@ module ForestLiana
|
|
|
49
63
|
|
|
50
64
|
def serialize_models(records, options = {}, fields_searched = [])
|
|
51
65
|
options[:is_collection] = true
|
|
52
|
-
if options[:
|
|
66
|
+
if options[:fields].nil?
|
|
53
67
|
options[:context] = { unoptimized: true }.merge(options[:context] || {})
|
|
54
68
|
end
|
|
55
69
|
|
|
@@ -110,9 +124,88 @@ module ForestLiana
|
|
|
110
124
|
render serializer: nil, json: { meta: { count: 'deactivated'} }
|
|
111
125
|
end
|
|
112
126
|
|
|
127
|
+
def count_deactivated?(model)
|
|
128
|
+
name = ForestLiana.name_for(model)
|
|
129
|
+
ForestLiana.apimap.find { |collection| collection.name.to_s == name }&.is_countable == false
|
|
130
|
+
end
|
|
131
|
+
|
|
113
132
|
private
|
|
114
133
|
|
|
134
|
+
# NOTICE: What survives becomes(), which allocates a fresh instance and copies @attributes
|
|
135
|
+
# onto it and nothing else. Shared by both controllers' get_record, unlike the rest of
|
|
136
|
+
# their rendering, because getting it wrong is a correctness bug rather than a
|
|
137
|
+
# difference of route.
|
|
138
|
+
#
|
|
139
|
+
# An association proxy is bound to the reflection of the class that built it, and
|
|
140
|
+
# association(name) hands back whatever sits under that key without re-checking it
|
|
141
|
+
# against the new class. An STI subclass is free to redefine a same-named association
|
|
142
|
+
# with its own class_name, scope or foreign_key, so copying such an entry wholesale
|
|
143
|
+
# would serve the subclass's target under the base class's field — plausible, wrong,
|
|
144
|
+
# and raising nothing. Only entries both classes reflect identically are carried over;
|
|
145
|
+
# anything else is left to load lazily, the behaviour it had before this existed.
|
|
146
|
+
def carry_preloaded_associations(record, became)
|
|
147
|
+
cache = record.instance_variable_get(:@association_cache) || {}
|
|
148
|
+
carried = cache.select do |name, _|
|
|
149
|
+
record.class.reflect_on_association(name) == became.class.reflect_on_association(name)
|
|
150
|
+
end
|
|
151
|
+
became.instance_variable_set(:@association_cache, carried)
|
|
152
|
+
|
|
153
|
+
became
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# NOTICE: Header-then-query fallback, decided here and nowhere else: the header is rewritten
|
|
157
|
+
# into params[:fields], so every route keeps reading the projection it always read
|
|
158
|
+
# and the header wins over the query params without any route knowing about it.
|
|
159
|
+
#
|
|
160
|
+
# Declared as a before_action by the routes that project, and by them only: a count,
|
|
161
|
+
# a stats or a write carries no projection, must not have its params rewritten by a
|
|
162
|
+
# header meant for the list next to it, and must not answer a 400 for one. The same
|
|
163
|
+
# rule as agent-nodejs, whose own suite pins the header ignored on count.
|
|
164
|
+
def apply_projection_header
|
|
165
|
+
header = request.headers[ForestLiana::ProjectionParser::HEADER_NAME]
|
|
166
|
+
return if header.nil?
|
|
167
|
+
|
|
168
|
+
root_model = projection_root_model
|
|
169
|
+
# NOTICE: An unresolvable collection is left to the route, which answers its own 404.
|
|
170
|
+
return if root_model.nil?
|
|
171
|
+
|
|
172
|
+
fields = ForestLiana::ProjectionParser.new(header, ForestLiana.name_for(root_model)).perform
|
|
173
|
+
params[:fields] = ActionController::Parameters.new(fields)
|
|
174
|
+
rescue ForestLiana::Errors::ExpectedError
|
|
175
|
+
# NOTICE: The deliberate 400 on a malformed header, which render_error answers.
|
|
176
|
+
raise
|
|
177
|
+
rescue => error
|
|
178
|
+
FOREST_REPORTER.report error
|
|
179
|
+
FOREST_LOGGER.error "Forest-Projection header error: #{error}\n#{format_stacktrace(error)}"
|
|
180
|
+
internal_server_error
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# NOTICE: A projection is rooted on the collection the records come from, which is the
|
|
184
|
+
# association target on the relationships routes, not the collection in the URL.
|
|
185
|
+
def projection_root_model
|
|
186
|
+
return nil if params[:collection].blank?
|
|
187
|
+
|
|
188
|
+
model = ForestLiana::SchemaUtils.find_model_from_collection_name(params[:collection])
|
|
189
|
+
return nil unless model.respond_to?(:reflect_on_association)
|
|
190
|
+
return model if params[:association_name].blank?
|
|
191
|
+
|
|
192
|
+
association = model.reflect_on_association(params[:association_name].to_sym)
|
|
193
|
+
# NOTICE: klass raises on a polymorphic reflection, and this runs in a before_action: the
|
|
194
|
+
# guard is what keeps a 500 from replacing the 404 the route answers on its own. A
|
|
195
|
+
# relationships index rejects a belongs_to, which a polymorphic relation always is.
|
|
196
|
+
return nil if association.nil? || ForestLiana::SchemaUtils.polymorphic?(association)
|
|
197
|
+
|
|
198
|
+
association.klass
|
|
199
|
+
end
|
|
200
|
+
|
|
115
201
|
def render_error(exception)
|
|
202
|
+
# Same rule as render_expected_error, for the errors that reach here through a rescue_from
|
|
203
|
+
# instead: a 5xx is a server-side failure, reported and logged as one.
|
|
204
|
+
if exception.error_code.to_i >= 500
|
|
205
|
+
FOREST_REPORTER.report exception
|
|
206
|
+
ForestLiana::Errors::ExceptionHelper.recursively_print(exception, is_error: true)
|
|
207
|
+
end
|
|
208
|
+
|
|
116
209
|
errors = {
|
|
117
210
|
status: exception.error_code,
|
|
118
211
|
detail: exception.message,
|
|
@@ -207,14 +300,27 @@ module ForestLiana
|
|
|
207
300
|
end
|
|
208
301
|
|
|
209
302
|
def render_csv getter, model
|
|
303
|
+
# Unlike index/show/update, an unreadable column here is dropped rather than refusing the
|
|
304
|
+
# whole export (named_collections: [] — nothing is ever "named") — matching agent-nodejs's
|
|
305
|
+
# own CSV route, which runs the same redactProjection as its JSON list but drops a denied
|
|
306
|
+
# column instead of 403ing the file whenever it doesn't error there either.
|
|
307
|
+
requested_fields = fields_per_model(params[:fields], model)
|
|
308
|
+
fields_to_serialize = redact_fields(forest_user, model, requested_fields, named_collections: [])
|
|
309
|
+
|
|
210
310
|
set_headers_file
|
|
211
311
|
set_headers_streaming
|
|
212
312
|
|
|
213
313
|
response.status = 200
|
|
214
|
-
csv_header = params[:header].split(',')
|
|
215
314
|
collection_name = ForestLiana.name_for(model)
|
|
216
|
-
|
|
217
|
-
|
|
315
|
+
requested_field_names = params[:fields][collection_name].split(',').map { |name| name.to_s }
|
|
316
|
+
requested_header = params[:header].split(',')
|
|
317
|
+
|
|
318
|
+
# Keep the header row aligned with whatever redact_fields left in fields_to_serialize,
|
|
319
|
+
# the same way agent-nodejs's CsvGenerator.filterHeader drops a redacted column's label.
|
|
320
|
+
surviving = (fields_to_serialize[collection_name] || '').split(',')
|
|
321
|
+
kept = requested_field_names.zip(requested_header).select { |name, _| surviving.include?(name) }
|
|
322
|
+
field_names_requested = kept.map(&:first)
|
|
323
|
+
csv_header = kept.map(&:last)
|
|
218
324
|
|
|
219
325
|
self.response_body = Enumerator.new do |content|
|
|
220
326
|
content << ::CSV::Row.new(field_names_requested, csv_header, true).to_s
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
module ForestLiana
|
|
2
2
|
class AssociationsController < ForestLiana::ApplicationController
|
|
3
|
+
include ForestLiana::Ability
|
|
4
|
+
|
|
5
|
+
# NOTICE: index covers the relationship list and its CSV export. count answers no
|
|
6
|
+
# projection, and update/associate/dissociate none either.
|
|
3
7
|
if Rails::VERSION::MAJOR < 4
|
|
4
8
|
before_filter :find_resource, except: :count
|
|
5
9
|
before_filter :find_association, except: :count
|
|
10
|
+
before_filter :apply_projection_header, only: :index
|
|
6
11
|
else
|
|
7
12
|
before_action :find_resource, except: :count
|
|
8
13
|
before_action :find_association, except: :count
|
|
14
|
+
before_action :apply_projection_header, only: :index
|
|
9
15
|
end
|
|
10
16
|
|
|
11
17
|
def index
|
|
18
|
+
# Parity with agent-nodejs's list-related route: browse/export on the foreign collection.
|
|
19
|
+
# Authorized outside the begin, like update/associate/dissociate, so a denial reaches
|
|
20
|
+
# ApplicationController's rescue_from and keeps its name/data instead of falling into the
|
|
21
|
+
# generic ExpectedError rescue below.
|
|
22
|
+
action = request.format == 'csv' ? 'export' : 'browse'
|
|
23
|
+
forest_authorize!(action, forest_user, @association.klass)
|
|
12
24
|
begin
|
|
13
25
|
getter = HasManyGetter.new(@resource, @association, params, forest_user)
|
|
14
26
|
getter.perform
|
|
@@ -17,6 +29,25 @@ module ForestLiana
|
|
|
17
29
|
format.json { render_jsonapi(getter) }
|
|
18
30
|
format.csv { render_csv(getter, @association.klass) }
|
|
19
31
|
end
|
|
32
|
+
rescue ForestLiana::Ability::Exceptions::UnauthorizedFieldsError,
|
|
33
|
+
ForestLiana::Ability::Exceptions::UnauthorizedQueryFieldError => error
|
|
34
|
+
# A CSV request already has its response Content-Type/Content-Disposition set by the
|
|
35
|
+
# respond_to format match before this rescue ever runs — force JSON back, or the client
|
|
36
|
+
# downloads a ".csv" file whose content is this JSON error. UnauthorizedQueryFieldError is
|
|
37
|
+
# raised earlier (before respond_to picks a format) and doesn't strictly need this fix, but
|
|
38
|
+
# is caught here anyway since UnauthorizedFieldsError (raised during serialization, after
|
|
39
|
+
# the format is already picked) does — same response shape either way.
|
|
40
|
+
render(serializer: nil, json: { errors: [{
|
|
41
|
+
status: error.error_code,
|
|
42
|
+
detail: error.message,
|
|
43
|
+
name: error.name,
|
|
44
|
+
data: error.data
|
|
45
|
+
}] }, status: error.status, content_type: 'application/json')
|
|
46
|
+
response.headers.delete('Content-Disposition')
|
|
47
|
+
rescue *QUERY_PERMISSION_ERRORS
|
|
48
|
+
raise
|
|
49
|
+
rescue ForestLiana::Errors::ExpectedError => error
|
|
50
|
+
render_expected_error(error)
|
|
20
51
|
rescue => error
|
|
21
52
|
FOREST_REPORTER.report error
|
|
22
53
|
FOREST_LOGGER.error "Association Index error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -32,11 +63,23 @@ module ForestLiana
|
|
|
32
63
|
# through and dereferencing a nil @association, which surfaced as
|
|
33
64
|
# a double-render / 500 rather than the intended 404.
|
|
34
65
|
return if performed?
|
|
66
|
+
# Authorized outside the begin, like index above, so a denial keeps its name/data. A
|
|
67
|
+
# polymorphic @association already raises here (Rails: "Polymorphic associations do not
|
|
68
|
+
# support computing the class") — pre-existing, and shared with associate/dissociate below
|
|
69
|
+
# (index and update never reach it: both are structurally limited to a non-polymorphic
|
|
70
|
+
# macro before ever calling .klass).
|
|
71
|
+
forest_authorize!('browse', forest_user, @association.klass)
|
|
72
|
+
return deactivate_count_response if count_deactivated?(@association.klass)
|
|
73
|
+
|
|
35
74
|
begin
|
|
36
75
|
getter = HasManyGetter.new(@resource, @association, params, forest_user)
|
|
37
76
|
getter.count
|
|
38
77
|
|
|
39
78
|
render serializer: nil, json: { count: getter.records_count }
|
|
79
|
+
rescue *QUERY_PERMISSION_ERRORS
|
|
80
|
+
raise
|
|
81
|
+
rescue ForestLiana::Errors::ExpectedError => error
|
|
82
|
+
render_expected_error(error)
|
|
40
83
|
rescue => error
|
|
41
84
|
FOREST_REPORTER.report error
|
|
42
85
|
FOREST_LOGGER.error "Association Index Count error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -45,6 +88,12 @@ module ForestLiana
|
|
|
45
88
|
end
|
|
46
89
|
|
|
47
90
|
def update
|
|
91
|
+
# BelongsToUpdater's writer saves the FK on the target for a has_one, on @resource only
|
|
92
|
+
# for a belongsTo. Authorized outside the begin, like ResourcesController's own actions, so
|
|
93
|
+
# a denial reaches ApplicationController's rescue_from and keeps its name/data.
|
|
94
|
+
edit_subject = @association.macro == :has_one ? @association.klass : @resource
|
|
95
|
+
forest_authorize!('edit', forest_user, edit_subject)
|
|
96
|
+
forest_authorize!('delete', forest_user, @association.klass) if BelongsToUpdater.replaces_destructively?(@association)
|
|
48
97
|
begin
|
|
49
98
|
updater = BelongsToUpdater.new(@resource, @association, params)
|
|
50
99
|
updater.perform
|
|
@@ -55,6 +104,8 @@ module ForestLiana
|
|
|
55
104
|
else
|
|
56
105
|
head :no_content
|
|
57
106
|
end
|
|
107
|
+
rescue ForestLiana::Errors::ExpectedError => error
|
|
108
|
+
render_expected_error(error)
|
|
58
109
|
rescue => error
|
|
59
110
|
FOREST_REPORTER.report error
|
|
60
111
|
FOREST_LOGGER.error "Association Update error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -63,11 +114,14 @@ module ForestLiana
|
|
|
63
114
|
end
|
|
64
115
|
|
|
65
116
|
def associate
|
|
117
|
+
forest_authorize!('edit', forest_user, HasManyAssociator.authorize_target(@association))
|
|
66
118
|
begin
|
|
67
119
|
associator = HasManyAssociator.new(@resource, @association, params)
|
|
68
120
|
associator.perform
|
|
69
121
|
|
|
70
122
|
head :no_content
|
|
123
|
+
rescue ForestLiana::Errors::ExpectedError => error
|
|
124
|
+
render_expected_error(error)
|
|
71
125
|
rescue => error
|
|
72
126
|
FOREST_REPORTER.report error
|
|
73
127
|
FOREST_LOGGER.error "Association Associate error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -76,6 +130,15 @@ module ForestLiana
|
|
|
76
130
|
end
|
|
77
131
|
|
|
78
132
|
def dissociate
|
|
133
|
+
if params[:delete].to_s == 'true'
|
|
134
|
+
# Explicit delete destroys the far record directly, regardless of association shape.
|
|
135
|
+
action = 'delete'
|
|
136
|
+
authorize_target = @association.klass
|
|
137
|
+
else
|
|
138
|
+
action = HasManyDissociator.destroys_on_unlink?(@association) ? 'delete' : 'edit'
|
|
139
|
+
authorize_target = HasManyDissociator.destroy_target(@association)
|
|
140
|
+
end
|
|
141
|
+
forest_authorize!(action, forest_user, authorize_target)
|
|
79
142
|
begin
|
|
80
143
|
dissociator = HasManyDissociator.new(@resource, @association, params, forest_user)
|
|
81
144
|
dissociator.perform
|
|
@@ -83,6 +146,10 @@ module ForestLiana
|
|
|
83
146
|
head :no_content
|
|
84
147
|
rescue ActiveRecord::RecordNotDestroyed => error
|
|
85
148
|
render json: { errors: [{ status: :bad_request, detail: error.message }] }, status: :bad_request
|
|
149
|
+
rescue *QUERY_PERMISSION_ERRORS
|
|
150
|
+
raise
|
|
151
|
+
rescue ForestLiana::Errors::ExpectedError => error
|
|
152
|
+
render_expected_error(error)
|
|
86
153
|
rescue => error
|
|
87
154
|
FOREST_REPORTER.report error
|
|
88
155
|
FOREST_LOGGER.error "Association Dissociate error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -122,20 +189,33 @@ module ForestLiana
|
|
|
122
189
|
@association.klass.columns.any? { |column| column.name == @association.klass.inheritance_column })
|
|
123
190
|
end
|
|
124
191
|
|
|
192
|
+
# See ResourcesController#get_record — becomes() drops the association cache and the
|
|
193
|
+
# polymorphic singleton readers, and with them everything the getter preloaded for this page.
|
|
125
194
|
def get_record record
|
|
126
|
-
|
|
195
|
+
return record unless is_sti_model?
|
|
196
|
+
|
|
197
|
+
carry_preloaded_associations(record, record.becomes(@association.klass))
|
|
127
198
|
end
|
|
128
199
|
|
|
129
200
|
def render_jsonapi getter
|
|
130
|
-
fields_to_serialize = fields_per_model(params[:fields], @association.klass)
|
|
131
|
-
records = getter.records.map { |record| get_record(record) }
|
|
132
|
-
|
|
133
201
|
includes = getter.includes_for_serialization
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
202
|
+
requested_fields = fields_per_model(params[:fields], @association.klass)
|
|
203
|
+
|
|
204
|
+
# The getter may include a relation the caller's own field list omitted (search decoration,
|
|
205
|
+
# scoped includes); splice it in before redaction runs, so it is checked like any other field
|
|
206
|
+
# instead of bypassing the check entirely by being added back afterwards.
|
|
207
|
+
association_name = ForestLiana.name_for(@association.klass)
|
|
208
|
+
if requested_fields && includes.length > 0 && requested_fields[association_name]
|
|
209
|
+
requested_fields[association_name] += ",#{includes.join(',')}"
|
|
137
210
|
end
|
|
138
211
|
|
|
212
|
+
fields_to_serialize = requested_fields || default_fields_to_serialize(@association.klass, includes)
|
|
213
|
+
fields_to_serialize = redact_fields(
|
|
214
|
+
forest_user, @association.klass, fields_to_serialize,
|
|
215
|
+
named_collections: requested_fields ? requested_fields.keys : []
|
|
216
|
+
)
|
|
217
|
+
records = getter.records.map { |record| get_record(record) }
|
|
218
|
+
|
|
139
219
|
json = serialize_models(
|
|
140
220
|
records,
|
|
141
221
|
{
|
|
@@ -154,5 +234,26 @@ module ForestLiana
|
|
|
154
234
|
collection_name = ForestLiana.name_for(model_association)
|
|
155
235
|
@collection ||= ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
|
|
156
236
|
end
|
|
237
|
+
|
|
238
|
+
# See ResourcesController#default_fields_to_serialize for the rationale; duplicated rather than
|
|
239
|
+
# shared, matching how this controller already keeps its own render_jsonapi/get_record instead
|
|
240
|
+
# of reusing ResourcesController's.
|
|
241
|
+
def default_fields_to_serialize(root_model, included_relation_names)
|
|
242
|
+
fields = { ForestLiana.name_for(root_model) => collection_field_names(root_model) }
|
|
243
|
+
|
|
244
|
+
Array(included_relation_names).each do |relation_name|
|
|
245
|
+
reflection = root_model.reflect_on_association(relation_name.to_sym)
|
|
246
|
+
next if reflection.nil? || reflection.polymorphic?
|
|
247
|
+
|
|
248
|
+
related_name = ForestLiana.name_for(reflection.klass)
|
|
249
|
+
fields[related_name] ||= collection_field_names(reflection.klass)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
fields
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def collection_field_names(model)
|
|
256
|
+
ForestLiana::SchemaHelper.find_collection_from_model(model).fields.map { |field| field[:field] }.join(',')
|
|
257
|
+
end
|
|
157
258
|
end
|
|
158
259
|
end
|
|
@@ -10,6 +10,29 @@ module ForestLiana
|
|
|
10
10
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
|
+
# The one shape for an expected error an action renders itself, rather than handing back to
|
|
14
|
+
# ApplicationController's rescue_from. A 5xx among them is a server-side failure wearing an
|
|
15
|
+
# expected-error class — a permissions fetch that could not be answered, an unimplemented
|
|
16
|
+
# method — so it is reported and logged as an error like any other 500 would be. Without that
|
|
17
|
+
# it reads as a client error, and the incident reaches nobody.
|
|
18
|
+
def render_expected_error(error)
|
|
19
|
+
if error.error_code.to_i >= 500
|
|
20
|
+
FOREST_REPORTER.report error
|
|
21
|
+
ForestLiana::Errors::ExceptionHelper.recursively_print(error, is_error: true)
|
|
22
|
+
else
|
|
23
|
+
error.display_error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# name/data carried like render_error does, when the class sets them: it is what tells a
|
|
27
|
+
# PermissionsUnavailableError from the RBAC refusals sharing its shape, on the client and in
|
|
28
|
+
# a support thread alike. Absent on the classes that set neither, exactly as before.
|
|
29
|
+
payload = { status: error.error_code, detail: error.message }
|
|
30
|
+
payload[:name] = error.name if error.try(:name)
|
|
31
|
+
payload[:data] = error.data if error.try(:data)
|
|
32
|
+
|
|
33
|
+
render(serializer: nil, json: ForestAdmin::JSONAPI::Serializer.serialize_errors([payload]), status: error.status)
|
|
34
|
+
end
|
|
35
|
+
|
|
13
36
|
def reject_unauthorized_ip
|
|
14
37
|
begin
|
|
15
38
|
ip = request.remote_ip
|
|
@@ -24,11 +47,7 @@ module ForestLiana
|
|
|
24
47
|
end
|
|
25
48
|
end
|
|
26
49
|
rescue ForestLiana::Errors::ExpectedError => exception
|
|
27
|
-
|
|
28
|
-
status: exception.error_code,
|
|
29
|
-
detail: exception.message
|
|
30
|
-
}])
|
|
31
|
-
render(serializer: nil, json: error_data, status: exception.status)
|
|
50
|
+
render_expected_error(exception)
|
|
32
51
|
rescue => exception
|
|
33
52
|
FOREST_REPORTER.report exception
|
|
34
53
|
FOREST_LOGGER.error(exception)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module ForestLiana
|
|
2
|
+
class CapabilitiesController < ForestLiana::ApplicationController
|
|
3
|
+
def index
|
|
4
|
+
capabilities = ForestLiana::CapabilitiesGetter.new(params[:collectionNames]).perform
|
|
5
|
+
|
|
6
|
+
render serializer: nil, json: capabilities, status: :ok
|
|
7
|
+
rescue => error
|
|
8
|
+
FOREST_REPORTER.report error
|
|
9
|
+
FOREST_LOGGER.error "Error while getting the capabilities: #{error.message}"
|
|
10
|
+
internal_server_error
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -8,10 +8,14 @@ module ForestLiana
|
|
|
8
8
|
|
|
9
9
|
rescue_from ActiveRecord::RecordNotFound, :with => :record_not_found
|
|
10
10
|
|
|
11
|
+
# NOTICE: index covers the list and the CSV export, show the get-one. Every other action
|
|
12
|
+
# here answers no projection, count included.
|
|
11
13
|
if Rails::VERSION::MAJOR < 4
|
|
12
14
|
before_filter :find_resource, except: :count
|
|
15
|
+
before_filter :apply_projection_header, only: [:index, :show]
|
|
13
16
|
else
|
|
14
17
|
before_action :find_resource, except: :count
|
|
18
|
+
before_action :apply_projection_header, only: [:index, :show]
|
|
15
19
|
end
|
|
16
20
|
|
|
17
21
|
def index
|
|
@@ -27,13 +31,25 @@ module ForestLiana
|
|
|
27
31
|
rescue ForestLiana::Errors::LiveQueryError => error
|
|
28
32
|
render json: { errors: [{ status: 422, detail: error.message }] },
|
|
29
33
|
status: :unprocessable_entity, serializer: nil
|
|
30
|
-
rescue ForestLiana::
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
rescue ForestLiana::Ability::Exceptions::UnauthorizedFieldsError,
|
|
35
|
+
ForestLiana::Ability::Exceptions::UnauthorizedQueryFieldError => error
|
|
36
|
+
# A CSV request already has its response Content-Type/Content-Disposition set by the
|
|
37
|
+
# respond_to format match before this rescue ever runs — force JSON back, or the client
|
|
38
|
+
# downloads a ".csv" file whose content is this JSON error. UnauthorizedQueryFieldError is
|
|
39
|
+
# raised earlier (before respond_to picks a format) and doesn't strictly need this fix, but
|
|
40
|
+
# is caught here anyway since UnauthorizedFieldsError (raised during serialization, after
|
|
41
|
+
# the format is already picked) does — same response shape either way.
|
|
42
|
+
render(serializer: nil, json: { errors: [{
|
|
33
43
|
status: error.error_code,
|
|
34
|
-
detail: error.message
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
detail: error.message,
|
|
45
|
+
name: error.name,
|
|
46
|
+
data: error.data
|
|
47
|
+
}] }, status: error.status, content_type: 'application/json')
|
|
48
|
+
response.headers.delete('Content-Disposition')
|
|
49
|
+
rescue *QUERY_PERMISSION_ERRORS
|
|
50
|
+
raise
|
|
51
|
+
rescue ForestLiana::Errors::ExpectedError => error
|
|
52
|
+
render_expected_error(error)
|
|
37
53
|
rescue => error
|
|
38
54
|
FOREST_REPORTER.report error
|
|
39
55
|
FOREST_LOGGER.error "Records Index error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -44,6 +60,8 @@ module ForestLiana
|
|
|
44
60
|
def count
|
|
45
61
|
find_resource
|
|
46
62
|
forest_authorize!('browse', forest_user, @resource)
|
|
63
|
+
return deactivate_count_response if count_deactivated?(@resource)
|
|
64
|
+
|
|
47
65
|
begin
|
|
48
66
|
getter = ForestLiana::ResourcesGetter.new(@resource, params, forest_user)
|
|
49
67
|
getter.count
|
|
@@ -53,13 +71,10 @@ module ForestLiana
|
|
|
53
71
|
rescue ForestLiana::Errors::LiveQueryError => error
|
|
54
72
|
render json: { errors: [{ status: 422, detail: error.message }] },
|
|
55
73
|
status: :unprocessable_entity, serializer: nil
|
|
74
|
+
rescue *QUERY_PERMISSION_ERRORS
|
|
75
|
+
raise
|
|
56
76
|
rescue ForestLiana::Errors::ExpectedError => error
|
|
57
|
-
error
|
|
58
|
-
error_data = ForestAdmin::JSONAPI::Serializer.serialize_errors([{
|
|
59
|
-
status: error.error_code,
|
|
60
|
-
detail: error.message
|
|
61
|
-
}])
|
|
62
|
-
render(serializer: nil, json: error_data, status: error.status)
|
|
77
|
+
render_expected_error(error)
|
|
63
78
|
rescue => error
|
|
64
79
|
FOREST_REPORTER.report error
|
|
65
80
|
FOREST_LOGGER.error "Records Index Count error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -73,9 +88,18 @@ module ForestLiana
|
|
|
73
88
|
getter = ForestLiana::ResourceGetter.new(@resource, params, forest_user)
|
|
74
89
|
getter.perform
|
|
75
90
|
|
|
76
|
-
render serializer: nil, json: render_record_jsonapi(getter.record)
|
|
91
|
+
render serializer: nil, json: render_record_jsonapi(getter.record, getter)
|
|
77
92
|
rescue ActiveRecord::RecordNotFound
|
|
78
93
|
render serializer: nil, json: { status: 404 }, status: :not_found
|
|
94
|
+
rescue *QUERY_PERMISSION_ERRORS
|
|
95
|
+
raise
|
|
96
|
+
rescue ForestLiana::Errors::ExpectedError => error
|
|
97
|
+
# Same shape as #index and #count: the permission errors above go back to
|
|
98
|
+
# ApplicationController's rescue_from for their name/data, every other expected error is
|
|
99
|
+
# rendered here with its own status (a 422 on a malformed field path) rather than
|
|
100
|
+
# re-raised. Re-raising the whole hierarchy would let the subclasses no rescue_from
|
|
101
|
+
# covers escape the controller entirely — no Forest error payload, and no report.
|
|
102
|
+
render_expected_error(error)
|
|
79
103
|
rescue => error
|
|
80
104
|
FOREST_REPORTER.report error
|
|
81
105
|
FOREST_LOGGER.error "Record Show error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -162,6 +186,10 @@ module ForestLiana
|
|
|
162
186
|
end
|
|
163
187
|
rescue ActiveRecord::RecordNotDestroyed => error
|
|
164
188
|
render json: { errors: [{ status: :bad_request, detail: error.message }] }, status: :bad_request
|
|
189
|
+
rescue *QUERY_PERMISSION_ERRORS
|
|
190
|
+
raise
|
|
191
|
+
rescue ForestLiana::Errors::ExpectedError => error
|
|
192
|
+
render_expected_error(error)
|
|
165
193
|
rescue => error
|
|
166
194
|
FOREST_REPORTER.report error
|
|
167
195
|
FOREST_LOGGER.error "Records Destroy error: #{error}\n#{format_stacktrace(error)}"
|
|
@@ -203,26 +231,88 @@ module ForestLiana
|
|
|
203
231
|
@resource.columns.any? { |column| column.name == @resource.inheritance_column })
|
|
204
232
|
end
|
|
205
233
|
|
|
234
|
+
# NOTICE: becomes() allocates a fresh instance and copies @attributes onto it — not the
|
|
235
|
+
# association cache, which init_internals has just emptied. What the getters
|
|
236
|
+
# preloaded (smart_field_preloads, and the relations eager loaded for serialization)
|
|
237
|
+
# would be dropped here and re-queried once per row, so an STI collection alone would
|
|
238
|
+
# keep the N+1 the rest of this pipeline exists to remove.
|
|
239
|
+
#
|
|
240
|
+
# The singleton readers preload_polymorphic_associations defines are lost too — they
|
|
241
|
+
# belong to the old instance's singleton class — but their targets are not: Rails'
|
|
242
|
+
# own Preloader writes a polymorphic target into the association cache as well, so
|
|
243
|
+
# those ride along with everything else. That is what preload_polymorphic_associations
|
|
244
|
+
# calls #call for on Rails 7+. Pinned by sti_record_conversion_spec.rb.
|
|
245
|
+
#
|
|
246
|
+
# Carried over explicitly, and only where the two classes agree on what the name
|
|
247
|
+
# means — see carry_preloaded_associations.
|
|
206
248
|
def get_record record
|
|
207
|
-
|
|
249
|
+
return record unless is_sti_model?
|
|
250
|
+
|
|
251
|
+
carry_preloaded_associations(record, record.becomes(@resource))
|
|
208
252
|
end
|
|
209
253
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
254
|
+
# NOTICE: create and update answer with the whole record: they carry no projection, so the
|
|
255
|
+
# getter is absent and every field is part of the default expansion, not something the
|
|
256
|
+
# caller named. show passes a getter; a field named through the projection header is
|
|
257
|
+
# refused rather than redacted if unreadable, same as index's own render_jsonapi below.
|
|
258
|
+
def render_record_jsonapi record, getter = nil
|
|
259
|
+
requested_fields = getter&.projection? ? fields_per_model(params[:fields], @resource) : nil
|
|
260
|
+
fields_to_serialize = redact_fields(
|
|
261
|
+
forest_user, @resource,
|
|
262
|
+
requested_fields || default_fields_to_serialize(@resource, record_includes),
|
|
263
|
+
named_collections: requested_fields ? requested_fields.keys : []
|
|
264
|
+
)
|
|
265
|
+
fields_to_serialize = keep_relationship_links(fields_to_serialize) if requested_fields
|
|
216
266
|
|
|
217
267
|
serialize_model(get_record(record), {
|
|
218
|
-
include: record_includes,
|
|
268
|
+
include: requested_fields ? getter.includes_for_serialization : record_includes,
|
|
219
269
|
fields: fields_to_serialize
|
|
220
270
|
})
|
|
221
271
|
end
|
|
222
272
|
|
|
273
|
+
# NOTICE: A projection names what the caller reads off the record, never the to-many
|
|
274
|
+
# relations: the frontend does not fetch them here, it follows their
|
|
275
|
+
# `links.related` off this very payload to load each one on its own route. Left out
|
|
276
|
+
# of `fields` they are not serialized at all, the links go with them, and every
|
|
277
|
+
# related-data list on the record silently stays empty — its count, built from a
|
|
278
|
+
# hand-made URL rather than from a link, keeps answering. They cost no query: a
|
|
279
|
+
# to-many outside `include` serializes to its link and nothing else.
|
|
280
|
+
#
|
|
281
|
+
# Added back after the redaction, and without a read check on the target collection:
|
|
282
|
+
# a `links.related` is a URL, not data, and AssociationsController#index authorizes
|
|
283
|
+
# `browse` on that very collection before answering it. Checking here would buy
|
|
284
|
+
# nothing and cost a read permission lookup per to-many on every single get-one,
|
|
285
|
+
# only to turn a Forest API hiccup into a 403 on a record the role may read.
|
|
286
|
+
def keep_relationship_links(fields_to_serialize)
|
|
287
|
+
root_name = ForestLiana.name_for(@resource)
|
|
288
|
+
projected = (fields_to_serialize[root_name] || '').split(',')
|
|
289
|
+
missing = to_many_field_names - projected
|
|
290
|
+
return fields_to_serialize if missing.empty?
|
|
291
|
+
|
|
292
|
+
fields_to_serialize.merge(root_name => (projected + missing).join(','))
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# NOTICE: What every to-many has in common, whatever it is made of. Keying on
|
|
296
|
+
# `field[:relationship]` would miss two of the three: SchemaAdapter types an
|
|
297
|
+
# association by its camelized macro, so a has_and_belongs_to_many reads
|
|
298
|
+
# "HasAndBelongsToMany" rather than "HasMany", and Collection#has_many — the smart
|
|
299
|
+
# DSL — never sets the key at all. The frontend makes none of these distinctions: it
|
|
300
|
+
# reads the array type, exactly as here. `reference` is what keeps a scalar list out:
|
|
301
|
+
# an array-typed smart field references nothing, and is a computed value the caller
|
|
302
|
+
# did not ask for rather than a relation with a link (User's `nicknames`).
|
|
303
|
+
def to_many_field_names
|
|
304
|
+
get_collection.fields
|
|
305
|
+
.select { |field| field[:type].is_a?(Array) && field[:reference].present? }
|
|
306
|
+
.map { |field| field[:field].to_s }
|
|
307
|
+
end
|
|
308
|
+
|
|
223
309
|
def render_jsonapi getter
|
|
224
310
|
records = getter.records.map { |record| get_record(record) }
|
|
225
|
-
|
|
311
|
+
requested_fields = fields_per_model(params[:fields], @resource)
|
|
312
|
+
fields_to_serialize = requested_fields || default_fields_to_serialize(@resource, includes(getter))
|
|
313
|
+
fields_to_serialize = redact_fields(
|
|
314
|
+
forest_user, @resource, fields_to_serialize, named_collections: requested_fields ? requested_fields.keys : []
|
|
315
|
+
)
|
|
226
316
|
|
|
227
317
|
json = serialize_models(
|
|
228
318
|
records,
|
|
@@ -237,6 +327,29 @@ module ForestLiana
|
|
|
237
327
|
render serializer: nil, json: json
|
|
238
328
|
end
|
|
239
329
|
|
|
330
|
+
# The full-schema field list `fields[...]` expands to when the caller never sent it — mirrors
|
|
331
|
+
# `render_record_jsonapi`'s own full-collection expansion, fanned out over the relations the
|
|
332
|
+
# response actually includes. A polymorphic relation is left out of this per-collection map: it
|
|
333
|
+
# has no single target collection to expand, and it already appears in the root's own field list
|
|
334
|
+
# (below), which is what makes the relationship link itself visible.
|
|
335
|
+
def default_fields_to_serialize(root_model, included_relation_names)
|
|
336
|
+
fields = { ForestLiana.name_for(root_model) => collection_field_names(root_model) }
|
|
337
|
+
|
|
338
|
+
Array(included_relation_names).each do |relation_name|
|
|
339
|
+
reflection = root_model.reflect_on_association(relation_name.to_sym)
|
|
340
|
+
next if reflection.nil? || reflection.polymorphic?
|
|
341
|
+
|
|
342
|
+
related_name = ForestLiana.name_for(reflection.klass)
|
|
343
|
+
fields[related_name] ||= collection_field_names(reflection.klass)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
fields
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def collection_field_names(model)
|
|
350
|
+
ForestLiana::SchemaHelper.find_collection_from_model(model).fields.map { |field| field[:field] }.join(',')
|
|
351
|
+
end
|
|
352
|
+
|
|
240
353
|
def get_collection
|
|
241
354
|
collection_name = ForestLiana.name_for(@resource)
|
|
242
355
|
@collection ||= ForestLiana.apimap.find { |collection| collection.name.to_s == collection_name }
|