jsonapi_compliable 0.11.34 → 1.0.alpha.2
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 +5 -5
- data/.ruby-version +1 -1
- data/.travis.yml +1 -2
- data/Rakefile +7 -3
- data/jsonapi_compliable.gemspec +7 -3
- data/lib/generators/jsonapi/resource_generator.rb +8 -79
- data/lib/generators/jsonapi/templates/application_resource.rb.erb +2 -1
- data/lib/generators/jsonapi/templates/controller.rb.erb +19 -64
- data/lib/generators/jsonapi/templates/resource.rb.erb +5 -47
- data/lib/generators/jsonapi/templates/resource_reads_spec.rb.erb +62 -0
- data/lib/generators/jsonapi/templates/resource_writes_spec.rb.erb +63 -0
- data/lib/jsonapi_compliable.rb +87 -18
- data/lib/jsonapi_compliable/adapters/abstract.rb +202 -45
- data/lib/jsonapi_compliable/adapters/active_record.rb +6 -130
- data/lib/jsonapi_compliable/adapters/active_record/base.rb +247 -0
- data/lib/jsonapi_compliable/adapters/active_record/belongs_to_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/has_many_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/has_one_sideload.rb +17 -0
- data/lib/jsonapi_compliable/adapters/active_record/inferrence.rb +12 -0
- data/lib/jsonapi_compliable/adapters/active_record/many_to_many_sideload.rb +30 -0
- data/lib/jsonapi_compliable/adapters/null.rb +177 -6
- data/lib/jsonapi_compliable/base.rb +33 -320
- data/lib/jsonapi_compliable/context.rb +16 -0
- data/lib/jsonapi_compliable/deserializer.rb +14 -39
- data/lib/jsonapi_compliable/errors.rb +227 -24
- data/lib/jsonapi_compliable/extensions/extra_attribute.rb +3 -1
- data/lib/jsonapi_compliable/filter_operators.rb +25 -0
- data/lib/jsonapi_compliable/hash_renderer.rb +57 -0
- data/lib/jsonapi_compliable/query.rb +190 -202
- data/lib/jsonapi_compliable/rails.rb +12 -6
- data/lib/jsonapi_compliable/railtie.rb +64 -0
- data/lib/jsonapi_compliable/renderer.rb +60 -0
- data/lib/jsonapi_compliable/resource.rb +35 -663
- data/lib/jsonapi_compliable/resource/configuration.rb +239 -0
- data/lib/jsonapi_compliable/resource/dsl.rb +138 -0
- data/lib/jsonapi_compliable/resource/interface.rb +32 -0
- data/lib/jsonapi_compliable/resource/polymorphism.rb +68 -0
- data/lib/jsonapi_compliable/resource/sideloading.rb +102 -0
- data/lib/jsonapi_compliable/resource_proxy.rb +127 -0
- data/lib/jsonapi_compliable/responders.rb +19 -0
- data/lib/jsonapi_compliable/runner.rb +25 -0
- data/lib/jsonapi_compliable/scope.rb +37 -79
- data/lib/jsonapi_compliable/scoping/extra_attributes.rb +29 -0
- data/lib/jsonapi_compliable/scoping/filter.rb +39 -58
- data/lib/jsonapi_compliable/scoping/filterable.rb +9 -14
- data/lib/jsonapi_compliable/scoping/paginate.rb +9 -3
- data/lib/jsonapi_compliable/scoping/sort.rb +16 -4
- data/lib/jsonapi_compliable/sideload.rb +221 -347
- data/lib/jsonapi_compliable/sideload/belongs_to.rb +34 -0
- data/lib/jsonapi_compliable/sideload/has_many.rb +16 -0
- data/lib/jsonapi_compliable/sideload/has_one.rb +9 -0
- data/lib/jsonapi_compliable/sideload/many_to_many.rb +24 -0
- data/lib/jsonapi_compliable/sideload/polymorphic_belongs_to.rb +108 -0
- data/lib/jsonapi_compliable/stats/payload.rb +4 -8
- data/lib/jsonapi_compliable/types.rb +172 -0
- data/lib/jsonapi_compliable/util/attribute_check.rb +88 -0
- data/lib/jsonapi_compliable/util/persistence.rb +29 -7
- data/lib/jsonapi_compliable/util/relationship_payload.rb +4 -4
- data/lib/jsonapi_compliable/util/render_options.rb +4 -32
- data/lib/jsonapi_compliable/util/serializer_attributes.rb +98 -0
- data/lib/jsonapi_compliable/util/validation_response.rb +15 -9
- data/lib/jsonapi_compliable/version.rb +1 -1
- metadata +105 -24
- data/lib/generators/jsonapi/field_generator.rb +0 -0
- data/lib/generators/jsonapi/templates/create_request_spec.rb.erb +0 -29
- data/lib/generators/jsonapi/templates/destroy_request_spec.rb.erb +0 -20
- data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +0 -22
- data/lib/generators/jsonapi/templates/payload.rb.erb +0 -39
- data/lib/generators/jsonapi/templates/serializer.rb.erb +0 -25
- data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +0 -19
- data/lib/generators/jsonapi/templates/update_request_spec.rb.erb +0 -33
- data/lib/jsonapi_compliable/adapters/active_record_sideloading.rb +0 -152
- data/lib/jsonapi_compliable/scoping/extra_fields.rb +0 -58
@@ -28,9 +28,12 @@ module JsonapiCompliable
|
|
28
28
|
# aliases. If valid, call either the default or custom filtering logic.
|
29
29
|
# @return the scope we are chaining/modifying
|
30
30
|
def apply
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
if missing_required_filters.any?
|
32
|
+
raise Errors::RequiredFilter.new(resource, missing_required_filters)
|
33
|
+
end
|
34
|
+
|
35
|
+
each_filter do |filter, operator, value|
|
36
|
+
@scope = filter_scope(filter, operator, value)
|
34
37
|
end
|
35
38
|
|
36
39
|
@scope
|
@@ -38,74 +41,52 @@ module JsonapiCompliable
|
|
38
41
|
|
39
42
|
private
|
40
43
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
if custom_scope = filter.values.first[
|
44
|
+
def filter_scope(filter, operator, value)
|
45
|
+
operator = operator.to_s.gsub('!', 'not_').to_sym
|
46
|
+
|
47
|
+
if custom_scope = filter.values.first[operator]
|
45
48
|
custom_scope.call(@scope, value, resource.context)
|
46
49
|
else
|
47
|
-
|
50
|
+
filter_via_adapter(filter, operator, value)
|
48
51
|
end
|
49
52
|
end
|
50
53
|
|
51
|
-
def
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
value = parse_string_arrays(value)
|
56
|
-
value = normalize_string_values(value)
|
57
|
-
yield filter, value
|
58
|
-
end
|
59
|
-
end
|
54
|
+
def filter_via_adapter(filter, operator, value)
|
55
|
+
type_name = Types.name_for(filter.values.first[:type])
|
56
|
+
method = :"filter_#{type_name}_#{operator}"
|
57
|
+
attribute = filter.keys.first
|
60
58
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
# {{{ "id": 1 }}} becomes { 'id' => 1 }
|
67
|
-
def parse_string_arrays(value)
|
68
|
-
if value.is_a?(String)# && value[0..2] != '{{{'
|
69
|
-
# Escaped JSON
|
70
|
-
if value[0..2] == '{{{'
|
71
|
-
value = value.sub('{{', '').sub('}}', '')
|
72
|
-
value = JSON.parse(value)
|
73
|
-
else
|
74
|
-
# Find the quoted strings
|
75
|
-
quotes = value.scan(/{{.*?}}/)
|
76
|
-
# remove them from the rest
|
77
|
-
quotes.each { |q| value.gsub!(q, '') }
|
78
|
-
# remove the quote characters from the quoted strings
|
79
|
-
quotes.each { |q| q.gsub!('{{', '').gsub!('}}', '') }
|
80
|
-
# merge everything back together into an array
|
81
|
-
value = Array(value.split(',')) + quotes
|
82
|
-
# remove any blanks that are left
|
83
|
-
value.reject! { |v| v.length.zero? }
|
84
|
-
value = value[0] if value.length == 1
|
85
|
-
end
|
59
|
+
if resource.adapter.respond_to?(method)
|
60
|
+
resource.adapter.send(method, @scope, attribute, value)
|
61
|
+
else
|
62
|
+
raise Errors::AdapterNotImplemented.new \
|
63
|
+
resource.adapter, attribute, method
|
86
64
|
end
|
87
|
-
value
|
88
65
|
end
|
89
66
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
value.
|
97
|
-
|
98
|
-
|
67
|
+
def each_filter
|
68
|
+
filter_param.each_pair do |param_name, param_value|
|
69
|
+
filter = find_filter!(param_name)
|
70
|
+
param_value = { eq: param_value } unless param_value.is_a?(Hash)
|
71
|
+
value = param_value.values.first
|
72
|
+
operator = param_value.keys.first
|
73
|
+
value = param_value.values.first unless filter.values[0][:type] == :hash
|
74
|
+
value = value.split(',') if value.is_a?(String) && value.include?(',')
|
75
|
+
value = coerce_types(param_name.to_sym, value)
|
76
|
+
yield filter, operator, value
|
99
77
|
end
|
100
78
|
end
|
101
79
|
|
102
|
-
def
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
80
|
+
def coerce_types(name, value)
|
81
|
+
type_name = @resource.all_attributes[name][:type]
|
82
|
+
is_array = type_name.to_s.starts_with?('array_of') ||
|
83
|
+
Types[type_name][:canonical_name] == :array
|
84
|
+
|
85
|
+
if is_array
|
86
|
+
@resource.typecast(name, value, :filterable)
|
107
87
|
else
|
108
|
-
value
|
88
|
+
value = value.nil? || value.is_a?(Hash) ? [value] : Array(value)
|
89
|
+
value.map { |v| @resource.typecast(name, v, :filterable) }
|
109
90
|
end
|
110
91
|
end
|
111
92
|
end
|
@@ -4,34 +4,29 @@ module JsonapiCompliable
|
|
4
4
|
# @api private
|
5
5
|
def find_filter(name)
|
6
6
|
find_filter!(name)
|
7
|
-
rescue JsonapiCompliable::Errors::
|
7
|
+
rescue JsonapiCompliable::Errors::AttributeError
|
8
8
|
nil
|
9
9
|
end
|
10
10
|
|
11
11
|
# @api private
|
12
12
|
def find_filter!(name)
|
13
|
-
|
14
|
-
|
15
|
-
raise JsonapiCompliable::Errors::BadFilter unless filter_name
|
16
|
-
if guard = filter_value[:if]
|
17
|
-
raise JsonapiCompliable::Errors::BadFilter if resource.context.send(guard) == false
|
18
|
-
end
|
19
|
-
{ filter_name => filter_value }
|
13
|
+
resource.class.get_attr!(name, :filterable, request: true)
|
14
|
+
{ name => resource.filters[name] }
|
20
15
|
end
|
21
16
|
|
22
17
|
# @api private
|
23
18
|
def filter_param
|
24
|
-
query_hash[:filter]
|
19
|
+
query_hash[:filter] || {}
|
25
20
|
end
|
26
21
|
|
27
22
|
def missing_required_filters
|
28
|
-
|
23
|
+
required_attributes - filter_param.keys
|
29
24
|
end
|
30
25
|
|
31
|
-
def
|
32
|
-
resource.
|
33
|
-
|
34
|
-
end
|
26
|
+
def required_attributes
|
27
|
+
resource.attributes.map do |k, v|
|
28
|
+
k if v[:filterable] == :required
|
29
|
+
end.compact
|
35
30
|
end
|
36
31
|
|
37
32
|
def required_filters_provided?
|
@@ -31,6 +31,8 @@ module JsonapiCompliable
|
|
31
31
|
if size > MAX_PAGE_SIZE
|
32
32
|
raise JsonapiCompliable::Errors::UnsupportedPageSize
|
33
33
|
.new(size, MAX_PAGE_SIZE)
|
34
|
+
elsif requested? && @opts[:sideload_parent_length].to_i > 1
|
35
|
+
raise JsonapiCompliable::Errors::UnsupportedPagination
|
34
36
|
else
|
35
37
|
super
|
36
38
|
end
|
@@ -42,8 +44,8 @@ module JsonapiCompliable
|
|
42
44
|
#
|
43
45
|
# @return [Boolean] should we apply this logic?
|
44
46
|
def apply?
|
45
|
-
if @opts[:
|
46
|
-
|
47
|
+
if @opts[:default_paginate] == false
|
48
|
+
requested?
|
47
49
|
else
|
48
50
|
true
|
49
51
|
end
|
@@ -66,12 +68,16 @@ module JsonapiCompliable
|
|
66
68
|
|
67
69
|
private
|
68
70
|
|
71
|
+
def requested?
|
72
|
+
not [page_param[:size], page_param[:number]].all?(&:nil?)
|
73
|
+
end
|
74
|
+
|
69
75
|
def page_param
|
70
76
|
@page_param ||= (query_hash[:page] || {})
|
71
77
|
end
|
72
78
|
|
73
79
|
def number
|
74
|
-
(page_param[:number] ||
|
80
|
+
(page_param[:number] || 1).to_i
|
75
81
|
end
|
76
82
|
|
77
83
|
def size
|
@@ -15,14 +15,18 @@ module JsonapiCompliable
|
|
15
15
|
class Scoping::Sort < Scoping::Base
|
16
16
|
# @return [Proc, Nil] The custom proc specified by Resource DSL
|
17
17
|
def custom_scope
|
18
|
-
resource.
|
18
|
+
resource.sort_all
|
19
19
|
end
|
20
20
|
|
21
21
|
# Apply default scope logic via Resource adapter
|
22
22
|
# @return the scope we are chaining/modifying
|
23
23
|
def apply_standard_scope
|
24
24
|
each_sort do |attribute, direction|
|
25
|
-
|
25
|
+
if sort = resource.sorts[attribute]
|
26
|
+
@scope = sort.call(@scope, direction)
|
27
|
+
else
|
28
|
+
@scope = resource.adapter.order(@scope, attribute, direction)
|
29
|
+
end
|
26
30
|
end
|
27
31
|
@scope
|
28
32
|
end
|
@@ -41,12 +45,20 @@ module JsonapiCompliable
|
|
41
45
|
|
42
46
|
def each_sort
|
43
47
|
sort_param.each do |sort_hash|
|
44
|
-
|
48
|
+
attribute = sort_hash.keys.first
|
49
|
+
direction = sort_hash.values.first
|
50
|
+
yield attribute, direction
|
45
51
|
end
|
46
52
|
end
|
47
53
|
|
48
54
|
def sort_param
|
49
|
-
@sort_param ||=
|
55
|
+
@sort_param ||= begin
|
56
|
+
if query_hash[:sort].blank?
|
57
|
+
resource.default_sort
|
58
|
+
else
|
59
|
+
query_hash[:sort]
|
60
|
+
end
|
61
|
+
end
|
50
62
|
end
|
51
63
|
end
|
52
64
|
end
|
@@ -1,220 +1,179 @@
|
|
1
1
|
module JsonapiCompliable
|
2
|
-
# @attr_reader [Symbol] name The name of the sideload
|
3
|
-
# @attr_reader [Class] resource_class The corresponding Resource class
|
4
|
-
# @attr_reader [Boolean] polymorphic Is this a polymorphic sideload?
|
5
|
-
# @attr_reader [Hash] polymorphic_groups The subgroups, when polymorphic
|
6
|
-
# @attr_reader [Hash] sideloads The associated sibling sideloads
|
7
|
-
# @attr_reader [Proc] scope_proc The configured 'scope' block
|
8
|
-
# @attr_reader [Proc] assign_proc The configured 'assign' block
|
9
|
-
# @attr_reader [Symbol] grouping_field The configured 'group_by' symbol
|
10
|
-
# @attr_reader [Symbol] foreign_key The attribute used to match objects - need not be a true database foreign key.
|
11
|
-
# @attr_reader [Symbol] primary_key The attribute used to match objects - need not be a true database primary key.
|
12
|
-
# @attr_reader [Symbol] type One of :has_many, :belongs_to, etc
|
13
2
|
class Sideload
|
3
|
+
HOOK_ACTIONS = [:save, :create, :update, :destroy, :disassociate]
|
4
|
+
TYPES = [:has_many, :belongs_to, :has_one, :many_to_many]
|
5
|
+
|
14
6
|
attr_reader :name,
|
15
7
|
:resource_class,
|
16
|
-
:
|
17
|
-
:polymorphic_groups,
|
18
|
-
:parent,
|
19
|
-
:sideloads,
|
20
|
-
:scope_proc,
|
21
|
-
:assign_proc,
|
22
|
-
:grouping_field,
|
8
|
+
:parent_resource_class,
|
23
9
|
:foreign_key,
|
24
10
|
:primary_key,
|
25
|
-
:
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
@
|
36
|
-
@
|
37
|
-
@
|
38
|
-
@
|
39
|
-
@
|
40
|
-
@
|
41
|
-
@
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
11
|
+
:parent,
|
12
|
+
:group_name
|
13
|
+
|
14
|
+
class_attribute :scope_proc,
|
15
|
+
:assign_proc,
|
16
|
+
:assign_each_proc,
|
17
|
+
:params_proc,
|
18
|
+
:pre_load_proc
|
19
|
+
|
20
|
+
def initialize(name, opts)
|
21
|
+
@name = name
|
22
|
+
@parent_resource_class = opts[:parent_resource]
|
23
|
+
@resource_class = opts[:resource]
|
24
|
+
@primary_key = opts[:primary_key]
|
25
|
+
@foreign_key = opts[:foreign_key]
|
26
|
+
@type = opts[:type]
|
27
|
+
@base_scope = opts[:base_scope]
|
28
|
+
@readable = opts[:readable]
|
29
|
+
@writable = opts[:writable]
|
30
|
+
@as = opts[:as]
|
31
|
+
|
32
|
+
# polymorphic-specific
|
33
|
+
@group_name = opts[:group_name]
|
34
|
+
@polymorphic_child = opts[:polymorphic_child]
|
35
|
+
@parent = opts[:parent]
|
36
|
+
if polymorphic_child?
|
37
|
+
parent.resource.polymorphic << resource_class
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.scope(&blk)
|
42
|
+
self.scope_proc = blk
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.assign(&blk)
|
46
|
+
self.assign_proc = blk
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.assign_each(&blk)
|
50
|
+
self.assign_each_proc = blk
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.params(&blk)
|
54
|
+
self.params_proc = blk
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.pre_load(&blk)
|
58
|
+
self.pre_load_proc = blk
|
59
|
+
end
|
60
|
+
|
61
|
+
def readable?
|
62
|
+
!!@readable
|
63
|
+
end
|
64
|
+
|
65
|
+
def writable?
|
66
|
+
!!@writable
|
67
|
+
end
|
68
|
+
|
69
|
+
def polymorphic_parent?
|
70
|
+
resource.polymorphic?
|
71
|
+
end
|
72
|
+
|
73
|
+
def polymorphic_child?
|
74
|
+
!!@polymorphic_child
|
75
|
+
end
|
76
|
+
|
77
|
+
def primary_key
|
78
|
+
@primary_key ||= :id
|
79
|
+
end
|
80
|
+
|
81
|
+
def foreign_key
|
82
|
+
@foreign_key ||= infer_foreign_key
|
83
|
+
end
|
84
|
+
|
85
|
+
def association_name
|
86
|
+
@as || name
|
87
|
+
end
|
88
|
+
|
89
|
+
def resource_class
|
90
|
+
@resource_class ||= infer_resource_class
|
91
|
+
end
|
92
|
+
|
93
|
+
def scope(parents)
|
94
|
+
raise "No #scope defined for sideload with name '#{name}'. Make sure to define this in your adapter, or pass a block that defines the scope."
|
95
|
+
end
|
96
|
+
|
97
|
+
def assign_each(parent, children)
|
98
|
+
raise 'Override #assign_each in subclass'
|
99
|
+
end
|
100
|
+
|
101
|
+
def type
|
102
|
+
@type || raise("Override #type in subclass. Should be one of #{TYPES.inspect}")
|
103
|
+
end
|
104
|
+
|
105
|
+
def load_params(parents, query)
|
106
|
+
raise 'Override #load_params in subclass'
|
107
|
+
end
|
108
|
+
|
109
|
+
def base_scope
|
110
|
+
if @base_scope
|
111
|
+
@base_scope.respond_to?(:call) ? @base_scope.call : @base_scope
|
112
|
+
else
|
113
|
+
resource.base_scope
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def load(parents, query)
|
118
|
+
params = load_params(parents, query)
|
119
|
+
params_proc.call(params, parents, query) if params_proc
|
120
|
+
opts = load_options(parents, query)
|
121
|
+
proxy = resource.class._all(params, opts, base_scope)
|
122
|
+
pre_load_proc.call(proxy) if pre_load_proc
|
123
|
+
proxy.to_a
|
124
|
+
end
|
125
|
+
|
126
|
+
# Override in subclass
|
127
|
+
def infer_foreign_key
|
128
|
+
model = parent_resource_class.model
|
129
|
+
namespace = namespace_for(model)
|
130
|
+
model_name = model.name.gsub("#{namespace}::", '')
|
131
|
+
:"#{model_name.underscore}_id"
|
132
|
+
end
|
133
|
+
|
48
134
|
def resource
|
49
135
|
@resource ||= resource_class.new
|
50
136
|
end
|
51
137
|
|
52
|
-
|
53
|
-
|
54
|
-
# Polymorphic sideloads group the parent objects in some fashion,
|
55
|
-
# so different 'types' can be resolved differently. Let's say an
|
56
|
-
# +Office+ has a polymorphic +Organization+, which can be either a
|
57
|
-
# +Business+ or +Government+:
|
58
|
-
#
|
59
|
-
# allow_sideload :organization, :polymorphic: true do
|
60
|
-
# group_by :organization_type
|
61
|
-
#
|
62
|
-
# allow_sideload 'Business', resource: BusinessResource do
|
63
|
-
# # ... code ...
|
64
|
-
# end
|
65
|
-
#
|
66
|
-
# allow_sideload 'Governemnt', resource: GovernmentResource do
|
67
|
-
# # ... code ...
|
68
|
-
# end
|
69
|
-
# end
|
70
|
-
#
|
71
|
-
# You probably want to extract this code into an Adapter. For instance,
|
72
|
-
# with ActiveRecord:
|
73
|
-
#
|
74
|
-
# polymorphic_belongs_to :organization,
|
75
|
-
# group_by: :organization_type,
|
76
|
-
# groups: {
|
77
|
-
# 'Business' => {
|
78
|
-
# scope: -> { Business.all },
|
79
|
-
# resource: BusinessResource,
|
80
|
-
# foreign_key: :organization_id
|
81
|
-
# },
|
82
|
-
# 'Government' => {
|
83
|
-
# scope: -> { Government.all },
|
84
|
-
# resource: GovernmentResource,
|
85
|
-
# foreign_key: :organization_id
|
86
|
-
# }
|
87
|
-
# }
|
88
|
-
#
|
89
|
-
# @see Adapters::ActiveRecordSideloading#polymorphic_belongs_to
|
90
|
-
# @return [Boolean] is this sideload polymorphic?
|
91
|
-
def polymorphic?
|
92
|
-
@polymorphic == true
|
93
|
-
end
|
94
|
-
|
95
|
-
# Build a scope that will be used to fetch the related records
|
96
|
-
# This scope will be further chained with filtering/sorting/etc
|
97
|
-
#
|
98
|
-
# You probably want to wrap this logic in an Adapter, instead of
|
99
|
-
# specifying in your resource directly.
|
100
|
-
#
|
101
|
-
# @example Default ActiveRecord
|
102
|
-
# class PostResource < ApplicationResource
|
103
|
-
# # ... code ...
|
104
|
-
# allow_sideload :comments, resource: CommentResource do
|
105
|
-
# scope do |posts|
|
106
|
-
# Comment.where(post_id: posts.map(&:id))
|
107
|
-
# end
|
108
|
-
# # ... code ...
|
109
|
-
# end
|
110
|
-
# end
|
111
|
-
#
|
112
|
-
# @example Custom Scope
|
113
|
-
# # In this example, our base scope is a Hash
|
114
|
-
# scope do |posts|
|
115
|
-
# { post_ids: posts.map(&:id) }
|
116
|
-
# end
|
117
|
-
#
|
118
|
-
# @example ActiveRecord via Adapter
|
119
|
-
# class PostResource < ApplicationResource
|
120
|
-
# # ... code ...
|
121
|
-
# has_many :comments,
|
122
|
-
# scope: -> { Comment.all },
|
123
|
-
# resource: CommentResource,
|
124
|
-
# foreign_key: :post_id
|
125
|
-
# end
|
126
|
-
#
|
127
|
-
# @see Adapters::Abstract
|
128
|
-
# @see Adapters::ActiveRecordSideloading#has_many
|
129
|
-
# @see #allow_sideload
|
130
|
-
# @yieldparam parents - The resolved parent records
|
131
|
-
def scope(&blk)
|
132
|
-
@scope_proc = blk
|
133
|
-
end
|
134
|
-
|
135
|
-
# The proc used to assign the resolved parents and children.
|
136
|
-
#
|
137
|
-
# You probably want to wrap this logic in an Adapter, instead of
|
138
|
-
# specifying in your resource directly.
|
139
|
-
#
|
140
|
-
# @example Default ActiveRecord
|
141
|
-
# class PostResource < ApplicationResource
|
142
|
-
# # ... code ...
|
143
|
-
# allow_sideload :comments, resource: CommentResource do
|
144
|
-
# # ... code ...
|
145
|
-
# assign do |posts, comments|
|
146
|
-
# posts.each do |post|
|
147
|
-
# relevant_comments = comments.select { |c| c.post_id == post.id }
|
148
|
-
# post.comments = relevant_comments
|
149
|
-
# end
|
150
|
-
# end
|
151
|
-
# end
|
152
|
-
# end
|
153
|
-
#
|
154
|
-
# @example ActiveRecord via Adapter
|
155
|
-
# class PostResource < ApplicationResource
|
156
|
-
# # ... code ...
|
157
|
-
# has_many :comments,
|
158
|
-
# scope: -> { Comment.all },
|
159
|
-
# resource: CommentResource,
|
160
|
-
# foreign_key: :post_id
|
161
|
-
# end
|
162
|
-
#
|
163
|
-
# @see Adapters::Abstract
|
164
|
-
# @see Adapters::ActiveRecordSideloading#has_many
|
165
|
-
# @see #allow_sideload
|
166
|
-
# @yieldparam parents - The resolved parent records
|
167
|
-
# @yieldparam children - The resolved child records
|
168
|
-
def assign(&blk)
|
169
|
-
@assign_proc = blk
|
170
|
-
end
|
171
|
-
|
172
|
-
# Configure how to associate parent and child records.
|
173
|
-
# Delegates to #resource
|
174
|
-
#
|
175
|
-
# @see #name
|
176
|
-
# @see #type
|
177
|
-
# @api private
|
178
|
-
def associate(parent, child)
|
179
|
-
association_name = @parent ? @parent.name : name
|
180
|
-
resource.associate(parent, child, association_name, type)
|
138
|
+
def parent_resource
|
139
|
+
@parent_resource ||= parent_resource_class.new
|
181
140
|
end
|
182
141
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
142
|
+
def assign(parents, children)
|
143
|
+
associated = []
|
144
|
+
parents.each do |parent|
|
145
|
+
relevant_children = fire_assign_each(parent, children)
|
146
|
+
if relevant_children.is_a?(Array)
|
147
|
+
associated |= relevant_children
|
148
|
+
associate_all(parent, relevant_children)
|
149
|
+
else
|
150
|
+
associated << relevant_children
|
151
|
+
associate(parent, relevant_children)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
(children - associated).each do |unassigned|
|
155
|
+
children.delete(unassigned)
|
156
|
+
end
|
192
157
|
end
|
193
158
|
|
194
|
-
|
159
|
+
def resolve(parents, query)
|
160
|
+
# legacy / custom / many-to-many
|
161
|
+
if self.class.scope_proc || type == :many_to_many
|
162
|
+
sideload_scope = fire_scope(parents)
|
163
|
+
sideload_scope = Scope.new sideload_scope,
|
164
|
+
resource,
|
165
|
+
query,
|
166
|
+
sideload_parent_length: parents.length,
|
167
|
+
default_paginate: false
|
168
|
+
sideload_scope.resolve do |sideload_results|
|
169
|
+
fire_assign(parents, sideload_results)
|
170
|
+
end
|
171
|
+
else
|
172
|
+
load(parents, query)
|
173
|
+
end
|
174
|
+
end
|
195
175
|
|
196
|
-
|
197
|
-
#
|
198
|
-
# In particular, helpful for bulk operations. "after_save" will fire
|
199
|
-
# for any persistence method - +:create+, +:update+, +:destroy+, +:disassociate+.
|
200
|
-
# Use "only" and "except" keyword arguments to fire only for a
|
201
|
-
# specific persistence method.
|
202
|
-
#
|
203
|
-
# @example Bulk Notify Users on Invite
|
204
|
-
# class ProjectResource < ApplicationResource
|
205
|
-
# # ... code ...
|
206
|
-
# allow_sideload :users, resource: UserResource do
|
207
|
-
# # scope {}
|
208
|
-
# # assign {}
|
209
|
-
# after_save only: [:create] do |project, users|
|
210
|
-
# UserMailer.invite(project, users).deliver_later
|
211
|
-
# end
|
212
|
-
# end
|
213
|
-
# end
|
214
|
-
#
|
215
|
-
# @see #hooks
|
216
|
-
# @see Util::Persistence
|
217
|
-
def after_save(only: [], except: [], &blk)
|
176
|
+
def self.after_save(only: [], except: [], &blk)
|
218
177
|
actions = HOOK_ACTIONS - except
|
219
178
|
actions = only & actions
|
220
179
|
actions = [:save] if only.empty? && except.empty?
|
@@ -223,10 +182,7 @@ module JsonapiCompliable
|
|
223
182
|
end
|
224
183
|
end
|
225
184
|
|
226
|
-
|
227
|
-
# @see #after_save
|
228
|
-
# @return hash of hooks, ie +{ after_create: #<Proc>}+
|
229
|
-
def hooks
|
185
|
+
def self.hooks
|
230
186
|
@hooks ||= {}.tap do |h|
|
231
187
|
HOOK_ACTIONS.each do |a|
|
232
188
|
h[:"after_#{a}"] = []
|
@@ -235,173 +191,91 @@ module JsonapiCompliable
|
|
235
191
|
end
|
236
192
|
end
|
237
193
|
|
238
|
-
|
239
|
-
|
240
|
-
# and +parent_type+. We would want to group on +parent_type+:
|
241
|
-
#
|
242
|
-
# allow_sideload :organization, polymorphic: true do
|
243
|
-
# # group parent_type, parent here is 'organization'
|
244
|
-
# group_by :organization_type
|
245
|
-
# end
|
246
|
-
#
|
247
|
-
# @see #polymorphic?
|
248
|
-
def group_by(grouping_field)
|
249
|
-
@grouping_field = grouping_field
|
250
|
-
end
|
251
|
-
|
252
|
-
# Resolve the sideload.
|
253
|
-
#
|
254
|
-
# * Uses the 'scope' proc to build a 'base scope'
|
255
|
-
# * Chains additional criteria onto that 'base scope'
|
256
|
-
# * Resolves that scope (see Scope#resolve)
|
257
|
-
# * Assigns the resulting child objects to their corresponding parents
|
258
|
-
#
|
259
|
-
# @see Scope#resolve
|
260
|
-
# @param [Object] parents The resolved parent models
|
261
|
-
# @param [Query] query The Query instance
|
262
|
-
# @param [Symbol] namespace The current namespace (see Resource#with_context)
|
263
|
-
# @see Query
|
264
|
-
# @see Resource#with_context
|
265
|
-
# @return [void]
|
266
|
-
# @api private
|
267
|
-
def resolve(parents, query, namespace)
|
268
|
-
if polymorphic?
|
269
|
-
resolve_polymorphic(parents, query)
|
270
|
-
else
|
271
|
-
resolve_basic(parents, query, namespace)
|
272
|
-
end
|
273
|
-
end
|
194
|
+
def fire_hooks!(parent, objects, method)
|
195
|
+
return unless self.class.hooks
|
274
196
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
# rather than using directly
|
279
|
-
#
|
280
|
-
# @example Default ActiveRecord
|
281
|
-
# # What happens 'under the hood'
|
282
|
-
# class CommentResource < ApplicationResource
|
283
|
-
# # ... code ...
|
284
|
-
# allow_sideload :post, resource: PostResource do
|
285
|
-
# scope do |comments|
|
286
|
-
# Post.where(id: comments.map(&:post_id))
|
287
|
-
# end
|
288
|
-
#
|
289
|
-
# assign do |comments, posts|
|
290
|
-
# comments.each do |comment|
|
291
|
-
# relevant_post = posts.find { |p| p.id == comment.post_id }
|
292
|
-
# comment.post = relevant_post
|
293
|
-
# end
|
294
|
-
# end
|
295
|
-
# end
|
296
|
-
# end
|
297
|
-
#
|
298
|
-
# # Rather than writing that code directly, go through the adapter:
|
299
|
-
# class CommentResource < ApplicationResource
|
300
|
-
# # ... code ...
|
301
|
-
# use_adapter JsonapiCompliable::Adapters::ActiveRecord
|
302
|
-
#
|
303
|
-
# belongs_to :post,
|
304
|
-
# scope: -> { Post.all },
|
305
|
-
# resource: PostResource,
|
306
|
-
# foreign_key: :post_id
|
307
|
-
# end
|
308
|
-
#
|
309
|
-
# @see Adapters::ActiveRecordSideloading#belongs_to
|
310
|
-
# @see #assign
|
311
|
-
# @see #scope
|
312
|
-
# @return void
|
313
|
-
def allow_sideload(name, opts = {}, &blk)
|
314
|
-
sideload = Sideload.new(name, opts)
|
315
|
-
sideload.instance_eval(&blk) if blk
|
316
|
-
|
317
|
-
if polymorphic?
|
318
|
-
@polymorphic_groups[name] = sideload
|
319
|
-
else
|
320
|
-
@sideloads[name] = sideload
|
197
|
+
all = self.class.hooks[:"after_#{method}"] + self.class.hooks[:after_save]
|
198
|
+
all.compact.each do |hook|
|
199
|
+
resource.instance_exec(parent, objects, &hook)
|
321
200
|
end
|
322
201
|
end
|
323
202
|
|
324
|
-
|
325
|
-
|
326
|
-
# @see +allow_sideload
|
327
|
-
# @return the corresponding Sideload object
|
328
|
-
def sideload(name)
|
329
|
-
@sideloads[name]
|
203
|
+
def associate_all(parent, children)
|
204
|
+
parent_resource.associate_all(parent, children, association_name, type)
|
330
205
|
end
|
331
206
|
|
332
|
-
|
333
|
-
|
334
|
-
{}.tap do |all|
|
335
|
-
if polymorphic?
|
336
|
-
polymorphic_groups.each_pair do |type, sl|
|
337
|
-
all.merge!(sl.resource.sideloading.all_sideloads)
|
338
|
-
end
|
339
|
-
else
|
340
|
-
all.merge!(@sideloads.merge(resource.sideloading.sideloads))
|
341
|
-
end
|
342
|
-
end
|
207
|
+
def associate(parent, child)
|
208
|
+
parent_resource.associate(parent, child, association_name, type)
|
343
209
|
end
|
344
210
|
|
345
|
-
def
|
346
|
-
|
347
|
-
|
348
|
-
memo << sl.name
|
349
|
-
memo |= sl.association_names(memo)
|
350
|
-
end
|
351
|
-
end
|
211
|
+
def disassociate(parent, child)
|
212
|
+
parent_resource.disassociate(parent, child, association_name, type)
|
213
|
+
end
|
352
214
|
|
353
|
-
|
215
|
+
def ids_for_parents(parents)
|
216
|
+
parent_ids = parents.map(&primary_key)
|
217
|
+
parent_ids.compact!
|
218
|
+
parent_ids.uniq!
|
219
|
+
parent_ids
|
354
220
|
end
|
355
221
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
222
|
+
private
|
223
|
+
|
224
|
+
def load_options(parents, query)
|
225
|
+
{}.tap do |opts|
|
226
|
+
opts[:default_paginate] = false
|
227
|
+
opts[:sideload_parent_length] = parents.length
|
228
|
+
opts[:after_resolve] = ->(results) {
|
229
|
+
fire_assign(parents, results)
|
230
|
+
}
|
360
231
|
end
|
361
232
|
end
|
362
233
|
|
363
|
-
def
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
resource.instance_exec(parent, objects, &hook)
|
234
|
+
def fire_assign_each(parent, children)
|
235
|
+
if self.class.assign_each_proc
|
236
|
+
instance_exec(parent, children, &self.class.assign_each_proc)
|
237
|
+
else
|
238
|
+
assign_each(parent, children)
|
369
239
|
end
|
370
240
|
end
|
371
241
|
|
372
|
-
|
242
|
+
def fire_assign(parents, children)
|
243
|
+
if self.class.assign_proc
|
244
|
+
instance_exec(parents, children, &self.class.assign_proc)
|
245
|
+
else
|
246
|
+
assign(parents, children)
|
247
|
+
end
|
248
|
+
end
|
373
249
|
|
374
|
-
def
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
250
|
+
def fire_scope(parents)
|
251
|
+
parent_ids = ids_for_parents(parents)
|
252
|
+
if self.class.scope_proc
|
253
|
+
instance_exec(parent_ids, parents, &self.class.scope_proc)
|
254
|
+
else
|
255
|
+
method = method(:scope)
|
256
|
+
if [2,-2].include?(method.arity)
|
257
|
+
scope(parent_ids, parents)
|
382
258
|
else
|
383
|
-
|
259
|
+
scope(parent_ids)
|
384
260
|
end
|
385
261
|
end
|
386
262
|
end
|
387
263
|
|
388
|
-
def
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
sideload_for_group = @polymorphic_groups[group_type]
|
393
|
-
if sideload_for_group
|
394
|
-
sideload_for_group.resolve(group_members, query, name)
|
395
|
-
end
|
396
|
-
end
|
264
|
+
def namespace_for(klass)
|
265
|
+
namespace = klass.name
|
266
|
+
split = namespace.split('::')
|
267
|
+
split[0,split.length-1].join('::')
|
397
268
|
end
|
398
269
|
|
399
|
-
def
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
270
|
+
def infer_resource_class
|
271
|
+
namespace = namespace_for(parent_resource.class)
|
272
|
+
inferred_name = "#{name.to_s.singularize.classify}Resource"
|
273
|
+
klass = "#{namespace}::#{inferred_name}".safe_constantize
|
274
|
+
klass ||= inferred_name.safe_constantize
|
275
|
+
unless klass
|
276
|
+
raise Errors::ResourceNotFound.new(parent_resource, name)
|
404
277
|
end
|
278
|
+
klass
|
405
279
|
end
|
406
280
|
end
|
407
281
|
end
|