graphiti 2.0.0.beta.10 → 2.0.0.beta.12
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/ci.yml +6 -0
- data/.github/workflows/release.yml +2 -0
- data/.tool-versions +1 -1
- data/CHANGELOG.md +39 -0
- data/Rakefile +17 -12
- data/lib/generators/graphiti/generator_mixin.rb +5 -4
- data/lib/generators/graphiti/install_generator.rb +3 -0
- data/lib/generators/graphiti/locale_generator.rb +30 -0
- data/lib/generators/graphiti/templates/locale.yml.erb +22 -0
- data/lib/graphiti/adapters/abstract.rb +4 -3
- data/lib/graphiti/error_serializers/conflict_request.rb +1 -1
- data/lib/graphiti/error_serializers/deprecated_constants.rb +6 -16
- data/lib/graphiti/error_serializers/invalid_request.rb +6 -0
- data/lib/graphiti/errors.rb +4 -4
- data/lib/graphiti/query.rb +27 -0
- data/lib/graphiti/rails/exception_handlers.rb +17 -0
- data/lib/graphiti/rails/railtie.rb +10 -0
- data/lib/graphiti/rails/rake_helpers.rb +5 -0
- data/lib/graphiti/rails.rb +18 -0
- data/lib/graphiti/renderer.rb +4 -1
- data/lib/graphiti/request_validators/update_validator.rb +1 -5
- data/lib/graphiti/request_validators/validator.rb +6 -6
- data/lib/graphiti/resource/configuration.rb +11 -10
- data/lib/graphiti/resource/dsl.rb +5 -5
- data/lib/graphiti/resource/links.rb +3 -0
- data/lib/graphiti/schema/check.rb +69 -0
- data/lib/graphiti/schema.rb +8 -9
- data/lib/graphiti/scope.rb +51 -22
- data/lib/graphiti/scoping/filter.rb +2 -2
- data/lib/graphiti/serializer.rb +25 -9
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +9 -9
- data/lib/graphiti/sideload.rb +36 -25
- data/lib/graphiti/spec_helpers/matchers.rb +1 -1
- data/lib/graphiti/spec_helpers/rspec.rb +5 -13
- data/lib/graphiti/util/simple_errors.rb +26 -5
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti.rb +1 -0
- data/lib/tasks/graphiti.rake +18 -0
- data/package.json +1 -1
- metadata +4 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
class Schema
|
|
3
|
+
class Check
|
|
4
|
+
attr_reader :path, :errors
|
|
5
|
+
|
|
6
|
+
def initialize(schema, path)
|
|
7
|
+
@path = path
|
|
8
|
+
@schema = schema
|
|
9
|
+
@generated = normalize(schema)
|
|
10
|
+
@committed = normalize(JSON.parse(File.read(path))) if File.exist?(path)
|
|
11
|
+
@errors = @committed ? SchemaDiff.new(@committed, @generated).compare : []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def missing?
|
|
15
|
+
@committed.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stale?
|
|
19
|
+
!missing? && @committed != @generated
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def compatible?
|
|
23
|
+
errors.empty?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def ok?
|
|
27
|
+
!missing? && !stale? && compatible?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write!
|
|
31
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
32
|
+
File.write(path, JSON.pretty_generate(@schema))
|
|
33
|
+
path
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def message
|
|
37
|
+
return "Schema is up to date: #{path}" if ok?
|
|
38
|
+
return "#{missing_message}\n\n#{regenerate}" if missing?
|
|
39
|
+
return "#{incompatible_message}\n\n#{errors.join("\n")}" unless compatible?
|
|
40
|
+
|
|
41
|
+
"Schema file is outdated: #{path}\n\n#{regenerate}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def normalize(schema)
|
|
47
|
+
JSON.parse(JSON.generate(schema))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def missing_message
|
|
51
|
+
"Schema file not found: #{path}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def incompatible_message
|
|
55
|
+
<<~MSG.chomp
|
|
56
|
+
Found backwards-incompatibilities in schema: #{path}
|
|
57
|
+
|
|
58
|
+
Re-run with FORCE_SCHEMA=true to accept them and overwrite the file.
|
|
59
|
+
|
|
60
|
+
Incompatibilities:
|
|
61
|
+
MSG
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def regenerate
|
|
65
|
+
"Run `rake graphiti:schema:generate` and commit the file."
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/graphiti/schema.rb
CHANGED
|
@@ -11,19 +11,18 @@ module Graphiti
|
|
|
11
11
|
new(resources).generate
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def self.generate!(resources = nil)
|
|
15
|
-
|
|
14
|
+
def self.generate!(resources = nil, path: Graphiti.config.schema_path, force: ENV["FORCE_SCHEMA"] == "true")
|
|
15
|
+
result = check(resources, path: path)
|
|
16
|
+
return result.errors if !force && !result.compatible?
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
old = JSON.parse(File.read(Graphiti.config.schema_path))
|
|
19
|
-
errors = Graphiti::SchemaDiff.new(old, schema).compare
|
|
20
|
-
return errors if errors.any?
|
|
21
|
-
end
|
|
22
|
-
FileUtils.mkdir_p(Graphiti.config.schema_path.to_s.gsub("/schema.json", ""))
|
|
23
|
-
File.write(Graphiti.config.schema_path, JSON.pretty_generate(schema))
|
|
18
|
+
result.write!
|
|
24
19
|
[]
|
|
25
20
|
end
|
|
26
21
|
|
|
22
|
+
def self.check(resources = nil, path: Graphiti.config.schema_path)
|
|
23
|
+
Check.new(generate(resources), path)
|
|
24
|
+
end
|
|
25
|
+
|
|
27
26
|
def initialize(resources)
|
|
28
27
|
@resources = resources.sort_by(&:name)
|
|
29
28
|
@remote_resources = @resources.select(&:remote?)
|
data/lib/graphiti/scope.rb
CHANGED
|
@@ -59,6 +59,9 @@ module Graphiti
|
|
|
59
59
|
@query = query
|
|
60
60
|
@opts = opts
|
|
61
61
|
|
|
62
|
+
@resolved_sideload_proxies = {}
|
|
63
|
+
@resolved_sideload_proxies_lock = Mutex.new
|
|
64
|
+
|
|
62
65
|
@object = @resource.around_scoping(@object, @query.hash) { |scope|
|
|
63
66
|
apply_scoping(scope, opts)
|
|
64
67
|
}
|
|
@@ -67,7 +70,7 @@ module Graphiti
|
|
|
67
70
|
def resolve(&blk)
|
|
68
71
|
# The caller blocks on .value! either way, so concurrency only benefits parallel sideloads
|
|
69
72
|
# See https://github.com/graphiti-api/graphiti/issues/505
|
|
70
|
-
if self.class.resolve_synchronously? || !
|
|
73
|
+
if self.class.resolve_synchronously? || !overlapping_sideloads?
|
|
71
74
|
sync_resolve(&blk)
|
|
72
75
|
else
|
|
73
76
|
future_resolve(&blk).value!
|
|
@@ -140,9 +143,6 @@ module Graphiti
|
|
|
140
143
|
|
|
141
144
|
private
|
|
142
145
|
|
|
143
|
-
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
144
|
-
# Resolves the resource and its sideloads inline without any promise
|
|
145
|
-
# machinery. See #resolve.
|
|
146
146
|
def sync_resolve(&blk)
|
|
147
147
|
return [] if @query.zero_results?
|
|
148
148
|
|
|
@@ -151,14 +151,15 @@ module Graphiti
|
|
|
151
151
|
resolved
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
-
# Synchronous counterpart to #future_resolve_sideloads, used when
|
|
155
|
-
# concurrency is off. Resolves each sideload inline. See #resolve_sideloads.
|
|
156
154
|
def sync_resolve_sideloads(results)
|
|
157
155
|
return if results == []
|
|
158
156
|
|
|
159
|
-
|
|
157
|
+
reset_captured_sideload_proxies
|
|
158
|
+
each_applicable_sideload do |name, sideload, sideload_query|
|
|
160
159
|
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
161
|
-
sideload.
|
|
160
|
+
sideload.sync_resolve(results, sideload_query, @resource) do |proxy|
|
|
161
|
+
capture_sideload_proxy(name, proxy)
|
|
162
|
+
end
|
|
162
163
|
end
|
|
163
164
|
end
|
|
164
165
|
|
|
@@ -176,7 +177,7 @@ module Graphiti
|
|
|
176
177
|
assign_serializer(resolved)
|
|
177
178
|
yield resolved if block_given?
|
|
178
179
|
@opts[:after_resolve]&.call(resolved)
|
|
179
|
-
resolved
|
|
180
|
+
@resolved_records = resolved
|
|
180
181
|
end
|
|
181
182
|
|
|
182
183
|
# Must run before sideloads assign, so every include path populates the
|
|
@@ -198,6 +199,8 @@ module Graphiti
|
|
|
198
199
|
|
|
199
200
|
# A customized sideload can load a record another path would not, so it keeps its own instances.
|
|
200
201
|
def deduplicable?
|
|
202
|
+
return false unless @query.repeated_resource_classes.include?(@resource.class)
|
|
203
|
+
|
|
201
204
|
sideload = @opts[:sideload]
|
|
202
205
|
return true unless sideload
|
|
203
206
|
|
|
@@ -207,9 +210,15 @@ module Graphiti
|
|
|
207
210
|
sideload.primary_key == :id
|
|
208
211
|
end
|
|
209
212
|
|
|
210
|
-
#
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
# One sideload has nothing to run beside it, so the pool would hand the work
|
|
214
|
+
# to another thread and wait for it. A chain is one at every level. A
|
|
215
|
+
# polymorphic sideload counts as its children, which do run beside each other.
|
|
216
|
+
def overlapping_sideloads?
|
|
217
|
+
found = 0
|
|
218
|
+
each_applicable_sideload do |_, sideload, _|
|
|
219
|
+
found += sideload.respond_to?(:children) ? sideload.children.size : 1
|
|
220
|
+
return true if found > 1
|
|
221
|
+
end
|
|
213
222
|
false
|
|
214
223
|
end
|
|
215
224
|
|
|
@@ -218,18 +227,34 @@ module Graphiti
|
|
|
218
227
|
sideload = @resource.class.sideload(name)
|
|
219
228
|
next if sideload.nil? || sideload.shared_remote?
|
|
220
229
|
|
|
221
|
-
yield sideload, sideload_query
|
|
230
|
+
yield name, sideload, sideload_query
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# The write path resolves sideloads twice on one scope, which would double every proxy in the cache key.
|
|
235
|
+
def reset_captured_sideload_proxies
|
|
236
|
+
@resolved_sideload_proxies_lock.synchronize { @resolved_sideload_proxies.clear }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# A proxy built while resolving is already resolved all the way down.
|
|
240
|
+
def capture_sideload_proxy(name, proxy)
|
|
241
|
+
@resolved_sideload_proxies_lock.synchronize do
|
|
242
|
+
captured = (@resolved_sideload_proxies[name] ||= [])
|
|
243
|
+
captured << proxy unless proxy.nil? || proxy == []
|
|
222
244
|
end
|
|
223
245
|
end
|
|
224
246
|
|
|
225
247
|
def future_resolve_sideloads(results)
|
|
226
248
|
return Concurrent::Promises.fulfilled_future(nil, self.class.global_thread_pool_executor) if results == []
|
|
227
249
|
|
|
250
|
+
reset_captured_sideload_proxies
|
|
228
251
|
sideload_promises = []
|
|
229
|
-
each_applicable_sideload do |sideload, sideload_query|
|
|
252
|
+
each_applicable_sideload do |name, sideload, sideload_query|
|
|
230
253
|
promise = future_with_context(results, sideload_query, @resource) do |parent_results, future_query, parent_resource|
|
|
231
254
|
Graphiti.config.before_sideload&.call(Graphiti.context)
|
|
232
|
-
sideload.future_resolve(parent_results, future_query, parent_resource)
|
|
255
|
+
sideload.future_resolve(parent_results, future_query, parent_resource) do |proxy|
|
|
256
|
+
capture_sideload_proxy(name, proxy)
|
|
257
|
+
end
|
|
233
258
|
end
|
|
234
259
|
sideload_promises << promise.flat
|
|
235
260
|
end
|
|
@@ -361,15 +386,19 @@ module Graphiti
|
|
|
361
386
|
|
|
362
387
|
def sideload_resource_proxies
|
|
363
388
|
@sideload_resource_proxies ||= begin
|
|
364
|
-
|
|
365
|
-
results = @
|
|
389
|
+
# Reached after the response resolved, where resolving again re-applies before_resolve to a mutated scope.
|
|
390
|
+
results = @resolved_records
|
|
391
|
+
if results.nil?
|
|
392
|
+
@object = @resource.before_resolve(@object, @query)
|
|
393
|
+
results = @resource.resolve(@object)
|
|
394
|
+
end
|
|
366
395
|
|
|
367
396
|
[].tap do |proxies|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
397
|
+
each_applicable_sideload do |name, sideload, q|
|
|
398
|
+
captured = @resolved_sideload_proxies[name]
|
|
399
|
+
if captured
|
|
400
|
+
proxies.concat(captured)
|
|
401
|
+
else
|
|
373
402
|
proxies << sideload.build_resource_proxy(results, q, parent_resource)
|
|
374
403
|
end
|
|
375
404
|
end
|
|
@@ -209,7 +209,7 @@ module Graphiti
|
|
|
209
209
|
end
|
|
210
210
|
|
|
211
211
|
def parse_string_null(filter, value)
|
|
212
|
-
return value unless filter[:blanks] == :
|
|
212
|
+
return value unless filter[:blanks] == :null
|
|
213
213
|
return value.map { |item| (item == "null") ? nil : item } if value.is_a?(Array)
|
|
214
214
|
return if value == "null"
|
|
215
215
|
|
|
@@ -217,7 +217,7 @@ module Graphiti
|
|
|
217
217
|
end
|
|
218
218
|
|
|
219
219
|
def check_blank_filters!(resource, filter, value)
|
|
220
|
-
return unless filter.values[0][:blanks] == :
|
|
220
|
+
return unless filter.values[0][:blanks] == :rejected
|
|
221
221
|
|
|
222
222
|
if value.nil? || value.empty? || value == "null"
|
|
223
223
|
raise Errors::InvalidFilterValue.new(resource, filter, "(empty)")
|
data/lib/graphiti/serializer.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
module Graphiti
|
|
2
2
|
class Serializer < JSONAPI::Serializable::Resource
|
|
3
|
-
UNREQUESTED_LINKS = [false, nil, "false"].freeze
|
|
4
|
-
|
|
5
3
|
include Graphiti::Extensions::BooleanAttribute
|
|
6
4
|
include Graphiti::Extensions::ExtraAttribute
|
|
7
5
|
include Graphiti::SerializableHash
|
|
@@ -26,6 +24,16 @@ module Graphiti
|
|
|
26
24
|
self.relationship_condition_blocks ||= {}
|
|
27
25
|
self.relationship_sideloads ||= {}
|
|
28
26
|
|
|
27
|
+
# Keyed on the sideloads hash itself, which is reassigned whenever a
|
|
28
|
+
# relationship is applied, so the answer is recomputed exactly then.
|
|
29
|
+
def self.on_demand_links?
|
|
30
|
+
sideloads = relationship_sideloads
|
|
31
|
+
return @on_demand_links.last if @on_demand_links&.first.equal?(sideloads)
|
|
32
|
+
|
|
33
|
+
@on_demand_links = [sideloads, sideloads.each_value.any? { |sideload| sideload.link_mode == :on_demand }]
|
|
34
|
+
@on_demand_links.last
|
|
35
|
+
end
|
|
36
|
+
|
|
29
37
|
def self.inherited(klass)
|
|
30
38
|
super
|
|
31
39
|
klass.class_eval do
|
|
@@ -76,7 +84,7 @@ module Graphiti
|
|
|
76
84
|
|
|
77
85
|
def as_jsonapi(kwargs = {})
|
|
78
86
|
super(**kwargs).tap do |hash|
|
|
79
|
-
strip_relationships!(hash)
|
|
87
|
+
strip_relationships!(hash)
|
|
80
88
|
add_links!(hash)
|
|
81
89
|
end
|
|
82
90
|
end
|
|
@@ -102,21 +110,29 @@ module Graphiti
|
|
|
102
110
|
hash[:links] = @resource.links(@object) if @resource.links?
|
|
103
111
|
end
|
|
104
112
|
|
|
105
|
-
#
|
|
106
|
-
# serializes as an empty stub, which JSON:API forbids.
|
|
113
|
+
# The meta: {included: false} stub is jsonapi-serializable's filler, not JSON:API.
|
|
107
114
|
def strip_relationships!(hash)
|
|
115
|
+
placeholders = relationship_placeholders?
|
|
116
|
+
return if placeholders && !strip_on_demand_relationships?
|
|
117
|
+
|
|
108
118
|
hash[:relationships]&.reject! do |name, payload|
|
|
109
119
|
next false if payload.key?(:data) || payload.key?(:links)
|
|
120
|
+
next true unless placeholders
|
|
110
121
|
|
|
111
122
|
self.class.relationship_sideloads[name]&.link_mode == :on_demand
|
|
112
123
|
end
|
|
113
124
|
end
|
|
114
125
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
126
|
+
# A remote resource exposes a stand-in object rather than a Resource.
|
|
127
|
+
def relationship_placeholders?
|
|
128
|
+
resource_class = @resource.class
|
|
129
|
+
return Resource.relationship_placeholders unless resource_class.respond_to?(:relationship_placeholders)
|
|
130
|
+
|
|
131
|
+
resource_class.relationship_placeholders
|
|
132
|
+
end
|
|
118
133
|
|
|
119
|
-
|
|
134
|
+
def strip_on_demand_relationships?
|
|
135
|
+
self.class.on_demand_links? && !@proxy&.query&.render_link?(:on_demand)
|
|
120
136
|
end
|
|
121
137
|
end
|
|
122
138
|
end
|
|
@@ -107,18 +107,18 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
|
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
def resolve(parents, query, graph_parent)
|
|
110
|
+
def resolve(parents, query, graph_parent, &proxy_block)
|
|
111
111
|
if ::Graphiti::Scope.resolve_synchronously?
|
|
112
|
-
sync_resolve(parents, query, graph_parent)
|
|
112
|
+
sync_resolve(parents, query, graph_parent, &proxy_block)
|
|
113
113
|
else
|
|
114
|
-
future_resolve(parents, query, graph_parent).value!
|
|
114
|
+
future_resolve(parents, query, graph_parent, &proxy_block).value!
|
|
115
115
|
end
|
|
116
116
|
end
|
|
117
117
|
|
|
118
|
-
def future_resolve(parents, query, graph_parent)
|
|
118
|
+
def future_resolve(parents, query, graph_parent, &proxy_block)
|
|
119
119
|
promises = []
|
|
120
120
|
each_resolvable_group(parents, query) do |child, group, child_query|
|
|
121
|
-
promises << child.future_resolve(group, child_query, graph_parent)
|
|
121
|
+
promises << child.future_resolve(group, child_query, graph_parent, &proxy_block)
|
|
122
122
|
end
|
|
123
123
|
return promises.first if promises.one?
|
|
124
124
|
|
|
@@ -130,14 +130,14 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
|
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def sync_resolve(parents, query, graph_parent)
|
|
133
|
+
def sync_resolve(parents, query, graph_parent, &proxy_block)
|
|
136
134
|
each_resolvable_group(parents, query) do |child, group, child_query|
|
|
137
|
-
child.
|
|
135
|
+
child.sync_resolve(group, child_query, graph_parent, &proxy_block)
|
|
138
136
|
end
|
|
139
137
|
end
|
|
140
138
|
|
|
139
|
+
private
|
|
140
|
+
|
|
141
141
|
# Group parents by their polymorphic type and yield each group's child
|
|
142
142
|
# sideload alongside a query pruned to the sideloads that child supports.
|
|
143
143
|
def each_resolvable_group(parents, query)
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -152,8 +152,12 @@ module Graphiti
|
|
|
152
152
|
!!@polymorphic_as
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
# Every check behind the blocker is static sideload configuration, and this
|
|
156
|
+
# is asked once per rendered record per relationship.
|
|
155
157
|
def resource_ids_from_foreign_key?
|
|
156
|
-
|
|
158
|
+
return @resource_ids_from_foreign_key unless @resource_ids_from_foreign_key.nil?
|
|
159
|
+
|
|
160
|
+
@resource_ids_from_foreign_key = resource_ids_blocker.nil?
|
|
157
161
|
end
|
|
158
162
|
|
|
159
163
|
def resource_ids_blocker
|
|
@@ -285,14 +289,20 @@ module Graphiti
|
|
|
285
289
|
proxy
|
|
286
290
|
end
|
|
287
291
|
|
|
288
|
-
def load(parents, query, graph_parent)
|
|
292
|
+
def load(parents, query, graph_parent, &proxy_block)
|
|
289
293
|
if Scope.resolve_synchronously?
|
|
290
|
-
|
|
294
|
+
sync_load(parents, query, graph_parent, &proxy_block)
|
|
291
295
|
else
|
|
292
|
-
future_load(parents, query, graph_parent).value!
|
|
296
|
+
future_load(parents, query, graph_parent, &proxy_block).value!
|
|
293
297
|
end
|
|
294
298
|
end
|
|
295
299
|
|
|
300
|
+
def sync_load(parents, query, graph_parent, &proxy_block)
|
|
301
|
+
proxy = build_resource_proxy(parents, query, graph_parent)
|
|
302
|
+
proxy_block&.call(proxy)
|
|
303
|
+
proxy.to_a
|
|
304
|
+
end
|
|
305
|
+
|
|
296
306
|
# Override in subclass
|
|
297
307
|
def infer_foreign_key
|
|
298
308
|
model = parent_resource_class.model
|
|
@@ -340,15 +350,30 @@ module Graphiti
|
|
|
340
350
|
children.replace(associated) if track_associated
|
|
341
351
|
end
|
|
342
352
|
|
|
343
|
-
def resolve(parents, query, graph_parent)
|
|
353
|
+
def resolve(parents, query, graph_parent, &proxy_block)
|
|
344
354
|
if Scope.resolve_synchronously?
|
|
345
|
-
sync_resolve(parents, query, graph_parent)
|
|
355
|
+
sync_resolve(parents, query, graph_parent, &proxy_block)
|
|
356
|
+
else
|
|
357
|
+
future_resolve(parents, query, graph_parent, &proxy_block).value!
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Called by a scope that already decided to stay inline, so it must not consult the pool again.
|
|
362
|
+
# A scope_proc builds a Scope rather than a proxy, and that nested scope decides for itself.
|
|
363
|
+
def sync_resolve(parents, query, graph_parent, &proxy_block)
|
|
364
|
+
assert_singular!(parents)
|
|
365
|
+
|
|
366
|
+
if self.class.scope_proc
|
|
367
|
+
build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
|
|
368
|
+
fire_assign(parents, sideload_results)
|
|
369
|
+
end
|
|
346
370
|
else
|
|
347
|
-
|
|
371
|
+
sync_load(parents, query, graph_parent, &proxy_block)
|
|
348
372
|
end
|
|
349
373
|
end
|
|
350
374
|
|
|
351
|
-
|
|
375
|
+
# A scope_proc builds a Scope rather than a proxy, and a Scope's cache key omits what a proxy's carries.
|
|
376
|
+
def future_resolve(parents, query, graph_parent, &proxy_block)
|
|
352
377
|
assert_singular!(parents)
|
|
353
378
|
|
|
354
379
|
if self.class.scope_proc
|
|
@@ -356,7 +381,7 @@ module Graphiti
|
|
|
356
381
|
fire_assign(parents, sideload_results)
|
|
357
382
|
end
|
|
358
383
|
else
|
|
359
|
-
future_load(parents, query, graph_parent)
|
|
384
|
+
future_load(parents, query, graph_parent, &proxy_block)
|
|
360
385
|
end
|
|
361
386
|
end
|
|
362
387
|
|
|
@@ -420,21 +445,6 @@ module Graphiti
|
|
|
420
445
|
|
|
421
446
|
private
|
|
422
447
|
|
|
423
|
-
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
424
|
-
# Resolves inline via the synchronous Scope#resolve / #load paths (no
|
|
425
|
-
# promises). See Scope#sync_resolve_sideloads.
|
|
426
|
-
def sync_resolve(parents, query, graph_parent)
|
|
427
|
-
assert_singular!(parents)
|
|
428
|
-
|
|
429
|
-
if self.class.scope_proc
|
|
430
|
-
build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
|
|
431
|
-
fire_assign(parents, sideload_results)
|
|
432
|
-
end
|
|
433
|
-
else
|
|
434
|
-
load(parents, query, graph_parent)
|
|
435
|
-
end
|
|
436
|
-
end
|
|
437
|
-
|
|
438
448
|
def assert_singular!(parents)
|
|
439
449
|
if single? && parents.length > 1
|
|
440
450
|
raise Errors::SingularSideload.new(self, parents.length)
|
|
@@ -451,8 +461,9 @@ module Graphiti
|
|
|
451
461
|
default_paginate: false
|
|
452
462
|
end
|
|
453
463
|
|
|
454
|
-
def future_load(parents, query, graph_parent)
|
|
464
|
+
def future_load(parents, query, graph_parent, &proxy_block)
|
|
455
465
|
proxy = build_resource_proxy(parents, query, graph_parent)
|
|
466
|
+
proxy_block&.call(proxy)
|
|
456
467
|
proxy.respond_to?(:future_resolve_data) ? proxy.future_resolve_data : Concurrent::Promises.fulfilled_future(proxy)
|
|
457
468
|
end
|
|
458
469
|
|
|
@@ -186,7 +186,7 @@ module Graphiti
|
|
|
186
186
|
# @param [Symbol] type
|
|
187
187
|
#
|
|
188
188
|
# @example expect(subject).to filter_attribute(:name, :string)
|
|
189
|
-
# @example expect(subject).to filter_attribute(:name, :string).with_options(blanks: :
|
|
189
|
+
# @example expect(subject).to filter_attribute(:name, :string).with_options(blanks: :null)
|
|
190
190
|
# @example expect(subject).not_to filter_attribute(:name, :string)
|
|
191
191
|
def filter_attribute(attribute, type)
|
|
192
192
|
FilterAttributeMatcher.new(attribute, type)
|
|
@@ -127,22 +127,14 @@ module Graphiti
|
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
def self.schema!(resources = nil)
|
|
130
|
+
def self.schema!(resources = nil, path: nil)
|
|
131
131
|
::RSpec.describe "Graphiti Schema" do
|
|
132
132
|
it "generates a backwards-compatible schema" do
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
check = Graphiti::Schema.check(resources, path: path || Graphiti.config.schema_path)
|
|
134
|
+
forced = ENV["FORCE_SCHEMA"] == "true"
|
|
135
|
+
check.write! if check.compatible? || forced
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
MSG
|
|
139
|
-
|
|
140
|
-
errors = Graphiti::Schema.generate!(resources)
|
|
141
|
-
errors.each do |e|
|
|
142
|
-
message << "#{e}\n"
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
expect(errors.empty?).to eq(true), message
|
|
137
|
+
expect(check.compatible? || forced).to eq(true), check.message
|
|
146
138
|
end
|
|
147
139
|
end
|
|
148
140
|
end
|
|
@@ -6,6 +6,21 @@ module Graphiti
|
|
|
6
6
|
class SimpleErrors
|
|
7
7
|
include Enumerable
|
|
8
8
|
|
|
9
|
+
# Overridable under graphiti.errors.messages.
|
|
10
|
+
DEFAULT_MESSAGES = {
|
|
11
|
+
missing: "is missing",
|
|
12
|
+
invalid: "must be an object",
|
|
13
|
+
invalid_relationship: "is not a valid relationship",
|
|
14
|
+
unwritable_relationship: "cannot be written",
|
|
15
|
+
unknown_attribute: "is an unknown attribute",
|
|
16
|
+
unwritable_attribute: "cannot be written",
|
|
17
|
+
type_error: "should be type %{type}",
|
|
18
|
+
attribute_mismatch: "does not match the server endpoint"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# Joins an attribute to its message, like Rails' own errors.format.
|
|
22
|
+
DEFAULT_FORMAT = "%{attribute} %{message}"
|
|
23
|
+
|
|
9
24
|
attr_reader :messages, :details
|
|
10
25
|
|
|
11
26
|
def initialize(validation_target)
|
|
@@ -50,11 +65,9 @@ module Graphiti
|
|
|
50
65
|
end
|
|
51
66
|
alias_method :blank?, :empty?
|
|
52
67
|
|
|
53
|
-
def add(attribute, code, message: nil)
|
|
54
|
-
message ||= "is #{code.to_s.humanize.downcase}"
|
|
55
|
-
|
|
68
|
+
def add(attribute, code, message: nil, **interpolations)
|
|
56
69
|
details[attribute.to_sym] << {error: code}
|
|
57
|
-
messages[attribute.to_sym] << message
|
|
70
|
+
messages[attribute.to_sym] << translate(code, message, **interpolations, attribute: attribute)
|
|
58
71
|
end
|
|
59
72
|
|
|
60
73
|
def added?(attribute, code)
|
|
@@ -73,11 +86,19 @@ module Graphiti
|
|
|
73
86
|
|
|
74
87
|
def full_message(attribute, message)
|
|
75
88
|
return message if attribute == :base
|
|
76
|
-
|
|
89
|
+
|
|
90
|
+
translate(:format, DEFAULT_FORMAT, [:graphiti, :errors], attribute: attribute, message: message)
|
|
77
91
|
end
|
|
78
92
|
|
|
79
93
|
private
|
|
80
94
|
|
|
95
|
+
def translate(key, fallback, scope = [:graphiti, :errors, :messages], **interpolations)
|
|
96
|
+
fallback ||= DEFAULT_MESSAGES.fetch(key) { "is #{key.to_s.humanize.downcase}" }
|
|
97
|
+
return fallback % interpolations unless defined?(::I18n)
|
|
98
|
+
|
|
99
|
+
::I18n.t(key, scope: scope, default: fallback, **interpolations)
|
|
100
|
+
end
|
|
101
|
+
|
|
81
102
|
def apply_default_array(hash)
|
|
82
103
|
hash.default_proc = proc { |h, key| h[key] = [] }
|
|
83
104
|
hash
|
data/lib/graphiti/version.rb
CHANGED
data/lib/graphiti.rb
CHANGED
|
@@ -175,6 +175,7 @@ require "graphiti/audit"
|
|
|
175
175
|
require "graphiti/audit/report"
|
|
176
176
|
require "graphiti/schema"
|
|
177
177
|
require "graphiti/schema_diff"
|
|
178
|
+
require "graphiti/schema/check"
|
|
178
179
|
require "graphiti/adapters/abstract"
|
|
179
180
|
require "graphiti/resource/sideloading"
|
|
180
181
|
require "graphiti/resource/links"
|
data/lib/tasks/graphiti.rake
CHANGED
|
@@ -16,6 +16,24 @@ namespace :graphiti do
|
|
|
16
16
|
Graphiti::Debugger.flush if debug
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
namespace :schema do
|
|
20
|
+
desc "Write the schema file. Refuses backwards-incompatible changes unless FORCE_SCHEMA=true. Takes an optional path, defaulting to Graphiti.config.schema_path."
|
|
21
|
+
task :generate, [:path] => [:environment] do |_, args|
|
|
22
|
+
check = Graphiti::Schema.check(path: helpers.schema_path(args[:path]))
|
|
23
|
+
abort check.message unless check.compatible? || ENV["FORCE_SCHEMA"] == "true"
|
|
24
|
+
|
|
25
|
+
puts "Schema written: #{check.write!}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "Fail unless the committed schema file exists, is up to date, and is backwards-compatible. Takes an optional path, defaulting to Graphiti.config.schema_path."
|
|
29
|
+
task :check, [:path] => [:environment] do |_, args|
|
|
30
|
+
check = Graphiti::Schema.check(path: helpers.schema_path(args[:path]))
|
|
31
|
+
abort check.message unless check.ok?
|
|
32
|
+
|
|
33
|
+
puts check.message
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
19
37
|
desc "Audit every relationship: what will raise, what loads to render ids, which render no ids, and which checks passed."
|
|
20
38
|
task audit: [:environment] do
|
|
21
39
|
helpers.setup_rails!
|
data/package.json
CHANGED
|
@@ -142,7 +142,7 @@
|
|
|
142
142
|
[
|
|
143
143
|
"@semantic-release/exec",
|
|
144
144
|
{
|
|
145
|
-
"prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --
|
|
145
|
+
"prepareCmd": "bundle exec ruby spec/performance/measure_releases.rb --promote v${nextRelease.version}"
|
|
146
146
|
}
|
|
147
147
|
],
|
|
148
148
|
[
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: graphiti
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.0.beta.
|
|
4
|
+
version: 2.0.0.beta.12
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lee Richmond
|
|
@@ -287,6 +287,7 @@ files:
|
|
|
287
287
|
- lib/generators/graphiti/api_test_generator.rb
|
|
288
288
|
- lib/generators/graphiti/generator_mixin.rb
|
|
289
289
|
- lib/generators/graphiti/install_generator.rb
|
|
290
|
+
- lib/generators/graphiti/locale_generator.rb
|
|
290
291
|
- lib/generators/graphiti/resource_generator.rb
|
|
291
292
|
- lib/generators/graphiti/resource_test_generator.rb
|
|
292
293
|
- lib/generators/graphiti/templates/application_resource.rb.erb
|
|
@@ -294,6 +295,7 @@ files:
|
|
|
294
295
|
- lib/generators/graphiti/templates/create_request_spec.rb.erb
|
|
295
296
|
- lib/generators/graphiti/templates/destroy_request_spec.rb.erb
|
|
296
297
|
- lib/generators/graphiti/templates/index_request_spec.rb.erb
|
|
298
|
+
- lib/generators/graphiti/templates/locale.yml.erb
|
|
297
299
|
- lib/generators/graphiti/templates/resource.rb.erb
|
|
298
300
|
- lib/generators/graphiti/templates/resource_reads_spec.rb.erb
|
|
299
301
|
- lib/generators/graphiti/templates/resource_writes_spec.rb.erb
|
|
@@ -359,6 +361,7 @@ files:
|
|
|
359
361
|
- lib/graphiti/responders.rb
|
|
360
362
|
- lib/graphiti/runner.rb
|
|
361
363
|
- lib/graphiti/schema.rb
|
|
364
|
+
- lib/graphiti/schema/check.rb
|
|
362
365
|
- lib/graphiti/schema_diff.rb
|
|
363
366
|
- lib/graphiti/scope.rb
|
|
364
367
|
- lib/graphiti/scoping/base.rb
|