sanger-jsonapi-resources 0.1.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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +53 -0
- data/lib/generators/jsonapi/USAGE +13 -0
- data/lib/generators/jsonapi/controller_generator.rb +14 -0
- data/lib/generators/jsonapi/resource_generator.rb +14 -0
- data/lib/generators/jsonapi/templates/jsonapi_controller.rb +4 -0
- data/lib/generators/jsonapi/templates/jsonapi_resource.rb +4 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +320 -0
- data/lib/jsonapi/cached_resource_fragment.rb +127 -0
- data/lib/jsonapi/callbacks.rb +51 -0
- data/lib/jsonapi/compiled_json.rb +36 -0
- data/lib/jsonapi/configuration.rb +258 -0
- data/lib/jsonapi/error.rb +47 -0
- data/lib/jsonapi/error_codes.rb +60 -0
- data/lib/jsonapi/exceptions.rb +563 -0
- data/lib/jsonapi/formatter.rb +169 -0
- data/lib/jsonapi/include_directives.rb +100 -0
- data/lib/jsonapi/link_builder.rb +152 -0
- data/lib/jsonapi/mime_types.rb +41 -0
- data/lib/jsonapi/naive_cache.rb +30 -0
- data/lib/jsonapi/operation.rb +24 -0
- data/lib/jsonapi/operation_dispatcher.rb +88 -0
- data/lib/jsonapi/operation_result.rb +65 -0
- data/lib/jsonapi/operation_results.rb +35 -0
- data/lib/jsonapi/paginator.rb +209 -0
- data/lib/jsonapi/processor.rb +328 -0
- data/lib/jsonapi/relationship.rb +94 -0
- data/lib/jsonapi/relationship_builder.rb +167 -0
- data/lib/jsonapi/request_parser.rb +678 -0
- data/lib/jsonapi/resource.rb +1255 -0
- data/lib/jsonapi/resource_controller.rb +5 -0
- data/lib/jsonapi/resource_controller_metal.rb +16 -0
- data/lib/jsonapi/resource_serializer.rb +531 -0
- data/lib/jsonapi/resources/version.rb +5 -0
- data/lib/jsonapi/response_document.rb +135 -0
- data/lib/jsonapi/routing_ext.rb +262 -0
- data/lib/jsonapi-resources.rb +27 -0
- metadata +223 -0
@@ -0,0 +1,1255 @@
|
|
1
|
+
require 'jsonapi/callbacks'
|
2
|
+
require 'jsonapi/relationship_builder'
|
3
|
+
|
4
|
+
module JSONAPI
|
5
|
+
class Resource
|
6
|
+
include Callbacks
|
7
|
+
|
8
|
+
attr_reader :context
|
9
|
+
|
10
|
+
define_jsonapi_resources_callbacks :create,
|
11
|
+
:update,
|
12
|
+
:remove,
|
13
|
+
:save,
|
14
|
+
:create_to_many_link,
|
15
|
+
:replace_to_many_links,
|
16
|
+
:create_to_one_link,
|
17
|
+
:replace_to_one_link,
|
18
|
+
:replace_polymorphic_to_one_link,
|
19
|
+
:remove_to_many_link,
|
20
|
+
:remove_to_one_link,
|
21
|
+
:replace_fields
|
22
|
+
|
23
|
+
def initialize(model, context)
|
24
|
+
@model = model
|
25
|
+
@context = context
|
26
|
+
@reload_needed = false
|
27
|
+
@changing = false
|
28
|
+
@save_needed = false
|
29
|
+
end
|
30
|
+
|
31
|
+
def _model
|
32
|
+
@model
|
33
|
+
end
|
34
|
+
|
35
|
+
def id
|
36
|
+
_model.public_send(self.class._primary_key)
|
37
|
+
end
|
38
|
+
|
39
|
+
def cache_id
|
40
|
+
[id, _model.public_send(self.class._cache_field)]
|
41
|
+
end
|
42
|
+
|
43
|
+
def is_new?
|
44
|
+
id.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def change(callback)
|
48
|
+
completed = false
|
49
|
+
|
50
|
+
if @changing
|
51
|
+
run_callbacks callback do
|
52
|
+
completed = (yield == :completed)
|
53
|
+
end
|
54
|
+
else
|
55
|
+
run_callbacks is_new? ? :create : :update do
|
56
|
+
@changing = true
|
57
|
+
run_callbacks callback do
|
58
|
+
completed = (yield == :completed)
|
59
|
+
end
|
60
|
+
|
61
|
+
completed = (save == :completed) if @save_needed || is_new?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
return completed ? :completed : :accepted
|
66
|
+
end
|
67
|
+
|
68
|
+
def remove
|
69
|
+
run_callbacks :remove do
|
70
|
+
_remove
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def create_to_many_links(relationship_type, relationship_key_values, options = {})
|
75
|
+
change :create_to_many_link do
|
76
|
+
_create_to_many_links(relationship_type, relationship_key_values, options)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def replace_to_many_links(relationship_type, relationship_key_values, options = {})
|
81
|
+
change :replace_to_many_links do
|
82
|
+
_replace_to_many_links(relationship_type, relationship_key_values, options)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def replace_to_one_link(relationship_type, relationship_key_value, options = {})
|
87
|
+
change :replace_to_one_link do
|
88
|
+
_replace_to_one_link(relationship_type, relationship_key_value, options)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options = {})
|
93
|
+
change :replace_polymorphic_to_one_link do
|
94
|
+
_replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def remove_to_many_link(relationship_type, key, options = {})
|
99
|
+
change :remove_to_many_link do
|
100
|
+
_remove_to_many_link(relationship_type, key, options)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def remove_to_one_link(relationship_type, options = {})
|
105
|
+
change :remove_to_one_link do
|
106
|
+
_remove_to_one_link(relationship_type, options)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def replace_fields(field_data)
|
111
|
+
change :replace_fields do
|
112
|
+
_replace_fields(field_data)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Override this on a resource instance to override the fetchable keys
|
117
|
+
def fetchable_fields
|
118
|
+
self.class.fields
|
119
|
+
end
|
120
|
+
|
121
|
+
# Override this on a resource to customize how the associated records
|
122
|
+
# are fetched for a model. Particularly helpful for authorization.
|
123
|
+
def records_for(relation_name)
|
124
|
+
_model.public_send relation_name
|
125
|
+
end
|
126
|
+
|
127
|
+
def model_error_messages
|
128
|
+
_model.errors.messages
|
129
|
+
end
|
130
|
+
|
131
|
+
# Add metadata to validation error objects.
|
132
|
+
#
|
133
|
+
# Suppose `model_error_messages` returned the following error messages
|
134
|
+
# hash:
|
135
|
+
#
|
136
|
+
# {password: ["too_short", "format"]}
|
137
|
+
#
|
138
|
+
# Then to add data to the validation error `validation_error_metadata`
|
139
|
+
# could return:
|
140
|
+
#
|
141
|
+
# {
|
142
|
+
# password: {
|
143
|
+
# "too_short": {"minimum_length" => 6},
|
144
|
+
# "format": {"requirement" => "must contain letters and numbers"}
|
145
|
+
# }
|
146
|
+
# }
|
147
|
+
#
|
148
|
+
# The specified metadata is then be merged into the validation error
|
149
|
+
# object.
|
150
|
+
def validation_error_metadata
|
151
|
+
{}
|
152
|
+
end
|
153
|
+
|
154
|
+
# Override this to return resource level meta data
|
155
|
+
# must return a hash, and if the hash is empty the meta section will not be serialized with the resource
|
156
|
+
# meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
|
157
|
+
# serializer's format_key and format_value methods if desired
|
158
|
+
# the _options hash will contain the serializer and the serialization_options
|
159
|
+
def meta(_options)
|
160
|
+
{}
|
161
|
+
end
|
162
|
+
|
163
|
+
# Override this to return custom links
|
164
|
+
# must return a hash, which will be merged with the default { self: 'self-url' } links hash
|
165
|
+
# links keys will be not be formatted with the key formatter for the serializer by default.
|
166
|
+
# They can however use the serializer's format_key and format_value methods if desired
|
167
|
+
# the _options hash will contain the serializer and the serialization_options
|
168
|
+
def custom_links(_options)
|
169
|
+
{}
|
170
|
+
end
|
171
|
+
|
172
|
+
def preloaded_fragments
|
173
|
+
# A hash of hashes
|
174
|
+
@preloaded_fragments ||= Hash.new
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def save
|
180
|
+
run_callbacks :save do
|
181
|
+
_save
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Override this on a resource to return a different result code. Any
|
186
|
+
# value other than :completed will result in operations returning
|
187
|
+
# `:accepted`
|
188
|
+
#
|
189
|
+
# For example to return `:accepted` if your model does not immediately
|
190
|
+
# save resources to the database you could override `_save` as follows:
|
191
|
+
#
|
192
|
+
# ```
|
193
|
+
# def _save
|
194
|
+
# super
|
195
|
+
# return :accepted
|
196
|
+
# end
|
197
|
+
# ```
|
198
|
+
def _save(validation_context = nil)
|
199
|
+
unless @model.valid?(validation_context)
|
200
|
+
fail JSONAPI::Exceptions::ValidationErrors.new(self)
|
201
|
+
end
|
202
|
+
|
203
|
+
if defined? @model.save
|
204
|
+
saved = @model.save(validate: false)
|
205
|
+
|
206
|
+
unless saved
|
207
|
+
if @model.errors.present?
|
208
|
+
fail JSONAPI::Exceptions::ValidationErrors.new(self)
|
209
|
+
else
|
210
|
+
fail JSONAPI::Exceptions::SaveFailed.new
|
211
|
+
end
|
212
|
+
end
|
213
|
+
else
|
214
|
+
saved = true
|
215
|
+
end
|
216
|
+
@model.reload if @reload_needed
|
217
|
+
@reload_needed = false
|
218
|
+
|
219
|
+
@save_needed = !saved
|
220
|
+
|
221
|
+
:completed
|
222
|
+
end
|
223
|
+
|
224
|
+
def _remove
|
225
|
+
unless @model.destroy
|
226
|
+
fail JSONAPI::Exceptions::ValidationErrors.new(self)
|
227
|
+
end
|
228
|
+
:completed
|
229
|
+
|
230
|
+
rescue ActiveRecord::DeleteRestrictionError => e
|
231
|
+
fail JSONAPI::Exceptions::RecordLocked.new(e.message)
|
232
|
+
end
|
233
|
+
|
234
|
+
def reflect_relationship?(relationship, options)
|
235
|
+
return false if !relationship.reflect ||
|
236
|
+
(!JSONAPI.configuration.use_relationship_reflection || options[:reflected_source])
|
237
|
+
|
238
|
+
inverse_relationship = relationship.resource_klass._relationships[relationship.inverse_relationship]
|
239
|
+
if inverse_relationship.nil?
|
240
|
+
warn "Inverse relationship could not be found for #{self.class.name}.#{relationship.name}. Relationship reflection disabled."
|
241
|
+
return false
|
242
|
+
end
|
243
|
+
true
|
244
|
+
end
|
245
|
+
|
246
|
+
def _create_to_many_links(relationship_type, relationship_key_values, options)
|
247
|
+
relationship = self.class._relationships[relationship_type]
|
248
|
+
|
249
|
+
# check if relationship_key_values are already members of this relationship
|
250
|
+
relation_name = relationship.relation_name(context: @context)
|
251
|
+
existing_relations = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_values)
|
252
|
+
if existing_relations.count > 0
|
253
|
+
# todo: obscure id so not to leak info
|
254
|
+
fail JSONAPI::Exceptions::HasManyRelationExists.new(existing_relations.first.id)
|
255
|
+
end
|
256
|
+
|
257
|
+
if options[:reflected_source]
|
258
|
+
@model.public_send(relation_name) << options[:reflected_source]._model
|
259
|
+
return :completed
|
260
|
+
end
|
261
|
+
|
262
|
+
# load requested related resources
|
263
|
+
# make sure they all exist (also based on context) and add them to relationship
|
264
|
+
|
265
|
+
related_resources = relationship.resource_klass.find_by_keys(relationship_key_values, context: @context)
|
266
|
+
|
267
|
+
if related_resources.count != relationship_key_values.count
|
268
|
+
# todo: obscure id so not to leak info
|
269
|
+
fail JSONAPI::Exceptions::RecordNotFound.new('unspecified')
|
270
|
+
end
|
271
|
+
|
272
|
+
reflect = reflect_relationship?(relationship, options)
|
273
|
+
|
274
|
+
related_resources.each do |related_resource|
|
275
|
+
if reflect
|
276
|
+
if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
|
277
|
+
related_resource.create_to_many_links(relationship.inverse_relationship, [id], reflected_source: self)
|
278
|
+
else
|
279
|
+
related_resource.replace_to_one_link(relationship.inverse_relationship, id, reflected_source: self)
|
280
|
+
end
|
281
|
+
@reload_needed = true
|
282
|
+
else
|
283
|
+
@model.public_send(relation_name) << related_resource._model
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
:completed
|
288
|
+
end
|
289
|
+
|
290
|
+
def _replace_to_many_links(relationship_type, relationship_key_values, options)
|
291
|
+
relationship = self.class._relationships[relationship_type]
|
292
|
+
|
293
|
+
reflect = reflect_relationship?(relationship, options)
|
294
|
+
|
295
|
+
if reflect
|
296
|
+
existing = send("#{relationship.foreign_key}")
|
297
|
+
to_delete = existing - (relationship_key_values & existing)
|
298
|
+
to_delete.each do |key|
|
299
|
+
_remove_to_many_link(relationship_type, key, reflected_source: self)
|
300
|
+
end
|
301
|
+
|
302
|
+
to_add = relationship_key_values - (relationship_key_values & existing)
|
303
|
+
_create_to_many_links(relationship_type, to_add, {})
|
304
|
+
|
305
|
+
@reload_needed = true
|
306
|
+
else
|
307
|
+
send("#{relationship.foreign_key}=", relationship_key_values)
|
308
|
+
@save_needed = true
|
309
|
+
end
|
310
|
+
|
311
|
+
:completed
|
312
|
+
end
|
313
|
+
|
314
|
+
def _replace_to_one_link(relationship_type, relationship_key_value, options)
|
315
|
+
relationship = self.class._relationships[relationship_type]
|
316
|
+
|
317
|
+
send("#{relationship.foreign_key}=", relationship_key_value)
|
318
|
+
@save_needed = true
|
319
|
+
|
320
|
+
:completed
|
321
|
+
end
|
322
|
+
|
323
|
+
def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type, options)
|
324
|
+
relationship = self.class._relationships[relationship_type.to_sym]
|
325
|
+
|
326
|
+
_model.public_send("#{relationship.foreign_key}=", key_value)
|
327
|
+
_model.public_send("#{relationship.polymorphic_type}=", _model_class_name(key_type))
|
328
|
+
|
329
|
+
@save_needed = true
|
330
|
+
|
331
|
+
:completed
|
332
|
+
end
|
333
|
+
|
334
|
+
def _remove_to_many_link(relationship_type, key, options)
|
335
|
+
relationship = self.class._relationships[relationship_type]
|
336
|
+
|
337
|
+
reflect = reflect_relationship?(relationship, options)
|
338
|
+
|
339
|
+
if reflect
|
340
|
+
|
341
|
+
related_resource = relationship.resource_klass.find_by_key(key, context: @context)
|
342
|
+
|
343
|
+
if related_resource.nil?
|
344
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(key)
|
345
|
+
else
|
346
|
+
if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany)
|
347
|
+
related_resource.remove_to_many_link(relationship.inverse_relationship, id, reflected_source: self)
|
348
|
+
else
|
349
|
+
related_resource.remove_to_one_link(relationship.inverse_relationship, reflected_source: self)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
@reload_needed = true
|
354
|
+
else
|
355
|
+
@model.public_send(relationship.relation_name(context: @context)).delete(key)
|
356
|
+
end
|
357
|
+
|
358
|
+
:completed
|
359
|
+
|
360
|
+
rescue ActiveRecord::DeleteRestrictionError => e
|
361
|
+
fail JSONAPI::Exceptions::RecordLocked.new(e.message)
|
362
|
+
rescue ActiveRecord::RecordNotFound
|
363
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(key)
|
364
|
+
end
|
365
|
+
|
366
|
+
def _remove_to_one_link(relationship_type, options)
|
367
|
+
relationship = self.class._relationships[relationship_type]
|
368
|
+
|
369
|
+
send("#{relationship.foreign_key}=", nil)
|
370
|
+
@save_needed = true
|
371
|
+
|
372
|
+
:completed
|
373
|
+
end
|
374
|
+
|
375
|
+
def _replace_fields(field_data)
|
376
|
+
field_data[:attributes].each do |attribute, value|
|
377
|
+
begin
|
378
|
+
send "#{attribute}=", value
|
379
|
+
@save_needed = true
|
380
|
+
rescue ArgumentError
|
381
|
+
# :nocov: Will be thrown if an enum value isn't allowed for an enum. Currently not tested as enums are a rails 4.1 and higher feature
|
382
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(attribute, value)
|
383
|
+
# :nocov:
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
field_data[:to_one].each do |relationship_type, value|
|
388
|
+
if value.nil?
|
389
|
+
remove_to_one_link(relationship_type)
|
390
|
+
else
|
391
|
+
case value
|
392
|
+
when Hash
|
393
|
+
replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type))
|
394
|
+
else
|
395
|
+
replace_to_one_link(relationship_type, value)
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end if field_data[:to_one]
|
399
|
+
|
400
|
+
field_data[:to_many].each do |relationship_type, values|
|
401
|
+
replace_to_many_links(relationship_type, values)
|
402
|
+
end if field_data[:to_many]
|
403
|
+
|
404
|
+
:completed
|
405
|
+
end
|
406
|
+
|
407
|
+
def _model_class_name(key_type)
|
408
|
+
type_class_name = key_type.to_s.classify
|
409
|
+
resource = self.class.resource_for(type_class_name)
|
410
|
+
resource ? resource._model_name.to_s : type_class_name
|
411
|
+
end
|
412
|
+
|
413
|
+
class << self
|
414
|
+
def inherited(subclass)
|
415
|
+
subclass.abstract(false)
|
416
|
+
subclass.immutable(false)
|
417
|
+
subclass.caching(false)
|
418
|
+
subclass._attributes = (_attributes || {}).dup
|
419
|
+
|
420
|
+
subclass._model_hints = (_model_hints || {}).dup
|
421
|
+
|
422
|
+
unless _model_name.empty?
|
423
|
+
subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true)
|
424
|
+
end
|
425
|
+
|
426
|
+
subclass.rebuild_relationships(_relationships || {})
|
427
|
+
|
428
|
+
subclass._allowed_filters = (_allowed_filters || Set.new).dup
|
429
|
+
|
430
|
+
type = subclass.name.demodulize.sub(/Resource$/, '').underscore
|
431
|
+
subclass._type = type.pluralize.to_sym
|
432
|
+
|
433
|
+
unless subclass._attributes[:id]
|
434
|
+
subclass.attribute :id, format: :id
|
435
|
+
end
|
436
|
+
|
437
|
+
check_reserved_resource_name(subclass._type, subclass.name)
|
438
|
+
end
|
439
|
+
|
440
|
+
def rebuild_relationships(relationships)
|
441
|
+
original_relationships = relationships.deep_dup
|
442
|
+
|
443
|
+
@_relationships = {}
|
444
|
+
|
445
|
+
if original_relationships.is_a?(Hash)
|
446
|
+
original_relationships.each_value do |relationship|
|
447
|
+
options = relationship.options.dup
|
448
|
+
options[:parent_resource] = self
|
449
|
+
_add_relationship(relationship.class, relationship.name, options)
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
|
454
|
+
def resource_for(type)
|
455
|
+
type = type.underscore
|
456
|
+
type_with_module = type.include?('/') ? type : module_path + type
|
457
|
+
|
458
|
+
resource_name = _resource_name_from_type(type_with_module)
|
459
|
+
resource = resource_name.safe_constantize if resource_name
|
460
|
+
if resource.nil?
|
461
|
+
fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
|
462
|
+
end
|
463
|
+
resource
|
464
|
+
end
|
465
|
+
|
466
|
+
def resource_for_model(model)
|
467
|
+
resource_for(resource_type_for(model))
|
468
|
+
end
|
469
|
+
|
470
|
+
def _resource_name_from_type(type)
|
471
|
+
"#{type.to_s.underscore.singularize}_resource".camelize
|
472
|
+
end
|
473
|
+
|
474
|
+
def resource_type_for(model)
|
475
|
+
model_name = model.class.to_s.underscore
|
476
|
+
if _model_hints[model_name]
|
477
|
+
_model_hints[model_name]
|
478
|
+
else
|
479
|
+
model_name.rpartition('/').last
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
attr_accessor :_attributes, :_relationships, :_type, :_model_hints
|
484
|
+
attr_writer :_allowed_filters, :_paginator
|
485
|
+
|
486
|
+
def create(context)
|
487
|
+
new(create_model, context)
|
488
|
+
end
|
489
|
+
|
490
|
+
def create_model
|
491
|
+
_model_class.new
|
492
|
+
end
|
493
|
+
|
494
|
+
def routing_options(options)
|
495
|
+
@_routing_resource_options = options
|
496
|
+
end
|
497
|
+
|
498
|
+
def routing_resource_options
|
499
|
+
@_routing_resource_options ||= {}
|
500
|
+
end
|
501
|
+
|
502
|
+
# Methods used in defining a resource class
|
503
|
+
def attributes(*attrs)
|
504
|
+
options = attrs.extract_options!.dup
|
505
|
+
attrs.each do |attr|
|
506
|
+
attribute(attr, options)
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def attribute(attr, options = {})
|
511
|
+
check_reserved_attribute_name(attr)
|
512
|
+
|
513
|
+
if (attr.to_sym == :id) && (options[:format].nil?)
|
514
|
+
ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
|
515
|
+
end
|
516
|
+
|
517
|
+
check_duplicate_attribute_name(attr) if options[:format].nil?
|
518
|
+
|
519
|
+
@_attributes ||= {}
|
520
|
+
@_attributes[attr] = options
|
521
|
+
define_method attr do
|
522
|
+
@model.public_send(options[:delegate] ? options[:delegate].to_sym : attr)
|
523
|
+
end unless method_defined?(attr)
|
524
|
+
|
525
|
+
define_method "#{attr}=" do |value|
|
526
|
+
@model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value)
|
527
|
+
end unless method_defined?("#{attr}=")
|
528
|
+
end
|
529
|
+
|
530
|
+
def default_attribute_options
|
531
|
+
{ format: :default }
|
532
|
+
end
|
533
|
+
|
534
|
+
def relationship(*attrs)
|
535
|
+
options = attrs.extract_options!
|
536
|
+
klass = case options[:to]
|
537
|
+
when :one
|
538
|
+
Relationship::ToOne
|
539
|
+
when :many
|
540
|
+
Relationship::ToMany
|
541
|
+
else
|
542
|
+
#:nocov:#
|
543
|
+
fail ArgumentError.new('to: must be either :one or :many')
|
544
|
+
#:nocov:#
|
545
|
+
end
|
546
|
+
_add_relationship(klass, *attrs, options.except(:to))
|
547
|
+
end
|
548
|
+
|
549
|
+
def has_one(*attrs)
|
550
|
+
_add_relationship(Relationship::ToOne, *attrs)
|
551
|
+
end
|
552
|
+
|
553
|
+
def belongs_to(*attrs)
|
554
|
+
ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
|
555
|
+
" using the `belongs_to` class method. We think `has_one`" \
|
556
|
+
" is more appropriate. If you know what you're doing," \
|
557
|
+
" and don't want to see this warning again, override the" \
|
558
|
+
" `belongs_to` class method on your resource."
|
559
|
+
_add_relationship(Relationship::ToOne, *attrs)
|
560
|
+
end
|
561
|
+
|
562
|
+
def has_many(*attrs)
|
563
|
+
_add_relationship(Relationship::ToMany, *attrs)
|
564
|
+
end
|
565
|
+
|
566
|
+
def model_name(model, options = {})
|
567
|
+
@_model_name = model.to_sym
|
568
|
+
|
569
|
+
model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
|
570
|
+
|
571
|
+
rebuild_relationships(_relationships)
|
572
|
+
end
|
573
|
+
|
574
|
+
def model_hint(model: _model_name, resource: _type)
|
575
|
+
resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
|
576
|
+
|
577
|
+
_model_hints[model.to_s.gsub('::', '/').underscore] = resource_type.to_s
|
578
|
+
end
|
579
|
+
|
580
|
+
def filters(*attrs)
|
581
|
+
@_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
|
582
|
+
end
|
583
|
+
|
584
|
+
def filter(attr, *args)
|
585
|
+
@_allowed_filters[attr.to_sym] = args.extract_options!
|
586
|
+
end
|
587
|
+
|
588
|
+
def primary_key(key)
|
589
|
+
@_primary_key = key.to_sym
|
590
|
+
end
|
591
|
+
|
592
|
+
def cache_field(field)
|
593
|
+
@_cache_field = field.to_sym
|
594
|
+
end
|
595
|
+
|
596
|
+
# Override in your resource to filter the updatable keys
|
597
|
+
def updatable_fields(_context = nil)
|
598
|
+
_updatable_relationships | _attributes.keys - [:id]
|
599
|
+
end
|
600
|
+
|
601
|
+
# Override in your resource to filter the creatable keys
|
602
|
+
def creatable_fields(_context = nil)
|
603
|
+
_updatable_relationships | _attributes.keys - [:id]
|
604
|
+
end
|
605
|
+
|
606
|
+
# Override in your resource to filter the sortable keys
|
607
|
+
def sortable_fields(_context = nil)
|
608
|
+
_attributes.keys
|
609
|
+
end
|
610
|
+
|
611
|
+
def fields
|
612
|
+
_relationships.keys | _attributes.keys
|
613
|
+
end
|
614
|
+
|
615
|
+
def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
|
616
|
+
case model_includes
|
617
|
+
when Array
|
618
|
+
return model_includes.map do |value|
|
619
|
+
resolve_relationship_names_to_relations(resource_klass, value, options)
|
620
|
+
end
|
621
|
+
when Hash
|
622
|
+
model_includes.keys.each do |key|
|
623
|
+
relationship = resource_klass._relationships[key]
|
624
|
+
value = model_includes[key]
|
625
|
+
model_includes.delete(key)
|
626
|
+
model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
|
627
|
+
end
|
628
|
+
return model_includes
|
629
|
+
when Symbol
|
630
|
+
relationship = resource_klass._relationships[model_includes]
|
631
|
+
return relationship.relation_name(options)
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
def apply_includes(records, options = {})
|
636
|
+
include_directives = options[:include_directives]
|
637
|
+
if include_directives
|
638
|
+
model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
|
639
|
+
records = records.includes(model_includes)
|
640
|
+
end
|
641
|
+
|
642
|
+
records
|
643
|
+
end
|
644
|
+
|
645
|
+
def apply_pagination(records, paginator, order_options)
|
646
|
+
records = paginator.apply(records, order_options) if paginator
|
647
|
+
records
|
648
|
+
end
|
649
|
+
|
650
|
+
def apply_sort(records, order_options, _context = {})
|
651
|
+
if order_options.any?
|
652
|
+
order_options.each_pair do |field, direction|
|
653
|
+
if field.to_s.include?(".")
|
654
|
+
*model_names, column_name = field.split(".")
|
655
|
+
|
656
|
+
associations = _lookup_association_chain([records.model.to_s, *model_names])
|
657
|
+
joins_query = _build_joins([records.model, *associations])
|
658
|
+
|
659
|
+
# _sorting is appended to avoid name clashes with manual joins eg. overridden filters
|
660
|
+
order_by_query = "#{associations.last.name}_sorting.#{column_name} #{direction}"
|
661
|
+
records = records.joins(joins_query).order(order_by_query)
|
662
|
+
else
|
663
|
+
records = records.order(field => direction)
|
664
|
+
end
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
records
|
669
|
+
end
|
670
|
+
|
671
|
+
def _lookup_association_chain(model_names)
|
672
|
+
associations = []
|
673
|
+
model_names.inject do |prev, current|
|
674
|
+
association = prev.classify.constantize.reflect_on_all_associations.detect do |assoc|
|
675
|
+
assoc.name.to_s.downcase == current.downcase
|
676
|
+
end
|
677
|
+
associations << association
|
678
|
+
association.class_name
|
679
|
+
end
|
680
|
+
|
681
|
+
associations
|
682
|
+
end
|
683
|
+
|
684
|
+
def _build_joins(associations)
|
685
|
+
joins = []
|
686
|
+
|
687
|
+
associations.inject do |prev, current|
|
688
|
+
joins << "LEFT JOIN #{current.table_name} AS #{current.name}_sorting ON #{current.name}_sorting.id = #{prev.table_name}.#{current.foreign_key}"
|
689
|
+
current
|
690
|
+
end
|
691
|
+
joins.join("\n")
|
692
|
+
end
|
693
|
+
|
694
|
+
def apply_filter(records, filter, value, options = {})
|
695
|
+
strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
|
696
|
+
|
697
|
+
if strategy
|
698
|
+
if strategy.is_a?(Symbol) || strategy.is_a?(String)
|
699
|
+
send(strategy, records, value, options)
|
700
|
+
else
|
701
|
+
strategy.call(records, value, options)
|
702
|
+
end
|
703
|
+
else
|
704
|
+
records.where(filter => value)
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
def apply_filters(records, filters, options = {})
|
709
|
+
required_includes = []
|
710
|
+
|
711
|
+
if filters
|
712
|
+
filters.each do |filter, value|
|
713
|
+
if _relationships.include?(filter)
|
714
|
+
if _relationships[filter].belongs_to?
|
715
|
+
records = apply_filter(records, _relationships[filter].foreign_key, value, options)
|
716
|
+
else
|
717
|
+
required_includes.push(filter.to_s)
|
718
|
+
records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
|
719
|
+
end
|
720
|
+
else
|
721
|
+
records = apply_filter(records, filter, value, options)
|
722
|
+
end
|
723
|
+
end
|
724
|
+
end
|
725
|
+
|
726
|
+
if required_includes.any?
|
727
|
+
records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(self, required_includes, force_eager_load: true)))
|
728
|
+
end
|
729
|
+
|
730
|
+
records
|
731
|
+
end
|
732
|
+
|
733
|
+
def filter_records(filters, options, records = records(options))
|
734
|
+
records = apply_filters(records, filters, options)
|
735
|
+
apply_includes(records, options)
|
736
|
+
end
|
737
|
+
|
738
|
+
def sort_records(records, order_options, context = {})
|
739
|
+
apply_sort(records, order_options, context)
|
740
|
+
end
|
741
|
+
|
742
|
+
# Assumes ActiveRecord's counting. Override if you need a different counting method
|
743
|
+
def count_records(records)
|
744
|
+
records.count(:all)
|
745
|
+
end
|
746
|
+
|
747
|
+
def find_count(filters, options = {})
|
748
|
+
count_records(filter_records(filters, options))
|
749
|
+
end
|
750
|
+
|
751
|
+
def find(filters, options = {})
|
752
|
+
resources_for(find_records(filters, options), options[:context])
|
753
|
+
end
|
754
|
+
|
755
|
+
def resources_for(records, context)
|
756
|
+
records.collect do |model|
|
757
|
+
resource_class = self.resource_for_model(model)
|
758
|
+
resource_class.new(model, context)
|
759
|
+
end
|
760
|
+
end
|
761
|
+
|
762
|
+
def find_by_keys(keys, options = {})
|
763
|
+
context = options[:context]
|
764
|
+
records = records(options)
|
765
|
+
records = apply_includes(records, options)
|
766
|
+
models = records.where({_primary_key => keys})
|
767
|
+
models.collect do |model|
|
768
|
+
self.resource_for_model(model).new(model, context)
|
769
|
+
end
|
770
|
+
end
|
771
|
+
|
772
|
+
def find_serialized_with_caching(filters_or_source, serializer, options = {})
|
773
|
+
if filters_or_source.is_a?(ActiveRecord::Relation)
|
774
|
+
records = filters_or_source
|
775
|
+
elsif _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
|
776
|
+
records = find_records(filters_or_source, options.except(:include_directives))
|
777
|
+
else
|
778
|
+
records = find(filters_or_source, options)
|
779
|
+
end
|
780
|
+
cached_resources_for(records, serializer, options)
|
781
|
+
end
|
782
|
+
|
783
|
+
def find_by_key(key, options = {})
|
784
|
+
context = options[:context]
|
785
|
+
records = find_records({_primary_key => key}, options.except(:paginator, :sort_criteria))
|
786
|
+
model = records.first
|
787
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
|
788
|
+
self.resource_for_model(model).new(model, context)
|
789
|
+
end
|
790
|
+
|
791
|
+
def find_by_key_serialized_with_caching(key, serializer, options = {})
|
792
|
+
if _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
|
793
|
+
results = find_serialized_with_caching({_primary_key => key}, serializer, options)
|
794
|
+
result = results.first
|
795
|
+
fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil?
|
796
|
+
return result
|
797
|
+
else
|
798
|
+
resource = find_by_key(key, options)
|
799
|
+
return cached_resources_for([resource], serializer, options).first
|
800
|
+
end
|
801
|
+
end
|
802
|
+
|
803
|
+
# Override this method if you want to customize the relation for
|
804
|
+
# finder methods (find, find_by_key, find_serialized_with_caching)
|
805
|
+
def records(_options = {})
|
806
|
+
_model_class.all
|
807
|
+
end
|
808
|
+
|
809
|
+
def verify_filters(filters, context = nil)
|
810
|
+
verified_filters = {}
|
811
|
+
filters.each do |filter, raw_value|
|
812
|
+
verified_filter = verify_filter(filter, raw_value, context)
|
813
|
+
verified_filters[verified_filter[0]] = verified_filter[1]
|
814
|
+
end
|
815
|
+
verified_filters
|
816
|
+
end
|
817
|
+
|
818
|
+
def is_filter_relationship?(filter)
|
819
|
+
filter == _type || _relationships.include?(filter)
|
820
|
+
end
|
821
|
+
|
822
|
+
def verify_filter(filter, raw, context = nil)
|
823
|
+
filter_values = []
|
824
|
+
if raw.present?
|
825
|
+
begin
|
826
|
+
filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
|
827
|
+
rescue CSV::MalformedCSVError
|
828
|
+
filter_values << raw
|
829
|
+
end
|
830
|
+
end
|
831
|
+
|
832
|
+
strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
|
833
|
+
|
834
|
+
if strategy
|
835
|
+
if strategy.is_a?(Symbol) || strategy.is_a?(String)
|
836
|
+
values = send(strategy, filter_values, context)
|
837
|
+
else
|
838
|
+
values = strategy.call(filter_values, context)
|
839
|
+
end
|
840
|
+
[filter, values]
|
841
|
+
else
|
842
|
+
if is_filter_relationship?(filter)
|
843
|
+
verify_relationship_filter(filter, filter_values, context)
|
844
|
+
else
|
845
|
+
verify_custom_filter(filter, filter_values, context)
|
846
|
+
end
|
847
|
+
end
|
848
|
+
end
|
849
|
+
|
850
|
+
def key_type(key_type)
|
851
|
+
@_resource_key_type = key_type
|
852
|
+
end
|
853
|
+
|
854
|
+
def resource_key_type
|
855
|
+
@_resource_key_type ||= JSONAPI.configuration.resource_key_type
|
856
|
+
end
|
857
|
+
|
858
|
+
def verify_key(key, context = nil)
|
859
|
+
key_type = resource_key_type
|
860
|
+
|
861
|
+
case key_type
|
862
|
+
when :integer
|
863
|
+
return if key.nil?
|
864
|
+
Integer(key)
|
865
|
+
when :string
|
866
|
+
return if key.nil?
|
867
|
+
if key.to_s.include?(',')
|
868
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
869
|
+
else
|
870
|
+
key
|
871
|
+
end
|
872
|
+
when :uuid
|
873
|
+
return if key.nil?
|
874
|
+
if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
|
875
|
+
key
|
876
|
+
else
|
877
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
878
|
+
end
|
879
|
+
else
|
880
|
+
key_type.call(key, context)
|
881
|
+
end
|
882
|
+
rescue
|
883
|
+
raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
|
884
|
+
end
|
885
|
+
|
886
|
+
# override to allow for key processing and checking
|
887
|
+
def verify_keys(keys, context = nil)
|
888
|
+
return keys.collect do |key|
|
889
|
+
verify_key(key, context)
|
890
|
+
end
|
891
|
+
end
|
892
|
+
|
893
|
+
# Either add a custom :verify labmda or override verify_custom_filter to allow for custom filters
|
894
|
+
def verify_custom_filter(filter, value, _context = nil)
|
895
|
+
[filter, value]
|
896
|
+
end
|
897
|
+
|
898
|
+
# Either add a custom :verify labmda or override verify_relationship_filter to allow for custom
|
899
|
+
# relationship logic, such as uuids, multiple keys or permission checks on keys
|
900
|
+
def verify_relationship_filter(filter, raw, _context = nil)
|
901
|
+
[filter, raw]
|
902
|
+
end
|
903
|
+
|
904
|
+
# quasi private class methods
|
905
|
+
def _attribute_options(attr)
|
906
|
+
default_attribute_options.merge(@_attributes[attr])
|
907
|
+
end
|
908
|
+
|
909
|
+
def _updatable_relationships
|
910
|
+
@_relationships.map { |key, _relationship| key }
|
911
|
+
end
|
912
|
+
|
913
|
+
def _relationship(type)
|
914
|
+
type = type.to_sym
|
915
|
+
@_relationships[type]
|
916
|
+
end
|
917
|
+
|
918
|
+
def _model_name
|
919
|
+
if _abstract
|
920
|
+
return ''
|
921
|
+
else
|
922
|
+
return @_model_name.to_s if defined?(@_model_name)
|
923
|
+
class_name = self.name
|
924
|
+
return '' if class_name.nil?
|
925
|
+
@_model_name = class_name.demodulize.sub(/Resource$/, '')
|
926
|
+
return @_model_name.to_s
|
927
|
+
end
|
928
|
+
end
|
929
|
+
|
930
|
+
def _primary_key
|
931
|
+
@_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
|
932
|
+
end
|
933
|
+
|
934
|
+
def _cache_field
|
935
|
+
@_cache_field ||= JSONAPI.configuration.default_resource_cache_field
|
936
|
+
end
|
937
|
+
|
938
|
+
def _table_name
|
939
|
+
@_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
|
940
|
+
end
|
941
|
+
|
942
|
+
def _as_parent_key
|
943
|
+
@_as_parent_key ||= "#{_type.to_s.singularize}_id"
|
944
|
+
end
|
945
|
+
|
946
|
+
def _allowed_filters
|
947
|
+
defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
|
948
|
+
end
|
949
|
+
|
950
|
+
def _paginator
|
951
|
+
@_paginator ||= JSONAPI.configuration.default_paginator
|
952
|
+
end
|
953
|
+
|
954
|
+
def paginator(paginator)
|
955
|
+
@_paginator = paginator
|
956
|
+
end
|
957
|
+
|
958
|
+
def abstract(val = true)
|
959
|
+
@abstract = val
|
960
|
+
end
|
961
|
+
|
962
|
+
def _abstract
|
963
|
+
@abstract
|
964
|
+
end
|
965
|
+
|
966
|
+
def immutable(val = true)
|
967
|
+
@immutable = val
|
968
|
+
end
|
969
|
+
|
970
|
+
def _immutable
|
971
|
+
@immutable
|
972
|
+
end
|
973
|
+
|
974
|
+
def mutable?
|
975
|
+
!@immutable
|
976
|
+
end
|
977
|
+
|
978
|
+
def caching(val = true)
|
979
|
+
@caching = val
|
980
|
+
end
|
981
|
+
|
982
|
+
def _caching
|
983
|
+
@caching
|
984
|
+
end
|
985
|
+
|
986
|
+
def caching?
|
987
|
+
@caching && !JSONAPI.configuration.resource_cache.nil?
|
988
|
+
end
|
989
|
+
|
990
|
+
def attribute_caching_context(context)
|
991
|
+
nil
|
992
|
+
end
|
993
|
+
|
994
|
+
def _model_class
|
995
|
+
return nil if _abstract
|
996
|
+
|
997
|
+
return @model_class if @model_class
|
998
|
+
|
999
|
+
model_name = _model_name
|
1000
|
+
return nil if model_name.to_s.blank?
|
1001
|
+
|
1002
|
+
@model_class = model_name.to_s.safe_constantize
|
1003
|
+
if @model_class.nil?
|
1004
|
+
warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this is a base Resource declare it as abstract."
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
@model_class
|
1008
|
+
end
|
1009
|
+
|
1010
|
+
def _allowed_filter?(filter)
|
1011
|
+
!_allowed_filters[filter].nil?
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
def module_path
|
1015
|
+
if name == 'JSONAPI::Resource'
|
1016
|
+
''
|
1017
|
+
else
|
1018
|
+
name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
def default_sort
|
1023
|
+
[{field: 'id', direction: :asc}]
|
1024
|
+
end
|
1025
|
+
|
1026
|
+
def construct_order_options(sort_params)
|
1027
|
+
sort_params ||= default_sort
|
1028
|
+
|
1029
|
+
return {} unless sort_params
|
1030
|
+
|
1031
|
+
sort_params.each_with_object({}) do |sort, order_hash|
|
1032
|
+
field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s
|
1033
|
+
order_hash[field] = sort[:direction]
|
1034
|
+
end
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
def _add_relationship(klass, *attrs)
|
1038
|
+
options = attrs.extract_options!
|
1039
|
+
options[:parent_resource] = self
|
1040
|
+
|
1041
|
+
attrs.each do |relationship_name|
|
1042
|
+
check_reserved_relationship_name(relationship_name)
|
1043
|
+
check_duplicate_relationship_name(relationship_name)
|
1044
|
+
|
1045
|
+
JSONAPI::RelationshipBuilder.new(klass, _model_class, options)
|
1046
|
+
.define_relationship_methods(relationship_name.to_sym)
|
1047
|
+
end
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
# Allows JSONAPI::RelationshipBuilder to access metaprogramming hooks
|
1051
|
+
def inject_method_definition(name, body)
|
1052
|
+
define_method(name, body)
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
def register_relationship(name, relationship_object)
|
1056
|
+
@_relationships[name] = relationship_object
|
1057
|
+
end
|
1058
|
+
|
1059
|
+
private
|
1060
|
+
|
1061
|
+
def cached_resources_for(records, serializer, options)
|
1062
|
+
if records.is_a?(Array) && records.all?{|rec| rec.is_a?(JSONAPI::Resource)}
|
1063
|
+
resources = records.map{|r| [r.id, r] }.to_h
|
1064
|
+
elsif self.caching?
|
1065
|
+
t = _model_class.arel_table
|
1066
|
+
cache_ids = pluck_arel_attributes(records, t[_primary_key], t[_cache_field])
|
1067
|
+
resources = CachedResourceFragment.fetch_fragments(self, serializer, options[:context], cache_ids)
|
1068
|
+
else
|
1069
|
+
resources = resources_for(records, options[:context]).map{|r| [r.id, r] }.to_h
|
1070
|
+
end
|
1071
|
+
|
1072
|
+
preload_included_fragments(resources, records, serializer, options)
|
1073
|
+
|
1074
|
+
resources.values
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
def find_records(filters, options = {})
|
1078
|
+
context = options[:context]
|
1079
|
+
|
1080
|
+
records = filter_records(filters, options)
|
1081
|
+
|
1082
|
+
sort_criteria = options.fetch(:sort_criteria) { [] }
|
1083
|
+
order_options = construct_order_options(sort_criteria)
|
1084
|
+
records = sort_records(records, order_options, context)
|
1085
|
+
|
1086
|
+
records = apply_pagination(records, options[:paginator], order_options)
|
1087
|
+
|
1088
|
+
records
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
def check_reserved_resource_name(type, name)
|
1092
|
+
if [:ids, :types, :hrefs, :links].include?(type)
|
1093
|
+
warn "[NAME COLLISION] `#{name}` is a reserved resource name."
|
1094
|
+
return
|
1095
|
+
end
|
1096
|
+
end
|
1097
|
+
|
1098
|
+
def check_reserved_attribute_name(name)
|
1099
|
+
# Allow :id since it can be used to specify the format. Since it is a method on the base Resource
|
1100
|
+
# an attribute method won't be created for it.
|
1101
|
+
if [:type].include?(name.to_sym)
|
1102
|
+
warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
|
1103
|
+
end
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
def check_reserved_relationship_name(name)
|
1107
|
+
if [:id, :ids, :type, :types].include?(name.to_sym)
|
1108
|
+
warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
def check_duplicate_relationship_name(name)
|
1113
|
+
if _relationships.include?(name.to_sym)
|
1114
|
+
warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
|
1115
|
+
end
|
1116
|
+
end
|
1117
|
+
|
1118
|
+
def check_duplicate_attribute_name(name)
|
1119
|
+
if _attributes.include?(name.to_sym)
|
1120
|
+
warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
|
1121
|
+
end
|
1122
|
+
end
|
1123
|
+
|
1124
|
+
def preload_included_fragments(resources, records, serializer, options)
|
1125
|
+
return if resources.empty?
|
1126
|
+
res_ids = resources.keys
|
1127
|
+
|
1128
|
+
include_directives = options[:include_directives]
|
1129
|
+
return unless include_directives
|
1130
|
+
|
1131
|
+
context = options[:context]
|
1132
|
+
|
1133
|
+
# For each association, including indirect associations, find the target record ids.
|
1134
|
+
# Even if a target class doesn't have caching enabled, we still have to look up
|
1135
|
+
# and match the target ids here, because we can't use ActiveRecord#includes.
|
1136
|
+
#
|
1137
|
+
# Note that `paths` returns partial paths before complete paths, so e.g. the partial
|
1138
|
+
# fragments for posts.comments will exist before we start working with posts.comments.author
|
1139
|
+
target_resources = {}
|
1140
|
+
include_directives.paths.each do |path|
|
1141
|
+
# If path is [:posts, :comments, :author], then...
|
1142
|
+
pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at]
|
1143
|
+
pluck_attrs << self._model_class.arel_table[self._primary_key]
|
1144
|
+
|
1145
|
+
relation = records
|
1146
|
+
.except(:limit, :offset, :order)
|
1147
|
+
.where({_primary_key => res_ids})
|
1148
|
+
|
1149
|
+
# These are updated as we iterate through the association path; afterwards they will
|
1150
|
+
# refer to the final resource on the path, i.e. the actual resource to find in the cache.
|
1151
|
+
# So e.g. if path is [:posts, :comments, :author], then after iteration...
|
1152
|
+
parent_klass = nil # Comment
|
1153
|
+
klass = self # Person
|
1154
|
+
relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author
|
1155
|
+
table = nil # people
|
1156
|
+
assocs_path = [] # [ :posts, :approved_comments, :author ]
|
1157
|
+
ar_hash = nil # { :posts => { :approved_comments => :author } }
|
1158
|
+
|
1159
|
+
# For each step on the path, figure out what the actual table name/alias in the join
|
1160
|
+
# will be, and include the primary key of that table in our list of fields to select
|
1161
|
+
non_polymorphic = true
|
1162
|
+
path.each do |elem|
|
1163
|
+
relationship = klass._relationships[elem]
|
1164
|
+
if relationship.polymorphic
|
1165
|
+
# Can't preload through a polymorphic belongs_to association, ResourceSerializer
|
1166
|
+
# will just have to bypass the cache and load the real Resource.
|
1167
|
+
non_polymorphic = false
|
1168
|
+
break
|
1169
|
+
end
|
1170
|
+
assocs_path << relationship.relation_name(options).to_sym
|
1171
|
+
# Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
|
1172
|
+
ar_hash = assocs_path.reverse.reduce{|memo, step| { step => memo } }
|
1173
|
+
# We can't just look up the table name from the resource class, because Arel could
|
1174
|
+
# have used a table alias if the relation includes a self-reference.
|
1175
|
+
join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node|
|
1176
|
+
arel_node.is_a?(Arel::Nodes::InnerJoin)
|
1177
|
+
end
|
1178
|
+
table = join_source.left
|
1179
|
+
parent_klass = klass
|
1180
|
+
klass = relationship.resource_klass
|
1181
|
+
pluck_attrs << table[klass._primary_key]
|
1182
|
+
end
|
1183
|
+
next unless non_polymorphic
|
1184
|
+
|
1185
|
+
# Pre-fill empty hashes for each resource up to the end of the path.
|
1186
|
+
# This allows us to later distinguish between a preload that returned nothing
|
1187
|
+
# vs. a preload that never ran.
|
1188
|
+
prefilling_resources = resources.values
|
1189
|
+
path.each do |rel_name|
|
1190
|
+
rel_name = serializer.key_formatter.format(rel_name)
|
1191
|
+
prefilling_resources.map! do |res|
|
1192
|
+
res.preloaded_fragments[rel_name] ||= {}
|
1193
|
+
res.preloaded_fragments[rel_name].values
|
1194
|
+
end
|
1195
|
+
prefilling_resources.flatten!(1)
|
1196
|
+
end
|
1197
|
+
|
1198
|
+
pluck_attrs << table[klass._cache_field] if klass.caching?
|
1199
|
+
relation = relation.joins(ar_hash)
|
1200
|
+
if relationship.is_a?(JSONAPI::Relationship::ToMany)
|
1201
|
+
# Rails doesn't include order clauses in `joins`, so we have to add that manually here.
|
1202
|
+
# FIXME Should find a better way to reflect on relationship ordering. :-(
|
1203
|
+
relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders)
|
1204
|
+
end
|
1205
|
+
|
1206
|
+
# [[post id, comment id, author id, author updated_at], ...]
|
1207
|
+
id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs)
|
1208
|
+
|
1209
|
+
target_resources[klass.name] ||= {}
|
1210
|
+
|
1211
|
+
if klass.caching?
|
1212
|
+
sub_cache_ids = id_rows
|
1213
|
+
.map{|row| row.last(2) }
|
1214
|
+
.reject{|row| target_resources[klass.name].has_key?(row.first) }
|
1215
|
+
.uniq
|
1216
|
+
target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments(
|
1217
|
+
klass, serializer, context, sub_cache_ids
|
1218
|
+
)
|
1219
|
+
else
|
1220
|
+
sub_res_ids = id_rows
|
1221
|
+
.map(&:last)
|
1222
|
+
.reject{|id| target_resources[klass.name].has_key?(id) }
|
1223
|
+
.uniq
|
1224
|
+
found = klass.find({klass._primary_key => sub_res_ids}, context: options[:context])
|
1225
|
+
target_resources[klass.name].merge! found.map{|r| [r.id, r] }.to_h
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
id_rows.each do |row|
|
1229
|
+
res = resources[row.first]
|
1230
|
+
path.each_with_index do |rel_name, index|
|
1231
|
+
rel_name = serializer.key_formatter.format(rel_name)
|
1232
|
+
rel_id = row[index+1]
|
1233
|
+
assoc_rels = res.preloaded_fragments[rel_name]
|
1234
|
+
if index == path.length - 1
|
1235
|
+
assoc_rels[rel_id] = target_resources[klass.name].fetch(rel_id)
|
1236
|
+
else
|
1237
|
+
res = assoc_rels[rel_id]
|
1238
|
+
end
|
1239
|
+
end
|
1240
|
+
end
|
1241
|
+
end
|
1242
|
+
end
|
1243
|
+
|
1244
|
+
def pluck_arel_attributes(relation, *attrs)
|
1245
|
+
conn = relation.connection
|
1246
|
+
quoted_attrs = attrs.map do |attr|
|
1247
|
+
quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name)
|
1248
|
+
quoted_column = conn.quote_column_name(attr.name)
|
1249
|
+
"#{quoted_table}.#{quoted_column}"
|
1250
|
+
end
|
1251
|
+
relation.pluck(*quoted_attrs)
|
1252
|
+
end
|
1253
|
+
end
|
1254
|
+
end
|
1255
|
+
end
|