jpie 0.1.0 → 0.3.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/.aiconfig +65 -0
- data/.rubocop.yml +110 -35
- data/CHANGELOG.md +93 -0
- data/LICENSE.txt +21 -0
- data/README.md +776 -1903
- data/Rakefile +14 -3
- data/jpie.gemspec +35 -18
- data/lib/jpie/configuration.rb +12 -0
- data/lib/jpie/controller/crud_actions.rb +110 -0
- data/lib/jpie/controller/error_handling.rb +41 -0
- data/lib/jpie/controller/parameter_parsing.rb +35 -0
- data/lib/jpie/controller/rendering.rb +60 -0
- data/lib/jpie/controller.rb +18 -0
- data/lib/jpie/deserializer.rb +110 -0
- data/lib/jpie/errors.rb +70 -0
- data/lib/jpie/generators/resource_generator.rb +39 -0
- data/lib/jpie/generators/templates/resource.rb.erb +12 -0
- data/lib/jpie/railtie.rb +36 -0
- data/lib/jpie/resource/attributable.rb +98 -0
- data/lib/jpie/resource/inferrable.rb +43 -0
- data/lib/jpie/resource/sortable.rb +93 -0
- data/lib/jpie/resource.rb +107 -0
- data/lib/jpie/serializer.rb +205 -0
- data/lib/{json_api → jpie}/version.rb +2 -2
- data/lib/jpie.rb +23 -3
- metadata +145 -50
- data/.gitignore +0 -21
- data/.rspec +0 -3
- data/.travis.yml +0 -7
- data/Gemfile +0 -21
- data/Gemfile.lock +0 -312
- data/bin/console +0 -15
- data/bin/setup +0 -8
- data/kiln/app/resources/user_message_resource.rb +0 -2
- data/lib/json_api/active_storage/deserialization.rb +0 -106
- data/lib/json_api/active_storage/detection.rb +0 -74
- data/lib/json_api/active_storage/serialization.rb +0 -32
- data/lib/json_api/configuration.rb +0 -58
- data/lib/json_api/controllers/base_controller.rb +0 -26
- data/lib/json_api/controllers/concerns/controller_helpers.rb +0 -223
- data/lib/json_api/controllers/concerns/resource_actions.rb +0 -657
- data/lib/json_api/controllers/relationships_controller.rb +0 -504
- data/lib/json_api/controllers/resources_controller.rb +0 -6
- data/lib/json_api/errors/parameter_not_allowed.rb +0 -19
- data/lib/json_api/railtie.rb +0 -75
- data/lib/json_api/resources/active_storage_blob_resource.rb +0 -11
- data/lib/json_api/resources/resource.rb +0 -238
- data/lib/json_api/resources/resource_loader.rb +0 -35
- data/lib/json_api/routing.rb +0 -72
- data/lib/json_api/serialization/deserializer.rb +0 -362
- data/lib/json_api/serialization/serializer.rb +0 -320
- data/lib/json_api/support/active_storage_support.rb +0 -85
- data/lib/json_api/support/collection_query.rb +0 -406
- data/lib/json_api/support/instrumentation.rb +0 -42
- data/lib/json_api/support/param_helpers.rb +0 -51
- data/lib/json_api/support/relationship_guard.rb +0 -16
- data/lib/json_api/support/relationship_helpers.rb +0 -74
- data/lib/json_api/support/resource_identifier.rb +0 -87
- data/lib/json_api/support/responders.rb +0 -100
- data/lib/json_api/support/response_helpers.rb +0 -10
- data/lib/json_api/support/sort_parsing.rb +0 -21
- data/lib/json_api/support/type_conversion.rb +0 -21
- data/lib/json_api/testing/test_helper.rb +0 -76
- data/lib/json_api/testing.rb +0 -3
- data/lib/json_api.rb +0 -50
- data/lib/rubocop/cop/custom/hash_value_omission.rb +0 -53
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JSONAPI
|
|
4
|
-
class CollectionQuery
|
|
5
|
-
attr_reader :scope, :total_count, :pagination_applied
|
|
6
|
-
|
|
7
|
-
def initialize(scope, definition:, model_class:, filter_params:, sort_params:, page_params:)
|
|
8
|
-
@scope = scope
|
|
9
|
-
@definition = definition
|
|
10
|
-
@model_class = model_class
|
|
11
|
-
@filter_params = filter_params
|
|
12
|
-
@sort_params = sort_params
|
|
13
|
-
@page_params = page_params
|
|
14
|
-
@pagination_applied = page_params.present?
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def execute
|
|
18
|
-
# Apply filtering
|
|
19
|
-
@scope = apply_filtering
|
|
20
|
-
|
|
21
|
-
# Get total count before sorting (for virtual attributes, we need to load records)
|
|
22
|
-
# If we have virtual attribute sorting, we'll recalculate after sorting
|
|
23
|
-
has_virtual_sort = sort_params.any? { |sort_field| virtual_attribute_sort?(sort_field) }
|
|
24
|
-
@total_count = @scope.count unless has_virtual_sort
|
|
25
|
-
|
|
26
|
-
# Apply sorting (may convert scope to array if virtual attributes are involved)
|
|
27
|
-
@scope = apply_sorting(@scope)
|
|
28
|
-
|
|
29
|
-
# Recalculate total count if we converted to array for virtual sorting
|
|
30
|
-
@total_count = @scope.count if has_virtual_sort && @scope.is_a?(Array)
|
|
31
|
-
|
|
32
|
-
# Apply pagination
|
|
33
|
-
@scope = apply_pagination
|
|
34
|
-
|
|
35
|
-
self
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
private
|
|
39
|
-
|
|
40
|
-
attr_reader :definition, :model_class, :filter_params, :sort_params, :page_params
|
|
41
|
-
|
|
42
|
-
def apply_filtering
|
|
43
|
-
# Apply nested relationship filters first (they require joins)
|
|
44
|
-
scope = apply_nested_relationship_filters(@scope)
|
|
45
|
-
|
|
46
|
-
# Then apply regular filters (column-aware operators or model scopes)
|
|
47
|
-
apply_regular_filters(scope)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
def apply_sorting(scope)
|
|
51
|
-
return scope if sort_params.empty?
|
|
52
|
-
|
|
53
|
-
# Check if any sort fields are virtual attributes
|
|
54
|
-
has_virtual_sorts = sort_params.any? { |sort_field| virtual_attribute_sort?(sort_field) }
|
|
55
|
-
|
|
56
|
-
if has_virtual_sorts
|
|
57
|
-
# Convert to array and sort by all fields (both DB and virtual) in Ruby
|
|
58
|
-
records = scope.to_a
|
|
59
|
-
records = apply_mixed_sorting(records, sort_params)
|
|
60
|
-
scope = records
|
|
61
|
-
else
|
|
62
|
-
# All sorts are database columns - use ActiveRecord ordering
|
|
63
|
-
sort_params.each do |sort_field|
|
|
64
|
-
direction = RelationshipHelpers.extract_sort_direction(sort_field)
|
|
65
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
66
|
-
scope = scope.order(field => direction)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
scope
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def virtual_attribute_sort?(sort_field)
|
|
74
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
75
|
-
!model_class.column_names.include?(field.to_s)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def apply_mixed_sorting(records, all_sorts)
|
|
79
|
-
records.sort do |a, b|
|
|
80
|
-
compare_by_sort_criteria(a, b, all_sorts)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def compare_by_sort_criteria(record_a, record_b, all_sorts)
|
|
85
|
-
all_sorts.each do |sort_field|
|
|
86
|
-
direction = RelationshipHelpers.extract_sort_direction(sort_field)
|
|
87
|
-
field = RelationshipHelpers.extract_sort_field_name(sort_field)
|
|
88
|
-
|
|
89
|
-
# Get values - use DB column if available, otherwise use definition getter
|
|
90
|
-
value_a = fetch_sort_value(record_a, field)
|
|
91
|
-
value_b = fetch_sort_value(record_b, field)
|
|
92
|
-
|
|
93
|
-
# Compare values
|
|
94
|
-
comparison = compare_values(value_a, value_b)
|
|
95
|
-
next if comparison.zero? # Equal, check next sort field
|
|
96
|
-
|
|
97
|
-
# Apply direction
|
|
98
|
-
return direction == :desc ? -comparison : comparison
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
0 # All fields equal
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def fetch_sort_value(record, field)
|
|
105
|
-
# Try database column first
|
|
106
|
-
if model_class.column_names.include?(field.to_s)
|
|
107
|
-
# Use public_send to get the attribute value (handles both string and symbol access)
|
|
108
|
-
if record.respond_to?(field.to_sym)
|
|
109
|
-
record.public_send(field.to_sym)
|
|
110
|
-
elsif record.respond_to?(:attributes) && record.attributes.is_a?(Hash)
|
|
111
|
-
record.attributes[field.to_s] || record.attributes[field.to_sym]
|
|
112
|
-
end
|
|
113
|
-
else
|
|
114
|
-
# Use definition getter for virtual attributes
|
|
115
|
-
definition_instance = definition.new(record, {})
|
|
116
|
-
fetch_virtual_value(definition_instance, field)
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def fetch_virtual_value(definition_instance, field)
|
|
121
|
-
field_sym = field.to_sym
|
|
122
|
-
return definition_instance.public_send(field_sym) if definition_instance.respond_to?(field_sym, false)
|
|
123
|
-
|
|
124
|
-
nil
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def compare_values(value_a, value_b)
|
|
128
|
-
# Handle nil values
|
|
129
|
-
return 0 if value_a.nil? && value_b.nil?
|
|
130
|
-
return -1 if value_a.nil?
|
|
131
|
-
return 1 if value_b.nil?
|
|
132
|
-
|
|
133
|
-
# Compare using standard Ruby comparison
|
|
134
|
-
value_a <=> value_b
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def apply_regular_filters(scope)
|
|
138
|
-
return scope if filter_params.empty?
|
|
139
|
-
|
|
140
|
-
# Exclude nested relationship filters (already applied)
|
|
141
|
-
regular_filters = filter_params.reject { |k, _v| k.to_s.include?(".") }
|
|
142
|
-
return scope if regular_filters.empty?
|
|
143
|
-
|
|
144
|
-
regular_filters.reduce(scope) do |current_scope, (filter_name, filter_value)|
|
|
145
|
-
apply_regular_filter(current_scope, filter_name.to_s, filter_value)
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def apply_regular_filter(scope, filter_name, filter_value)
|
|
150
|
-
return scope if filter_value.respond_to?(:empty?) ? filter_value.empty? : filter_value.nil?
|
|
151
|
-
|
|
152
|
-
column_filter = parse_column_filter(filter_name)
|
|
153
|
-
if column_filter
|
|
154
|
-
apply_column_filter(scope, column_filter, filter_value)
|
|
155
|
-
else
|
|
156
|
-
apply_scope_fallback(scope, filter_name, filter_value)
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def parse_column_filter(filter_name)
|
|
161
|
-
match = filter_name.match(/\A(.+)_(eq|match|lt|lte|gt|gte)\z/)
|
|
162
|
-
return nil unless match
|
|
163
|
-
|
|
164
|
-
column_name = match[1]
|
|
165
|
-
operator = match[2].to_sym
|
|
166
|
-
|
|
167
|
-
{ column: column_name, operator: }
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def apply_column_filter(scope, column_filter, raw_value)
|
|
171
|
-
column_name = column_filter[:column]
|
|
172
|
-
operator = column_filter[:operator]
|
|
173
|
-
|
|
174
|
-
column = model_class.column_for_attribute(column_name)
|
|
175
|
-
return scope unless column
|
|
176
|
-
|
|
177
|
-
value = normalize_filter_value(column, raw_value)
|
|
178
|
-
return scope if value.nil?
|
|
179
|
-
|
|
180
|
-
condition = build_condition(column, value, operator)
|
|
181
|
-
return scope unless condition
|
|
182
|
-
|
|
183
|
-
apply_condition(scope, condition)
|
|
184
|
-
rescue ArgumentError, TypeError, StandardError => e
|
|
185
|
-
# If casting fails or any other error occurs, skip filter
|
|
186
|
-
Rails.logger.warn("Filter error for #{column_filter[:column]}_#{operator}: #{e.class} - #{e.message}") if defined?(Rails.logger)
|
|
187
|
-
scope
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
def normalize_filter_value(column, raw_value)
|
|
191
|
-
value = raw_value.is_a?(Array) ? raw_value.first : raw_value
|
|
192
|
-
return nil if value.nil?
|
|
193
|
-
|
|
194
|
-
type = model_class.type_for_attribute(column.name)
|
|
195
|
-
type.cast(value)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def build_condition(column, value, operator)
|
|
199
|
-
attr = model_class.arel_table[column.name]
|
|
200
|
-
|
|
201
|
-
case operator
|
|
202
|
-
when :eq
|
|
203
|
-
attr.eq(value)
|
|
204
|
-
when :lt
|
|
205
|
-
attr.lt(value)
|
|
206
|
-
when :lte
|
|
207
|
-
attr.lteq(value)
|
|
208
|
-
when :gt
|
|
209
|
-
attr.gt(value)
|
|
210
|
-
when :gte
|
|
211
|
-
attr.gteq(value)
|
|
212
|
-
when :match
|
|
213
|
-
pattern = "%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"
|
|
214
|
-
lower_attr = Arel::Nodes::NamedFunction.new("LOWER", [attr])
|
|
215
|
-
lower_attr.matches(pattern.downcase)
|
|
216
|
-
end
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
def normalize_filter_value_for_model(model, column, raw_value)
|
|
220
|
-
return nil unless column
|
|
221
|
-
|
|
222
|
-
value = raw_value.is_a?(Array) ? raw_value.first : raw_value
|
|
223
|
-
return nil if value.nil?
|
|
224
|
-
|
|
225
|
-
type = model.type_for_attribute(column.name)
|
|
226
|
-
type.cast(value)
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
def build_condition_for_model(model, column, value, operator)
|
|
230
|
-
attr = model.arel_table[column.name]
|
|
231
|
-
|
|
232
|
-
case operator
|
|
233
|
-
when :eq
|
|
234
|
-
attr.eq(value)
|
|
235
|
-
when :lt
|
|
236
|
-
attr.lt(value)
|
|
237
|
-
when :lte
|
|
238
|
-
attr.lteq(value)
|
|
239
|
-
when :gt
|
|
240
|
-
attr.gt(value)
|
|
241
|
-
when :gte
|
|
242
|
-
attr.gteq(value)
|
|
243
|
-
when :match
|
|
244
|
-
pattern = "%#{ActiveRecord::Base.sanitize_sql_like(value.to_s)}%"
|
|
245
|
-
lower_attr = Arel::Nodes::NamedFunction.new("LOWER", [attr])
|
|
246
|
-
lower_attr.matches(pattern.downcase)
|
|
247
|
-
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def apply_condition(scope, condition)
|
|
251
|
-
scope.where(condition)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def apply_scope_fallback(scope, filter_name, filter_value)
|
|
255
|
-
return scope unless model_class.respond_to?(filter_name.to_sym)
|
|
256
|
-
|
|
257
|
-
scope.public_send(filter_name.to_sym, filter_value)
|
|
258
|
-
rescue ArgumentError, NoMethodError
|
|
259
|
-
scope
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def apply_nested_relationship_filters(scope)
|
|
263
|
-
return scope if filter_params.empty?
|
|
264
|
-
|
|
265
|
-
nested_filters = filter_params.select { |k, _v| k.to_s.include?(".") }
|
|
266
|
-
return scope if nested_filters.empty?
|
|
267
|
-
|
|
268
|
-
nested_filters.reduce(scope) do |current_scope, (filter_name, filter_value)|
|
|
269
|
-
apply_filter_for_path(current_scope, filter_name.to_s, filter_value)
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
def apply_filter_for_path(scope, filter_name, filter_value)
|
|
274
|
-
parts = filter_name.split(".")
|
|
275
|
-
return scope if parts.length < 2
|
|
276
|
-
|
|
277
|
-
relationship_chain = parts[0..-2]
|
|
278
|
-
leaf_filter = parts.last
|
|
279
|
-
|
|
280
|
-
current_model = model_class
|
|
281
|
-
current_definition = definition
|
|
282
|
-
|
|
283
|
-
relationship_chain.each do |relationship_name|
|
|
284
|
-
association = current_model.reflect_on_association(relationship_name.to_sym)
|
|
285
|
-
return scope unless association
|
|
286
|
-
|
|
287
|
-
if association.polymorphic?
|
|
288
|
-
attributes = { leaf_filter => filter_value }
|
|
289
|
-
return apply_polymorphic_nested_filters(scope, association, relationship_name, attributes)
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
current_model = association.klass
|
|
293
|
-
current_definition = JSONAPI::Resource.resource_for_model(current_model)
|
|
294
|
-
return scope unless current_definition
|
|
295
|
-
end
|
|
296
|
-
|
|
297
|
-
join_hash = build_join_hash_for_chain(relationship_chain)
|
|
298
|
-
scope = scope.joins(join_hash) if join_hash.present?
|
|
299
|
-
|
|
300
|
-
apply_filter_on_model(scope, current_model, current_definition, leaf_filter, filter_value)
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def build_join_hash_for_chain(chain)
|
|
304
|
-
return nil if chain.empty?
|
|
305
|
-
|
|
306
|
-
chain.reverse.reduce(nil) do |acc, name|
|
|
307
|
-
if acc.nil?
|
|
308
|
-
name.to_sym
|
|
309
|
-
else
|
|
310
|
-
{ name.to_sym => acc }
|
|
311
|
-
end
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
def apply_filter_on_model(scope, target_model, target_resource, filter_name, filter_value)
|
|
316
|
-
return scope if filter_value.respond_to?(:empty?) ? filter_value.empty? : filter_value.nil?
|
|
317
|
-
|
|
318
|
-
column_filter = parse_column_filter(filter_name)
|
|
319
|
-
if column_filter
|
|
320
|
-
column = target_model.column_for_attribute(column_filter[:column])
|
|
321
|
-
if column
|
|
322
|
-
value = normalize_filter_value_for_model(target_model, column, filter_value)
|
|
323
|
-
condition = build_condition_for_model(target_model, column, value, column_filter[:operator]) if value
|
|
324
|
-
return apply_condition(scope, condition) if condition
|
|
325
|
-
end
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
if target_model.column_names.include?(filter_name)
|
|
329
|
-
table_name = target_model.table_name
|
|
330
|
-
return scope.where(table_name => { filter_name => filter_value })
|
|
331
|
-
end
|
|
332
|
-
|
|
333
|
-
if target_model.respond_to?(filter_name.to_sym)
|
|
334
|
-
begin
|
|
335
|
-
return scope.merge(target_model.public_send(filter_name.to_sym, filter_value))
|
|
336
|
-
rescue ArgumentError, NoMethodError
|
|
337
|
-
# Fall through to return scope
|
|
338
|
-
end
|
|
339
|
-
end
|
|
340
|
-
|
|
341
|
-
if target_resource &&
|
|
342
|
-
target_resource.permitted_filters.map(&:to_s).include?(filter_name) &&
|
|
343
|
-
target_model.respond_to?(filter_name.to_sym)
|
|
344
|
-
begin
|
|
345
|
-
return scope.merge(target_model.public_send(filter_name.to_sym, filter_value))
|
|
346
|
-
rescue ArgumentError, NoMethodError
|
|
347
|
-
# Fall through to return scope
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
scope
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
def apply_polymorphic_nested_filters(scope, association, _relationship_name, attributes)
|
|
355
|
-
foreign_key = association.foreign_key
|
|
356
|
-
foreign_type = association.foreign_type
|
|
357
|
-
fk_column = model_class.column_for_attribute(foreign_key)
|
|
358
|
-
type_value = attributes["type"] || attributes["type_eq"]
|
|
359
|
-
|
|
360
|
-
attributes.each do |attr_name, attr_value|
|
|
361
|
-
next if attr_value.respond_to?(:empty?) ? attr_value.empty? : attr_value.nil?
|
|
362
|
-
next if attr_name == "type"
|
|
363
|
-
|
|
364
|
-
column_filter = parse_column_filter(attr_name)
|
|
365
|
-
if column_filter && column_filter[:column] == "id" && fk_column
|
|
366
|
-
value = normalize_filter_value_for_model(model_class, fk_column, attr_value)
|
|
367
|
-
condition = build_condition_for_model(model_class, fk_column, value, column_filter[:operator]) if value
|
|
368
|
-
scope = apply_condition(scope, condition) if condition
|
|
369
|
-
elsif attr_name == "id"
|
|
370
|
-
scope = scope.where(foreign_key => attr_value)
|
|
371
|
-
end
|
|
372
|
-
|
|
373
|
-
if foreign_type && association.options[:class_name]
|
|
374
|
-
scope = scope.where(foreign_type => association.options[:class_name])
|
|
375
|
-
elsif foreign_type && type_value
|
|
376
|
-
scope = scope.where(foreign_type => type_value)
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
if foreign_type && association.options[:class_name].nil? && type_value
|
|
381
|
-
scope = scope.where(foreign_type => type_value)
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
scope
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
def apply_pagination
|
|
388
|
-
return @scope if page_params.empty?
|
|
389
|
-
|
|
390
|
-
number = page_params["number"]&.to_i || 1
|
|
391
|
-
size = page_params["size"]&.to_i || JSONAPI.configuration.default_page_size
|
|
392
|
-
|
|
393
|
-
# Enforce max page size
|
|
394
|
-
size = [size, JSONAPI.configuration.max_page_size].min
|
|
395
|
-
|
|
396
|
-
offset = (number - 1) * size
|
|
397
|
-
|
|
398
|
-
# Handle array (after virtual attribute sorting) vs ActiveRecord relation
|
|
399
|
-
if @scope.is_a?(Array)
|
|
400
|
-
@scope.slice(offset, size) || []
|
|
401
|
-
else
|
|
402
|
-
@scope.offset(offset).limit(size)
|
|
403
|
-
end
|
|
404
|
-
end
|
|
405
|
-
end
|
|
406
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JSONAPI
|
|
4
|
-
module Instrumentation
|
|
5
|
-
def self.enabled?
|
|
6
|
-
defined?(Rails) && Rails.respond_to?(:event) && Rails.event.respond_to?(:notify)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
def self.resource_event(action:, resource_type:, resource_id:, changes: {})
|
|
10
|
-
return unless enabled?
|
|
11
|
-
|
|
12
|
-
Rails.event.tagged("jsonapi") do
|
|
13
|
-
Rails.event.notify(
|
|
14
|
-
"jsonapi.#{resource_type}.#{action}",
|
|
15
|
-
resource_type:,
|
|
16
|
-
resource_id:,
|
|
17
|
-
changes: changes.compact
|
|
18
|
-
)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.relationship_event(action:, resource_type:, resource_id:, relationship_name:, related_ids: nil,
|
|
23
|
-
related_type: nil)
|
|
24
|
-
return unless enabled?
|
|
25
|
-
|
|
26
|
-
payload = {
|
|
27
|
-
resource_type:,
|
|
28
|
-
resource_id:,
|
|
29
|
-
relationship_name:
|
|
30
|
-
}
|
|
31
|
-
payload[:related_type] = related_type if related_type
|
|
32
|
-
payload[:related_ids] = Array(related_ids) if related_ids
|
|
33
|
-
|
|
34
|
-
Rails.event.tagged("jsonapi", "relationship") do
|
|
35
|
-
Rails.event.notify(
|
|
36
|
-
"jsonapi.#{resource_type}.relationship.#{action}",
|
|
37
|
-
payload
|
|
38
|
-
)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "cgi"
|
|
4
|
-
|
|
5
|
-
module JSONAPI
|
|
6
|
-
module ParamHelpers
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
|
-
def flatten_filter_hash(hash, parent_key = nil, accumulator = {})
|
|
10
|
-
hash.each do |key, value|
|
|
11
|
-
next unless key
|
|
12
|
-
|
|
13
|
-
full_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
|
|
14
|
-
|
|
15
|
-
if value.is_a?(Hash)
|
|
16
|
-
flatten_filter_hash(value, full_key, accumulator)
|
|
17
|
-
else
|
|
18
|
-
accumulator[full_key] = value
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
accumulator
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def deep_symbolize_params(params)
|
|
26
|
-
if params.is_a?(ActionController::Parameters)
|
|
27
|
-
params.to_unsafe_h.deep_symbolize_keys
|
|
28
|
-
else
|
|
29
|
-
params.to_h.deep_symbolize_keys
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def build_query_string(query_params)
|
|
34
|
-
query_parts = []
|
|
35
|
-
query_params.each do |key, value|
|
|
36
|
-
query_parts.concat(build_query_parts_for_param(key, value))
|
|
37
|
-
end
|
|
38
|
-
query_parts.join("&")
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def build_query_parts_for_param(key, value)
|
|
42
|
-
if value.is_a?(Hash)
|
|
43
|
-
value.map { |k, v| "#{CGI.escape(key.to_s)}[#{CGI.escape(k.to_s)}]=#{CGI.escape(v.to_s)}" }
|
|
44
|
-
elsif value.is_a?(Array)
|
|
45
|
-
value.map { |v| "#{CGI.escape(key.to_s)}=#{CGI.escape(v.to_s)}" }
|
|
46
|
-
else
|
|
47
|
-
["#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"]
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json_api/errors/parameter_not_allowed"
|
|
4
|
-
|
|
5
|
-
module JSONAPI
|
|
6
|
-
module RelationshipGuard
|
|
7
|
-
module_function
|
|
8
|
-
|
|
9
|
-
def ensure_writable!(association, error_target, readonly: false)
|
|
10
|
-
return unless association
|
|
11
|
-
return unless readonly
|
|
12
|
-
|
|
13
|
-
raise JSONAPI::Exceptions::ParameterNotAllowed, [error_target]
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
end
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JSONAPI
|
|
4
|
-
module RelationshipHelpers
|
|
5
|
-
module_function
|
|
6
|
-
|
|
7
|
-
# Delegate to TypeConversion
|
|
8
|
-
def type_to_class_name(type)
|
|
9
|
-
TypeConversion.type_to_class_name(type)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def model_type_name(model_class)
|
|
13
|
-
TypeConversion.model_type_name(model_class)
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def resource_type_name(definition_class)
|
|
17
|
-
TypeConversion.resource_type_name(definition_class)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Delegate to SortParsing
|
|
21
|
-
def extract_sort_field_name(sort_field)
|
|
22
|
-
SortParsing.extract_sort_field_name(sort_field)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def extract_sort_direction(sort_field)
|
|
26
|
-
SortParsing.extract_sort_direction(sort_field)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Delegate to ResourceIdentifier
|
|
30
|
-
def serialize_resource_identifier(
|
|
31
|
-
record, association: nil, resource_class: nil, use_instance_class: false, base_resource_class: nil
|
|
32
|
-
)
|
|
33
|
-
ResourceIdentifier.serialize_identifier(
|
|
34
|
-
record,
|
|
35
|
-
association:,
|
|
36
|
-
definition: resource_class,
|
|
37
|
-
use_instance_class:
|
|
38
|
-
)
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def extract_id_from_identifier(identifier)
|
|
42
|
-
ResourceIdentifier.extract_id(identifier)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def extract_type_from_identifier(identifier)
|
|
46
|
-
ResourceIdentifier.extract_type(identifier)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def resolve_and_find_related_resource(identifier, association:, resource_class:, relationship_name:)
|
|
50
|
-
ResourceIdentifier.resolve_and_find_related_record(
|
|
51
|
-
identifier,
|
|
52
|
-
association:,
|
|
53
|
-
definition: resource_class,
|
|
54
|
-
relationship_name:
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def polymorphic_association?(definition, relationship_name)
|
|
59
|
-
ResourceIdentifier.polymorphic_association?(definition, relationship_name)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def sti_subclass?(instance_class, association_class)
|
|
63
|
-
ResourceIdentifier.sti_subclass?(instance_class, association_class)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def polymorphic_association_for_association?(association, definition)
|
|
67
|
-
ResourceIdentifier.polymorphic_association_for_association?(association, definition)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def find_relationship_definition(definition, relationship_name)
|
|
71
|
-
definition.relationship_definitions.find { |r| r[:name].to_s == relationship_name.to_s }
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JSONAPI
|
|
4
|
-
module ResourceIdentifier
|
|
5
|
-
module_function
|
|
6
|
-
|
|
7
|
-
def serialize_identifier(record, association:, definition:, use_instance_class: false)
|
|
8
|
-
model_class = determine_model_class(record, association:, definition:,
|
|
9
|
-
use_instance_class:)
|
|
10
|
-
related_definition = JSONAPI::ResourceLoader.find_for_model(model_class)
|
|
11
|
-
related_type = TypeConversion.resource_type_name(related_definition)
|
|
12
|
-
|
|
13
|
-
{ type: related_type, id: record.id.to_s }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def extract_id(identifier)
|
|
17
|
-
identifier[:id].to_s.presence
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def extract_type(identifier)
|
|
21
|
-
identifier[:type]
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def resolve_and_find_related_record(identifier, association:, definition:, relationship_name:)
|
|
25
|
-
type = extract_type(identifier)
|
|
26
|
-
id = extract_id(identifier)
|
|
27
|
-
|
|
28
|
-
raise ArgumentError, "Missing type or id in relationship data" unless type && id
|
|
29
|
-
|
|
30
|
-
is_polymorphic = polymorphic_association?(definition, relationship_name)
|
|
31
|
-
|
|
32
|
-
unless is_polymorphic
|
|
33
|
-
expected_type = TypeConversion.model_type_name(association.klass)
|
|
34
|
-
if type != expected_type
|
|
35
|
-
raise ArgumentError, "Invalid relationship type: expected #{expected_type}, got #{type}"
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
begin
|
|
40
|
-
related_model_class = if is_polymorphic
|
|
41
|
-
TypeConversion.type_to_class_name(type).constantize
|
|
42
|
-
else
|
|
43
|
-
association.klass
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
related_model_class.find(id)
|
|
47
|
-
rescue ActiveRecord::RecordNotFound
|
|
48
|
-
raise ArgumentError, "Related resource not found: #{type} with id #{id}"
|
|
49
|
-
rescue NameError
|
|
50
|
-
raise ArgumentError, "Invalid relationship type: #{type} does not correspond to a valid model class"
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def polymorphic_association?(definition, relationship_name)
|
|
55
|
-
relationship_def = definition.relationship_definitions.find do |r|
|
|
56
|
-
r[:name].to_s == relationship_name.to_s
|
|
57
|
-
end
|
|
58
|
-
return false unless relationship_def
|
|
59
|
-
|
|
60
|
-
relationship_def[:options][:polymorphic] == true
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def sti_subclass?(instance_class, association_class)
|
|
64
|
-
return false unless instance_class.respond_to?(:base_class)
|
|
65
|
-
|
|
66
|
-
instance_class.base_class == association_class && instance_class != association_class
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def determine_model_class(record, association:, definition:, use_instance_class:)
|
|
70
|
-
if association && (polymorphic_association_for_association?(association, definition) ||
|
|
71
|
-
(use_instance_class && sti_subclass?(record.class, association.klass)))
|
|
72
|
-
record.class
|
|
73
|
-
elsif association
|
|
74
|
-
association.klass
|
|
75
|
-
else
|
|
76
|
-
record.class
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def polymorphic_association_for_association?(association, definition)
|
|
81
|
-
return false unless definition
|
|
82
|
-
|
|
83
|
-
relationship_name = association.name
|
|
84
|
-
polymorphic_association?(definition, relationship_name)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|