graphiti 2.0.2 → 2.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 +4 -4
- data/.github/workflows/docs.yml +7 -0
- data/CHANGELOG.md +13 -0
- data/bin/cut-docs-version +24 -0
- data/lib/generators/graphiti/generator_mixin.rb +1 -1
- data/lib/generators/graphiti/templates/application_resource.rb.erb +5 -5
- data/lib/graphiti/adapters/abstract.rb +8 -0
- data/lib/graphiti/adapters/active_record.rb +8 -0
- data/lib/graphiti/adapters/persistence/associations.rb +6 -1
- data/lib/graphiti/audit/report.rb +3 -1
- data/lib/graphiti/audit.rb +14 -1
- data/lib/graphiti/errors.rb +36 -0
- data/lib/graphiti/query.rb +7 -0
- data/lib/graphiti/request_validators/validator.rb +10 -1
- data/lib/graphiti/resource/configuration.rb +124 -4
- data/lib/graphiti/resource/dsl.rb +47 -0
- data/lib/graphiti/resource/interface.rb +39 -0
- data/lib/graphiti/resource/persistence.rb +29 -0
- data/lib/graphiti/resource/sideloading.rb +2 -0
- data/lib/graphiti/resource.rb +4 -0
- data/lib/graphiti/resource_proxy.rb +6 -3
- data/lib/graphiti/runner.rb +3 -1
- data/lib/graphiti/schema.rb +4 -0
- data/lib/graphiti/schema_diff.rb +5 -0
- data/lib/graphiti/scope.rb +8 -0
- data/lib/graphiti/scoping/default_filter.rb +2 -0
- data/lib/graphiti/scoping/filter.rb +79 -20
- data/lib/graphiti/scoping/sort.rb +1 -1
- data/lib/graphiti/sideload/belongs_to.rb +52 -8
- data/lib/graphiti/sideload/has_many.rb +33 -2
- data/lib/graphiti/sideload/many_to_many.rb +3 -3
- data/lib/graphiti/sideload.rb +25 -1
- data/lib/graphiti/stats/payload.rb +20 -3
- data/lib/graphiti/util/internal_param.rb +16 -0
- data/lib/graphiti/util/link.rb +10 -3
- data/lib/graphiti/util/persistence.rb +1 -1
- data/lib/graphiti/util/public_id_block.rb +15 -0
- data/lib/graphiti/util/public_id_map.rb +34 -0
- data/lib/graphiti/util/public_id_sources.rb +27 -0
- data/lib/graphiti/util/serializer_relationships.rb +5 -3
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +16 -0
- metadata +6 -1
data/lib/graphiti/resource.rb
CHANGED
|
@@ -150,6 +150,10 @@ module Graphiti
|
|
|
150
150
|
persistence.run
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
+
def model_attribute_for(name)
|
|
154
|
+
self.class.model_attribute_for(name)
|
|
155
|
+
end
|
|
156
|
+
|
|
153
157
|
def stat(attribute, calculation)
|
|
154
158
|
stats_dsl = stats[attribute] || stats[attribute.to_sym]
|
|
155
159
|
raise Errors::StatNotFound.new(attribute, calculation) unless stats_dsl
|
|
@@ -125,14 +125,17 @@ module Graphiti
|
|
|
125
125
|
scope = @scope.unpaginated_object
|
|
126
126
|
if resource.adapter.can_group?
|
|
127
127
|
if (group = @query.hash[:stats].delete(:group_by))
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
# A foreign key whose values render as public ids exposes nothing, so it may group even when unreadable.
|
|
129
|
+
flag = resource.class.public_id_source_for(group[0]) ? :filterable : :readable
|
|
130
|
+
resource.get_attr!(group[0], flag, request: true)
|
|
131
|
+
scope = resource.adapter.group(scope, resource.model_attribute_for(group[0]))
|
|
130
132
|
end
|
|
131
133
|
end
|
|
132
134
|
payload = Stats::Payload.new @resource,
|
|
133
135
|
@query,
|
|
134
136
|
scope,
|
|
135
|
-
data
|
|
137
|
+
data,
|
|
138
|
+
group_by: group&.first
|
|
136
139
|
payload.generate
|
|
137
140
|
else
|
|
138
141
|
{}
|
data/lib/graphiti/runner.rb
CHANGED
data/lib/graphiti/schema.rb
CHANGED
data/lib/graphiti/schema_diff.rb
CHANGED
|
@@ -46,6 +46,11 @@ module Graphiti
|
|
|
46
46
|
if old_resource[:type] != new_resource[:type]
|
|
47
47
|
@errors << "#{old_resource[:name]} changed type from #{old_resource[:type].inspect} to #{new_resource[:type].inspect}."
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
# Every id a client holds, and every url built from one, stops resolving.
|
|
51
|
+
if old_resource[:public_id] != new_resource[:public_id]
|
|
52
|
+
@errors << "#{old_resource[:name]} changed public_id from #{old_resource[:public_id].inspect} to #{new_resource[:public_id].inspect}."
|
|
53
|
+
end
|
|
49
54
|
yield
|
|
50
55
|
end
|
|
51
56
|
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -153,6 +153,7 @@ module Graphiti
|
|
|
153
153
|
def sync_resolve_sideloads(results)
|
|
154
154
|
return if results == []
|
|
155
155
|
|
|
156
|
+
collect_foreign_keys(results)
|
|
156
157
|
reset_captured_sideload_proxies
|
|
157
158
|
each_applicable_sideload do |name, sideload, sideload_query|
|
|
158
159
|
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
@@ -221,6 +222,12 @@ module Graphiti
|
|
|
221
222
|
false
|
|
222
223
|
end
|
|
223
224
|
|
|
225
|
+
def collect_foreign_keys(results)
|
|
226
|
+
return unless Graphiti.public_ids_declared? && !@opts[:translating_public_ids]
|
|
227
|
+
|
|
228
|
+
@resource.class.sideloads.each_value { |sideload| sideload.collect_foreign_keys(results, @query) }
|
|
229
|
+
end
|
|
230
|
+
|
|
224
231
|
def each_applicable_sideload
|
|
225
232
|
@query.sideloads.each_pair do |name, sideload_query|
|
|
226
233
|
sideload = @resource.class.sideload(name)
|
|
@@ -246,6 +253,7 @@ module Graphiti
|
|
|
246
253
|
def future_resolve_sideloads(results)
|
|
247
254
|
return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
|
|
248
255
|
|
|
256
|
+
collect_foreign_keys(results)
|
|
249
257
|
reset_captured_sideload_proxies
|
|
250
258
|
sideload_promises = []
|
|
251
259
|
each_applicable_sideload do |name, sideload, sideload_query|
|
|
@@ -36,6 +36,8 @@ module Graphiti
|
|
|
36
36
|
#
|
|
37
37
|
# @return scope the scope object we are chaining/modifying
|
|
38
38
|
def apply
|
|
39
|
+
return @scope if @opts[:bypass_default_filters]
|
|
40
|
+
|
|
39
41
|
resource.default_filters.each_pair do |name, opts|
|
|
40
42
|
next if overridden?(name)
|
|
41
43
|
@scope = resource.instance_exec(@scope, resource.context, &opts[:filter])
|
|
@@ -19,8 +19,8 @@ module Graphiti
|
|
|
19
19
|
resource, missing_dependent_filters
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
each_filter do |filter, operator, value|
|
|
23
|
-
@scope = filter_scope(filter, operator, value)
|
|
22
|
+
each_filter do |filter, operator, value, primary_keys, decoded|
|
|
23
|
+
@scope = filter_scope(filter, operator, value, primary_keys, decoded)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
resource.after_filtering(@scope)
|
|
@@ -28,18 +28,24 @@ module Graphiti
|
|
|
28
28
|
|
|
29
29
|
private
|
|
30
30
|
|
|
31
|
-
def filter_scope(filter, operator, value)
|
|
31
|
+
def filter_scope(filter, operator, value, primary_keys, decoded)
|
|
32
32
|
if (custom_scope = filter.values[0][:operators][operator])
|
|
33
|
-
|
|
33
|
+
if takes_primary_keys?(filter, operator)
|
|
34
|
+
@resource.instance_exec(@scope, value, resource.context, primary_keys: primary_keys, &custom_scope)
|
|
35
|
+
else
|
|
36
|
+
@resource.instance_exec(@scope, value, resource.context, &custom_scope)
|
|
37
|
+
end
|
|
34
38
|
else
|
|
35
|
-
filter_via_adapter(filter, operator, value)
|
|
39
|
+
filter_via_adapter(filter, operator, value, decoded)
|
|
36
40
|
end
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
# A foreign key filter holds primary keys once its public ids are decoded, whatever type the attribute renders as.
|
|
44
|
+
def filter_via_adapter(filter, operator, value, decoded = false)
|
|
45
|
+
type_name = Types.name_for(decoded ? :integer_id : filter.values.first[:type])
|
|
46
|
+
type_name = :public_id if type_name == :string && [:eq, :not_eq].include?(operator) && resource.class.public_id_attribute?(filter.keys.first)
|
|
41
47
|
method = :"filter_#{type_name}_#{operator}"
|
|
42
|
-
attribute = filter.keys.first
|
|
48
|
+
attribute = resource.model_attribute_for(filter.keys.first)
|
|
43
49
|
|
|
44
50
|
if resource.adapter.respond_to?(method)
|
|
45
51
|
resource.adapter.send(method, @scope, attribute, value)
|
|
@@ -53,27 +59,80 @@ module Graphiti
|
|
|
53
59
|
filter_param.each_pair do |param_name, param_value|
|
|
54
60
|
filter = find_filter!(param_name)
|
|
55
61
|
|
|
62
|
+
internal = param_value.is_a?(Util::InternalParam)
|
|
63
|
+
if internal
|
|
64
|
+
param_value = param_value.value
|
|
65
|
+
elsif filter.values[0][:internal]
|
|
66
|
+
raise Errors::InvalidAttributeAccess.new(resource, param_name, :filterable, request: true)
|
|
67
|
+
else
|
|
68
|
+
source = resource.class.public_id_source_for(filter.keys[0])
|
|
69
|
+
end
|
|
70
|
+
|
|
56
71
|
normalize_param(filter, param_value).each do |operator, value|
|
|
57
72
|
operator = operator.to_s.gsub("!", "not_").to_sym
|
|
58
73
|
validate_operator(filter, operator)
|
|
74
|
+
custom = filter.values[0][:operators][operator]
|
|
75
|
+
decoded = !!source && !custom
|
|
76
|
+
value = decode_public_ids(source, value) if decoded
|
|
59
77
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
value = parse_string_value(filter.values[0], value)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
check_blank_filters!(resource, filter, value)
|
|
66
|
-
value = parse_string_null(filter.values[0], value)
|
|
67
|
-
validate_singular(resource, filter, value)
|
|
68
|
-
value = coerce_types(filter.values[0], param_name.to_sym, value)
|
|
69
|
-
validate_allowlist(resource, filter, value)
|
|
70
|
-
validate_denylist(resource, filter, value)
|
|
78
|
+
decoding = source && takes_primary_keys?(filter, operator)
|
|
79
|
+
value = validated_and_cast(filter, param_name, value, decoding ? source : nil) unless internal
|
|
71
80
|
value = value[0] if filter.values[0][:single]
|
|
72
|
-
|
|
81
|
+
primary_keys = decoded_for_block(filter, operator, source, value)
|
|
82
|
+
yield filter, operator, value, primary_keys, decoded
|
|
73
83
|
end
|
|
74
84
|
end
|
|
75
85
|
end
|
|
76
86
|
|
|
87
|
+
# A block asking for primary_keys: was sent public ids, which cast like the source's id rather than the key.
|
|
88
|
+
def validated_and_cast(filter, param_name, value, public_id_source)
|
|
89
|
+
type = Types[filter.values[0][:type]]
|
|
90
|
+
unless type[:canonical_name] == :hash || !value.is_a?(String)
|
|
91
|
+
value = parse_string_value(filter.values[0], value)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
check_blank_filters!(resource, filter, value)
|
|
95
|
+
value = parse_string_null(filter.values[0], value)
|
|
96
|
+
validate_singular(resource, filter, value)
|
|
97
|
+
value = public_id_source ? cast_as_public_ids(public_id_source, value) : coerce_types(filter.values[0], param_name.to_sym, value)
|
|
98
|
+
validate_allowlist(resource, filter, value)
|
|
99
|
+
validate_denylist(resource, filter, value)
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def cast_as_public_ids(source, value)
|
|
104
|
+
source_instance = source.new
|
|
105
|
+
Array(value).map { |public_id| source_instance.typecast(:id, public_id, :filterable) }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def takes_primary_keys?(filter, operator)
|
|
109
|
+
Array(filter.values[0][:operators_taking_primary_keys]).include?(operator)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def decoded_for_block(filter, operator, source, value)
|
|
113
|
+
return unless takes_primary_keys?(filter, operator)
|
|
114
|
+
source ||= resource.class if filter.keys[0] == :id && resource.class.publishes_public_id?
|
|
115
|
+
return value unless source
|
|
116
|
+
|
|
117
|
+
keys = decode_public_ids(source, value)
|
|
118
|
+
filter.values[0][:single] ? keys.first : keys
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def decode_public_ids(source_resource_class, value)
|
|
122
|
+
case value
|
|
123
|
+
when Hash
|
|
124
|
+
value.transform_values { |operand| decode_public_ids(source_resource_class, operand) }
|
|
125
|
+
when Array
|
|
126
|
+
lookup_primary_keys(source_resource_class, value)
|
|
127
|
+
else
|
|
128
|
+
lookup_primary_keys(source_resource_class, value.to_s.split(","))
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def lookup_primary_keys(source_resource_class, public_ids)
|
|
133
|
+
source_resource_class.decode_public_ids(public_ids)
|
|
134
|
+
end
|
|
135
|
+
|
|
77
136
|
def coerce_types(filter, name, value)
|
|
78
137
|
type_name = filter[:type]
|
|
79
138
|
is_array = type_name.to_s.starts_with?("array_of") ||
|
|
@@ -31,7 +31,7 @@ module Graphiti
|
|
|
31
31
|
@scope = if sort[:proc]
|
|
32
32
|
resource.instance_exec(@scope, direction, &sort[:proc])
|
|
33
33
|
else
|
|
34
|
-
resource.adapter.order(@scope, attribute, direction)
|
|
34
|
+
resource.adapter.order(@scope, resource.model_attribute_for(attribute), direction)
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -17,7 +17,7 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
|
|
|
17
17
|
|
|
18
18
|
def resource_ids_blocker
|
|
19
19
|
return :unreadable unless renderable_at_all?
|
|
20
|
-
return :custom_primary_key unless foreign_key_is_related_id?
|
|
20
|
+
return :custom_primary_key unless foreign_key_is_related_id? || resolves_public_ids?
|
|
21
21
|
return :polymorphic_child if polymorphic_child?
|
|
22
22
|
return :scope_block if self.class.scope_proc
|
|
23
23
|
return :params_block if self.class.params_proc
|
|
@@ -28,12 +28,6 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
|
|
|
28
28
|
nil
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
# base_filter matches the foreign key against primary_key, so a custom
|
|
32
|
-
# primary_key means the key holds that column's value, not the related id.
|
|
33
|
-
def foreign_key_is_related_id?
|
|
34
|
-
primary_key == :id
|
|
35
|
-
end
|
|
36
|
-
|
|
37
31
|
def load_params(parents, query)
|
|
38
32
|
query.hash.tap do |hash|
|
|
39
33
|
hash[:filter] ||= {}
|
|
@@ -43,7 +37,49 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
|
|
|
43
37
|
|
|
44
38
|
def base_filter(parents)
|
|
45
39
|
parent_ids = ids_for_parents(parents)
|
|
46
|
-
{
|
|
40
|
+
return {primary_key_filter => Graphiti::Util::InternalParam.new(parent_ids)} if primary_key_filter == :_primary_key
|
|
41
|
+
|
|
42
|
+
{primary_key_filter => parent_ids.join(",")}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resolves_public_ids?
|
|
46
|
+
@resolves_public_ids ||= target_publishes_id? && resource_class.filters.key?(primary_key_filter)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def link_hides_primary_key?
|
|
50
|
+
!target_publishes_id? || resolves_public_ids?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def register_public_id_source
|
|
54
|
+
return unless target_publishes_id? && resource_class_loaded? && parent_resource_class.model_declared?
|
|
55
|
+
|
|
56
|
+
parent_resource_class.guard_public_id_leak!(foreign_key)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def rendered_id_for(foreign_key, query)
|
|
60
|
+
return foreign_key unless target_publishes_id?
|
|
61
|
+
return unless resolves_public_ids?
|
|
62
|
+
|
|
63
|
+
public_id_map(query)[foreign_key]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The serializer reports a foreign key the record cannot answer with a better error.
|
|
67
|
+
def collect_foreign_keys(parents, query)
|
|
68
|
+
return unless resource_class_loaded? && resolves_public_ids?
|
|
69
|
+
return unless parents.first.respond_to?(foreign_key)
|
|
70
|
+
|
|
71
|
+
public_id_map(query).add(ids_for_parents(parents))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def primary_key_filter
|
|
75
|
+
return primary_key unless target_publishes_id?
|
|
76
|
+
return :_primary_key if primary_key == :id || primary_key.to_s == resource_class.model_primary_key.to_s
|
|
77
|
+
|
|
78
|
+
primary_key
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def foreign_key_is_related_id?
|
|
82
|
+
primary_key_filter == :id
|
|
47
83
|
end
|
|
48
84
|
|
|
49
85
|
def ids_for_parents(parents)
|
|
@@ -75,6 +111,14 @@ class Graphiti::Sideload::BelongsTo < Graphiti::Sideload
|
|
|
75
111
|
|
|
76
112
|
private
|
|
77
113
|
|
|
114
|
+
def target_publishes_id?
|
|
115
|
+
@target_publishes_id ||= Graphiti.public_ids_declared? && resource_class.publishes_public_id?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def public_id_map(query)
|
|
119
|
+
query.public_id_maps.compute_if_absent(self) { Graphiti::Util::PublicIdMap.new(resource_class, primary_key_filter) }
|
|
120
|
+
end
|
|
121
|
+
|
|
78
122
|
def child_map(children)
|
|
79
123
|
children.index_by(&primary_key)
|
|
80
124
|
end
|
|
@@ -21,11 +21,30 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def base_filter(parents)
|
|
24
|
-
{foreign_key =>
|
|
24
|
+
{foreign_key => internal_parent_filter(parents, foreign_key)}
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def link_filter(parents)
|
|
28
|
-
{inverse_filter =>
|
|
28
|
+
{inverse_filter => public_parent_filter(parents)}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def link_hides_primary_key?
|
|
32
|
+
return true unless links_by_public_id?
|
|
33
|
+
|
|
34
|
+
resource_class.public_id_source_for(inverse_filter) == parent_resource_class &&
|
|
35
|
+
resource_class.filter_accepts_public_ids?(inverse_filter)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def links_by_public_id?
|
|
39
|
+
@links_by_public_id ||= Graphiti.public_ids_declared? && primary_key == :id && !!parent_resource_class&.publishes_public_id?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def register_public_id_source
|
|
43
|
+
return unless links_by_public_id? && resource_class_loaded?
|
|
44
|
+
return if parent_resource_class.abstract_class? || !parent_resource_class.model_loaded?
|
|
45
|
+
|
|
46
|
+
Graphiti.public_id_sources.register(resource_class, inverse_filter, parent_resource_class)
|
|
47
|
+
resource_class.guard_public_id_leak!(inverse_filter)
|
|
29
48
|
end
|
|
30
49
|
|
|
31
50
|
private
|
|
@@ -34,6 +53,18 @@ class Graphiti::Sideload::HasMany < Graphiti::Sideload
|
|
|
34
53
|
ids_for_parents(parents).join(",")
|
|
35
54
|
end
|
|
36
55
|
|
|
56
|
+
def internal_parent_filter(parents, filter_name)
|
|
57
|
+
return parent_filter(parents) unless resource_class.public_id_source_for(filter_name)
|
|
58
|
+
|
|
59
|
+
Graphiti::Util::InternalParam.new(ids_for_parents(parents))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def public_parent_filter(parents)
|
|
63
|
+
return parent_filter(parents) unless links_by_public_id?
|
|
64
|
+
|
|
65
|
+
parents.map { |parent| parent_resource_class.public_id_for(parent) }.compact.uniq.join(",")
|
|
66
|
+
end
|
|
67
|
+
|
|
37
68
|
def child_map(children)
|
|
38
69
|
children.group_by(&foreign_key)
|
|
39
70
|
end
|
|
@@ -16,7 +16,7 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def base_filter(parents)
|
|
19
|
-
{true_foreign_key =>
|
|
19
|
+
{true_foreign_key => internal_parent_filter(parents, true_foreign_key)}
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def infer_foreign_key
|
|
@@ -39,8 +39,8 @@ class Graphiti::Sideload::ManyToMany < Graphiti::Sideload::HasMany
|
|
|
39
39
|
# Do not recreate if filter already exists
|
|
40
40
|
unless resource_class.config[:filters].has_key?(inverse_filter.to_sym)
|
|
41
41
|
resource_class.filter inverse_filter, fk_type do
|
|
42
|
-
eq do |scope, value
|
|
43
|
-
self_ref.belongs_to_many_filter(scope,
|
|
42
|
+
eq do |scope, value, primary_keys:|
|
|
43
|
+
self_ref.belongs_to_many_filter(scope, primary_keys)
|
|
44
44
|
end
|
|
45
45
|
end
|
|
46
46
|
end
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -198,8 +198,18 @@ module Graphiti
|
|
|
198
198
|
|
|
199
199
|
# A custom link block means the author wants the link, so a false default does not silence it.
|
|
200
200
|
def link_mode
|
|
201
|
-
|
|
201
|
+
mode = requested_link_mode
|
|
202
|
+
return false unless mode
|
|
203
|
+
return false unless link_proc || link_hides_primary_key?
|
|
202
204
|
|
|
205
|
+
mode
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def requested_link_mode
|
|
209
|
+
@link.nil? ? default_link_mode : @link
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def default_link_mode
|
|
203
213
|
default = @parent_resource_class.relationship_links
|
|
204
214
|
(link_proc && default == false) ? true : default
|
|
205
215
|
end
|
|
@@ -208,6 +218,20 @@ module Graphiti
|
|
|
208
218
|
link_mode != false
|
|
209
219
|
end
|
|
210
220
|
|
|
221
|
+
def link_hides_primary_key?
|
|
222
|
+
true
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def collect_foreign_keys(parents, query)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def register_public_id_source
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def rendered_id_for(foreign_key, query)
|
|
232
|
+
foreign_key
|
|
233
|
+
end
|
|
234
|
+
|
|
211
235
|
def link_filter(parents)
|
|
212
236
|
base_filter(parents)
|
|
213
237
|
end
|
|
@@ -14,11 +14,12 @@ module Graphiti
|
|
|
14
14
|
# meta: { stats: { total: { count: 100 } } }
|
|
15
15
|
# }
|
|
16
16
|
class Payload
|
|
17
|
-
def initialize(resource, query, scope, data)
|
|
17
|
+
def initialize(resource, query, scope, data, group_by: nil)
|
|
18
18
|
@resource = resource
|
|
19
19
|
@query = query
|
|
20
20
|
@scope = scope
|
|
21
21
|
@data = data
|
|
22
|
+
@group_by = group_by
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
# Generate the payload for +{ meta: { stats: { ... } } }+
|
|
@@ -38,14 +39,30 @@ module Graphiti
|
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
def calculate_stat(name, function)
|
|
41
|
-
args = [@scope, name]
|
|
42
|
+
args = [@scope, @resource.model_attribute_for(name)]
|
|
42
43
|
args << @resource.context if function.arity >= 3
|
|
43
44
|
args << @data if function.arity == 4
|
|
44
|
-
function.call(*args)
|
|
45
|
+
translate_group_keys(function.call(*args))
|
|
45
46
|
end
|
|
46
47
|
|
|
47
48
|
private
|
|
48
49
|
|
|
50
|
+
def translate_group_keys(result)
|
|
51
|
+
return result unless result.is_a?(Hash) && @group_by
|
|
52
|
+
|
|
53
|
+
source = @resource.class.public_id_source_for(@group_by)
|
|
54
|
+
return result unless source
|
|
55
|
+
|
|
56
|
+
public_ids = source.public_ids_by(:_primary_key, result.keys.compact)
|
|
57
|
+
result.each_with_object({}) do |(primary_key, value), translated|
|
|
58
|
+
if primary_key.nil?
|
|
59
|
+
translated[nil] = value
|
|
60
|
+
elsif public_ids.key?(primary_key)
|
|
61
|
+
translated[public_ids[primary_key]] = value
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
49
66
|
def each_calculation(name, calculations)
|
|
50
67
|
calculations.each do |calc|
|
|
51
68
|
function = @resource.stat(name, calc)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Util
|
|
3
|
+
# Request params can only ever be strings, arrays and hashes, so a value wrapped in this class must have come from Graphiti itself.
|
|
4
|
+
class InternalParam
|
|
5
|
+
attr_reader :value
|
|
6
|
+
|
|
7
|
+
def initialize(value)
|
|
8
|
+
@value = value
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def blank?
|
|
12
|
+
value.blank?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/graphiti/util/link.rb
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
module Graphiti
|
|
2
2
|
module Util
|
|
3
3
|
class Link
|
|
4
|
-
def initialize(sideload, model)
|
|
4
|
+
def initialize(sideload, model, query = nil)
|
|
5
5
|
@sideload = sideload
|
|
6
6
|
@model = model
|
|
7
|
+
@query = query
|
|
7
8
|
@linkable = true
|
|
8
9
|
|
|
9
10
|
if @sideload.type == :polymorphic_belongs_to
|
|
@@ -30,12 +31,18 @@ module Graphiti
|
|
|
30
31
|
return false if @polymorphic_sideload_not_found
|
|
31
32
|
|
|
32
33
|
if @sideload.type == :belongs_to
|
|
33
|
-
|
|
34
|
+
!related_id.nil?
|
|
34
35
|
else
|
|
35
36
|
@linkable
|
|
36
37
|
end
|
|
37
38
|
end
|
|
38
39
|
|
|
40
|
+
def related_id
|
|
41
|
+
return @related_id if defined?(@related_id)
|
|
42
|
+
|
|
43
|
+
@related_id = @sideload.rendered_id_for(@model.send(@sideload.foreign_key), @query)
|
|
44
|
+
end
|
|
45
|
+
|
|
39
46
|
def raw_url
|
|
40
47
|
if @sideload.link_proc
|
|
41
48
|
@sideload.link_proc.call(@model)
|
|
@@ -76,7 +83,7 @@ module Graphiti
|
|
|
76
83
|
@path ||=
|
|
77
84
|
path = @sideload.resource.endpoint[:url].to_s
|
|
78
85
|
if @sideload.type == :belongs_to && !@sideload.remote?
|
|
79
|
-
path = "#{path}/#{
|
|
86
|
+
path = "#{path}/#{related_id}"
|
|
80
87
|
end
|
|
81
88
|
path
|
|
82
89
|
end
|
|
@@ -102,7 +102,7 @@ class Graphiti::Util::Persistence
|
|
|
102
102
|
def apply_derived_attributes(attributes, payload_attributes)
|
|
103
103
|
return unless @assigned_model
|
|
104
104
|
|
|
105
|
-
derived = attributes.reject { |key, value| payload_attributes[key] == value }
|
|
105
|
+
derived = @resource.decode_foreign_keys(attributes).reject { |key, value| payload_attributes[key] == value }
|
|
106
106
|
return if derived.empty?
|
|
107
107
|
|
|
108
108
|
@resource.assign_attributes(@assigned_model, derived, metadata)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Util
|
|
3
|
+
class PublicIdMap
|
|
4
|
+
def initialize(resource_class, filter_name)
|
|
5
|
+
@resource_class = resource_class
|
|
6
|
+
@filter_name = filter_name
|
|
7
|
+
@pending = Concurrent::Map.new
|
|
8
|
+
@resolved = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add(primary_keys)
|
|
12
|
+
primary_keys.each { |key| @pending[key] = true unless @resolved.key?(key) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def [](primary_key)
|
|
16
|
+
return if primary_key.nil?
|
|
17
|
+
|
|
18
|
+
resolve(@pending.keys) unless @pending.empty?
|
|
19
|
+
resolve([primary_key]) unless @resolved.key?(primary_key)
|
|
20
|
+
@resolved[primary_key]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def resolve(primary_keys)
|
|
26
|
+
found = @resource_class.public_ids_by(@filter_name, primary_keys)
|
|
27
|
+
primary_keys.each do |key|
|
|
28
|
+
@pending.delete(key)
|
|
29
|
+
@resolved[key] = found[key]
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module Util
|
|
3
|
+
class PublicIdSources
|
|
4
|
+
def initialize
|
|
5
|
+
@sources = {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def register(child_resource_class, filter_name, parent_resource_class)
|
|
9
|
+
key = [child_resource_class.name, filter_name.to_sym]
|
|
10
|
+
registered = @sources[key]
|
|
11
|
+
if registered && registered.name != parent_resource_class.name
|
|
12
|
+
raise Errors::ConflictingPublicIdSource.new(child_resource_class, filter_name, registered, parent_resource_class)
|
|
13
|
+
end
|
|
14
|
+
@sources[key] = parent_resource_class
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def [](child_resource_class, filter_name)
|
|
18
|
+
klass = child_resource_class
|
|
19
|
+
while klass
|
|
20
|
+
source = @sources[[klass.name, filter_name.to_sym]]
|
|
21
|
+
return source if source
|
|
22
|
+
klass = klass.superclass
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -83,10 +83,12 @@ module Graphiti
|
|
|
83
83
|
.new(resource_class_ref, sideload_ref, @object)
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
related_id = sideload_ref.rendered_id_for(foreign_key, @proxy.query)
|
|
87
|
+
|
|
88
|
+
unless related_id.nil?
|
|
87
89
|
{
|
|
88
90
|
type: sideload_ref.resource.type,
|
|
89
|
-
id:
|
|
91
|
+
id: related_id.to_s
|
|
90
92
|
}
|
|
91
93
|
end
|
|
92
94
|
end
|
|
@@ -98,7 +100,7 @@ module Graphiti
|
|
|
98
100
|
self_ref.send(:validate_link!) unless self_ref.send(:eagerly_validate_links?)
|
|
99
101
|
|
|
100
102
|
link(:related) do
|
|
101
|
-
::Graphiti::Util::Link.new(sideload_ref, @object).generate
|
|
103
|
+
::Graphiti::Util::Link.new(sideload_ref, @object, @proxy.query).generate
|
|
102
104
|
end
|
|
103
105
|
end
|
|
104
106
|
end
|