graphiti 2.0.0.beta.3 → 2.0.0.beta.5
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 +30 -86
- data/.github/workflows/docs.yml +57 -0
- data/.github/workflows/release.yml +8 -8
- data/.gitignore +7 -0
- data/.npmrc +9 -0
- data/.standard.yml +4 -1
- data/Appraisals +33 -32
- data/CHANGELOG.md +48 -0
- data/README.md +13 -2
- data/UPGRADING.md +2 -58
- data/docs/concepts/backends-and-models.md +122 -0
- data/docs/concepts/endpoints.md +183 -0
- data/docs/concepts/links.md +212 -0
- data/docs/concepts/overview.md +80 -0
- data/docs/concepts/persisting.md +376 -0
- data/docs/concepts/relationships.md +527 -0
- data/docs/concepts/resources.md +677 -0
- data/docs/getting-started/first-api.md +289 -0
- data/docs/getting-started/installation.md +185 -0
- data/docs/intro.md +307 -0
- data/docs/js/authentication.md +63 -0
- data/docs/js/ddau.md +20 -0
- data/docs/js/extra-params.md +41 -0
- data/docs/js/index.md +112 -0
- data/docs/js/installation.md +120 -0
- data/docs/js/middleware.md +72 -0
- data/docs/js/models.md +202 -0
- data/docs/js/reads.md +494 -0
- data/docs/js/state-syncing.md +100 -0
- data/docs/js/writes.md +373 -0
- data/docs/reference/vandal.md +63 -0
- data/docs/reference/why.md +13 -0
- data/docs/topics/authorization.md +155 -0
- data/docs/topics/caching.md +55 -0
- data/docs/topics/customizing-sideloads.md +156 -0
- data/docs/topics/debugging.md +216 -0
- data/docs/topics/error-handling.md +210 -0
- data/docs/topics/etags.md +46 -0
- data/docs/topics/hopping-relationships.md +149 -0
- data/docs/topics/json-attributes.md +77 -0
- data/docs/topics/openstruct-models.md +50 -0
- data/docs/topics/remote-resources.md +291 -0
- data/docs/topics/testing.md +894 -0
- data/docs/topics/without-activerecord.md +324 -0
- data/docs/tutorial/index.md +58 -0
- data/docs/tutorial/step_0.md +107 -0
- data/docs/tutorial/step_1.md +199 -0
- data/docs/tutorial/step_2.md +312 -0
- data/docs/tutorial/step_3.md +142 -0
- data/docs/tutorial/step_4.md +135 -0
- data/docs/tutorial/step_5.md +69 -0
- data/docs/tutorial/step_6.md +82 -0
- data/docs/tutorial/step_7.md +205 -0
- data/docs/tutorial/step_8.md +128 -0
- data/docs/tutorial/step_9.md +171 -0
- data/docs/upgrading.md +260 -0
- data/gemfiles/rails_7_1.gemfile +4 -3
- data/gemfiles/{rails_7_2_graphiti_rails.gemfile → rails_7_2.gemfile} +3 -3
- data/gemfiles/{rails_8_1_graphiti_rails.gemfile → rails_8_0.gemfile} +3 -3
- data/gemfiles/{rails_8_0_graphiti_rails.gemfile → rails_8_1.gemfile} +3 -3
- data/graphiti.gemspec +7 -23
- data/{deprecated_generators → lib/generators}/graphiti/api_test_generator.rb +7 -1
- data/{deprecated_generators → lib/generators}/graphiti/generator_mixin.rb +14 -1
- data/{deprecated_generators → lib/generators}/graphiti/install_generator.rb +19 -13
- data/{deprecated_generators → lib/generators}/graphiti/resource_generator.rb +68 -7
- data/{deprecated_generators → lib/generators}/graphiti/templates/controller.rb.erb +1 -1
- data/{deprecated_generators → lib/generators}/graphiti/templates/index_request_spec.rb.erb +1 -1
- data/{deprecated_generators → lib/generators}/graphiti/templates/resource_reads_spec.rb.erb +6 -6
- data/{deprecated_generators → lib/generators}/graphiti/templates/show_request_spec.rb.erb +1 -1
- data/lib/graphiti/configuration.rb +2 -2
- data/lib/graphiti/error_serializers/conflict_request.rb +19 -0
- data/lib/graphiti/error_serializers/deprecated_constants.rb +48 -0
- data/lib/graphiti/error_serializers/invalid_request.rb +56 -0
- data/lib/graphiti/error_serializers/validation.rb +143 -0
- data/lib/graphiti/errors.rb +4 -4
- data/lib/graphiti/query.rb +1 -1
- data/lib/graphiti/rails/context.rb +33 -0
- data/lib/graphiti/rails/controller.rb +41 -0
- data/lib/graphiti/rails/debugging.rb +18 -0
- data/lib/graphiti/rails/exception_handlers.rb +77 -0
- data/lib/graphiti/rails/railtie.rb +139 -0
- data/lib/graphiti/rails/rake_helpers.rb +42 -0
- data/lib/graphiti/rails/responders.rb +21 -0
- data/lib/graphiti/rails/test_helpers.rb +22 -0
- data/lib/graphiti/rails.rb +47 -29
- data/lib/graphiti/resource/configuration.rb +1 -0
- data/lib/graphiti/resource/interface.rb +2 -2
- data/lib/graphiti/resource/remote.rb +2 -2
- data/lib/graphiti/resource/sideloading.rb +1 -1
- data/lib/graphiti/resource.rb +13 -1
- data/lib/graphiti/responders.rb +7 -20
- data/lib/graphiti/schema.rb +5 -1
- data/lib/graphiti/schema_diff.rb +4 -0
- data/lib/graphiti/scope.rb +45 -37
- data/lib/graphiti/serializer.rb +6 -0
- data/lib/graphiti/sideload/belongs_to.rb +38 -5
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +27 -23
- data/lib/graphiti/sideload.rb +54 -35
- data/lib/graphiti/spec_helpers/errors.rb +73 -0
- data/lib/graphiti/spec_helpers/errors_proxy.rb +75 -0
- data/lib/graphiti/spec_helpers/helpers.rb +107 -0
- data/lib/graphiti/spec_helpers/matchers.rb +196 -0
- data/lib/graphiti/spec_helpers/node.rb +88 -0
- data/lib/graphiti/spec_helpers/rspec.rb +148 -0
- data/lib/graphiti/spec_helpers.rb +54 -0
- data/lib/graphiti/util/include_params.rb +2 -2
- data/lib/graphiti/util/serializer_relationships.rb +41 -5
- data/lib/graphiti/version.rb +1 -1
- data/lib/graphiti-rails.rb +11 -0
- data/lib/graphiti.rb +34 -10
- data/lib/graphiti_errors.rb +11 -0
- data/lib/graphiti_spec_helpers/rspec.rb +3 -0
- data/lib/graphiti_spec_helpers.rb +11 -0
- data/lib/tasks/graphiti.rake +29 -0
- data/package-lock.json +6199 -0
- data/package.json +5 -4
- data/website/.gitignore +20 -0
- data/website/README.md +43 -0
- data/website/docusaurus.config.js +197 -0
- data/website/package-lock.json +19474 -0
- data/website/package.json +46 -0
- data/website/sidebars.js +82 -0
- data/website/src/css/custom.css +85 -0
- data/website/src/pages/markdown-page.mdx +7 -0
- data/website/static/.nojekyll +0 -0
- data/website/static/1.13/2019/03/31/graphiti-1-0.html +205 -0
- data/website/static/1.13/2019/05/08/graphiti-1-1.html +212 -0
- data/website/static/1.13/2019/05/20/graphiti-1-2.html +214 -0
- data/website/static/1.13/2019/10/14/tutorial.html +198 -0
- data/website/static/1.13/CNAME +1 -0
- data/website/static/1.13/README.md +16 -0
- data/website/static/1.13/assets/css/syntax.css +60 -0
- data/website/static/1.13/assets/favicons/android-chrome-192x192.png +0 -0
- data/website/static/1.13/assets/favicons/android-chrome-256x256.png +0 -0
- data/website/static/1.13/assets/favicons/apple-touch-icon.png +0 -0
- data/website/static/1.13/assets/favicons/browserconfig.xml +9 -0
- data/website/static/1.13/assets/favicons/favicon-16x16.png +0 -0
- data/website/static/1.13/assets/favicons/favicon-32x32.png +0 -0
- data/website/static/1.13/assets/favicons/favicon.ico +0 -0
- data/website/static/1.13/assets/favicons/mstile-150x150.png +0 -0
- data/website/static/1.13/assets/favicons/safari-pinned-tab.svg +1 -0
- data/website/static/1.13/assets/favicons/site.webmanifest +19 -0
- data/website/static/1.13/assets/img/backend.gif +0 -0
- data/website/static/1.13/assets/img/conformity.png +0 -0
- data/website/static/1.13/assets/img/error_payload.png +0 -0
- data/website/static/1.13/assets/img/fancy-cushion.png +0 -0
- data/website/static/1.13/assets/img/gh.png +0 -0
- data/website/static/1.13/assets/img/home-bg.jpg +0 -0
- data/website/static/1.13/assets/img/lifecycle.gif +0 -0
- data/website/static/1.13/assets/img/logo-500.png +0 -0
- data/website/static/1.13/assets/img/logo.png +0 -0
- data/website/static/1.13/assets/img/love-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/meta_total_count.png +0 -0
- data/website/static/1.13/assets/img/persist.jpg +0 -0
- data/website/static/1.13/assets/img/resource.gif +0 -0
- data/website/static/1.13/assets/img/rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/rest1.gif +0 -0
- data/website/static/1.13/assets/img/rest2.gif +0 -0
- data/website/static/1.13/assets/img/rest3.gif +0 -0
- data/website/static/1.13/assets/img/rethink-rest-graffiti.jpg +0 -0
- data/website/static/1.13/assets/img/sunrise.png +0 -0
- data/website/static/1.13/assets/img/why.png +0 -0
- data/website/static/1.13/assets/js/highlight.pack.js +2 -0
- data/website/static/1.13/assets/main.css +15518 -0
- data/website/static/1.13/assets/main.css.map +1 -0
- data/website/static/1.13/bin/bundle +109 -0
- data/website/static/1.13/bin/jekyll +27 -0
- data/website/static/1.13/bin/kramdown +27 -0
- data/website/static/1.13/bin/listen +27 -0
- data/website/static/1.13/bin/rake +27 -0
- data/website/static/1.13/bin/rougify +27 -0
- data/website/static/1.13/bin/safe_yaml +27 -0
- data/website/static/1.13/bin/sass +27 -0
- data/website/static/1.13/bin/sass-convert +27 -0
- data/website/static/1.13/bin/scss +27 -0
- data/website/static/1.13/blog.html +259 -0
- data/website/static/1.13/cheatsheet.html +316 -0
- data/website/static/1.13/cookbooks/authorization.md +0 -0
- data/website/static/1.13/cookbooks/caching.md +0 -0
- data/website/static/1.13/cookbooks/customizing-sideloads.html +325 -0
- data/website/static/1.13/cookbooks/etags.md +0 -0
- data/website/static/1.13/cookbooks/hopping-relationships.html +324 -0
- data/website/static/1.13/cookbooks/json_attributes.md +0 -0
- data/website/static/1.13/cookbooks/openstruct-models.md +0 -0
- data/website/static/1.13/cookbooks/remote-resources.md +0 -0
- data/website/static/1.13/cookbooks/without-activerecord.html +510 -0
- data/website/static/1.13/features.html +249 -0
- data/website/static/1.13/feed.xml +106 -0
- data/website/static/1.13/guides/concepts/backends-and-models.html +467 -0
- data/website/static/1.13/guides/concepts/debugging.html +440 -0
- data/website/static/1.13/guides/concepts/endpoints.html +432 -0
- data/website/static/1.13/guides/concepts/error-handling.html +396 -0
- data/website/static/1.13/guides/concepts/links.html +501 -0
- data/website/static/1.13/guides/concepts/remote-resources.html +536 -0
- data/website/static/1.13/guides/concepts/resources.html +2176 -0
- data/website/static/1.13/guides/concepts/testing.html +1469 -0
- data/website/static/1.13/guides/getting-started/installation.html +420 -0
- data/website/static/1.13/guides/graphiti-rails-migration.html +242 -0
- data/website/static/1.13/guides/index.html +269 -0
- data/website/static/1.13/guides/overview.html +325 -0
- data/website/static/1.13/guides/upgrading-2-0.html +193 -0
- data/website/static/1.13/guides/upgrading.html +314 -0
- data/website/static/1.13/guides/vandal.html +282 -0
- data/website/static/1.13/guides/why.html +1121 -0
- data/website/static/1.13/index.html +72 -0
- data/website/static/1.13/js/authentication.html +295 -0
- data/website/static/1.13/js/ddau.html +238 -0
- data/website/static/1.13/js/extra-params.html +270 -0
- data/website/static/1.13/js/index.html +321 -0
- data/website/static/1.13/js/installation.html +637 -0
- data/website/static/1.13/js/introduction.html +257 -0
- data/website/static/1.13/js/middleware.html +318 -0
- data/website/static/1.13/js/reads/fieldsets.html +271 -0
- data/website/static/1.13/js/reads/filtering.html +289 -0
- data/website/static/1.13/js/reads/includes.html +260 -0
- data/website/static/1.13/js/reads/index.html +497 -0
- data/website/static/1.13/js/reads/nested-queries.html +353 -0
- data/website/static/1.13/js/reads/pagination.html +260 -0
- data/website/static/1.13/js/reads/sorting.html +265 -0
- data/website/static/1.13/js/reads/statistics.html +289 -0
- data/website/static/1.13/js/state-syncing.html +340 -0
- data/website/static/1.13/js/writes/deferred.html +296 -0
- data/website/static/1.13/js/writes/dirty-tracking.html +399 -0
- data/website/static/1.13/js/writes/index.html +391 -0
- data/website/static/1.13/js/writes/nested.html +330 -0
- data/website/static/1.13/js/writes/validations.html +272 -0
- data/website/static/1.13/quickstart.html +660 -0
- data/website/static/1.13/template +161 -0
- data/website/static/1.13/tutorial/index.html +250 -0
- data/website/static/1.13/tutorial/step_0.html +292 -0
- data/website/static/1.13/tutorial/step_1.html +517 -0
- data/website/static/1.13/tutorial/step_2.html +481 -0
- data/website/static/1.13/tutorial/step_3.html +323 -0
- data/website/static/1.13/tutorial/step_4.html +318 -0
- data/website/static/1.13/tutorial/step_5.html +265 -0
- data/website/static/1.13/tutorial/step_6.html +276 -0
- data/website/static/1.13/tutorial/step_7.html +390 -0
- data/website/static/1.13/tutorial/step_8.html +316 -0
- data/website/static/1.13/tutorial/step_9.html +365 -0
- data/website/static/CNAME +1 -0
- data/website/static/assets/img/error_payload.png +0 -0
- data/website/static/assets/img/legacy/legacy-0378a3bb39.png +0 -0
- data/website/static/assets/img/legacy/legacy-05bbd3e5fd.png +0 -0
- data/website/static/assets/img/legacy/legacy-07aa104495.png +0 -0
- data/website/static/assets/img/legacy/legacy-0c75a16b3a.gif +0 -0
- data/website/static/assets/img/legacy/legacy-3076df6209.png +0 -0
- data/website/static/assets/img/legacy/legacy-7f6889bc89.png +0 -0
- data/website/static/assets/img/legacy/legacy-a2cc4363c3.png +0 -0
- data/website/static/assets/img/legacy/legacy-f67cfa89ab.png +0 -0
- data/website/static/assets/img/meta_total_count.png +0 -0
- data/website/static/img/docusaurus-social-card.jpg +0 -0
- data/website/static/img/docusaurus.png +0 -0
- data/website/static/img/favicon.ico +0 -0
- data/website/static/img/logo.png +0 -0
- data/website/static/img/logo.svg +1 -0
- data/website/static/img/undraw_docusaurus_mountain.svg +171 -0
- data/website/static/img/undraw_docusaurus_react.svg +170 -0
- data/website/static/img/undraw_docusaurus_tree.svg +40 -0
- metadata +251 -54
- data/gemfiles/rails_6.gemfile +0 -18
- data/gemfiles/rails_6_graphiti_rails.gemfile +0 -19
- data/gemfiles/rails_7.gemfile +0 -18
- data/gemfiles/rails_7_1_graphiti_rails.gemfile +0 -19
- data/gemfiles/rails_7_graphiti_rails.gemfile +0 -19
- data/lib/graphiti/deprecated_tasks.rb +0 -49
- data/lib/graphiti/railtie.rb +0 -121
- /data/{deprecated_generators → lib/generators}/graphiti/resource_test_generator.rb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/application_resource.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/create_request_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/destroy_request_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/resource.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/resource_writes_spec.rb.erb +0 -0
- /data/{deprecated_generators → lib/generators}/graphiti/templates/update_request_spec.rb.erb +0 -0
|
@@ -108,40 +108,44 @@ class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
|
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def resolve(parents, query, graph_parent)
|
|
111
|
-
|
|
111
|
+
if Graphiti.config.concurrency
|
|
112
|
+
future_resolve(parents, query, graph_parent).value!
|
|
113
|
+
else
|
|
114
|
+
sync_resolve(parents, query, graph_parent)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
112
117
|
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
def future_resolve(parents, query, graph_parent)
|
|
119
|
+
promises = []
|
|
120
|
+
each_resolvable_group(parents, query) do |child, group, child_query|
|
|
121
|
+
promises << child.future_resolve(group, child_query, graph_parent)
|
|
122
|
+
end
|
|
123
|
+
Concurrent::Promises.zip(*promises)
|
|
124
|
+
end
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
err = ::Graphiti::Errors::PolymorphicSideloadChildNotFound
|
|
122
|
-
raise err.new(self, group_name)
|
|
123
|
-
end
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def sync_resolve(parents, query, graph_parent)
|
|
129
|
+
each_resolvable_group(parents, query) do |child, group, child_query|
|
|
130
|
+
child.resolve(group, child_query, graph_parent)
|
|
124
131
|
end
|
|
125
132
|
end
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
# Group parents by their polymorphic type and yield each group's child
|
|
135
|
+
# sideload alongside a query pruned to the sideloads that child supports.
|
|
136
|
+
def each_resolvable_group(parents, query)
|
|
137
|
+
parents.group_by(&grouper.field_name).each_pair do |group_name, group|
|
|
129
138
|
next if group_name.nil? || grouper.ignore?(group_name)
|
|
130
139
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
sideload.future_resolve(group, duped, graph_parent)
|
|
135
|
-
else
|
|
136
|
-
err = ::Graphiti::Errors::PolymorphicSideloadChildNotFound
|
|
137
|
-
raise err.new(self, group_name)
|
|
140
|
+
child = children.values.find { |candidate| candidate.group_name == group_name.to_sym }
|
|
141
|
+
unless child
|
|
142
|
+
raise ::Graphiti::Errors::PolymorphicSideloadChildNotFound.new(self, group_name)
|
|
138
143
|
end
|
|
144
|
+
|
|
145
|
+
yield child, group, remove_invalid_sideloads(child.resource, query)
|
|
139
146
|
end
|
|
140
|
-
Concurrent::Promises.zip(*promises)
|
|
141
147
|
end
|
|
142
148
|
|
|
143
|
-
private
|
|
144
|
-
|
|
145
149
|
# We may be requesting a relationship that some subclasses support,
|
|
146
150
|
# but not others. Remove anything we don't support.
|
|
147
151
|
# TODO: spec to ensure this dupe logic doesn't mutate the original
|
data/lib/graphiti/sideload.rb
CHANGED
|
@@ -122,8 +122,25 @@ module Graphiti
|
|
|
122
122
|
!!@polymorphic_as
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# False everywhere but a plain belongs_to - see
|
|
126
|
+
# Sideload::BelongsTo#linkage_from_foreign_key?.
|
|
127
|
+
def linkage_from_foreign_key?
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# nil at either of the first two levels means "not specified" rather
|
|
132
|
+
# than "false".
|
|
125
133
|
def always_include_resource_ids?
|
|
126
|
-
!!@always_include_resource_ids
|
|
134
|
+
return !!@always_include_resource_ids unless @always_include_resource_ids.nil?
|
|
135
|
+
|
|
136
|
+
configured = parent_resource_class&.always_include_resource_ids_by_default
|
|
137
|
+
return !!configured unless configured.nil?
|
|
138
|
+
|
|
139
|
+
default_include_resource_ids?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def default_include_resource_ids?
|
|
143
|
+
false
|
|
127
144
|
end
|
|
128
145
|
|
|
129
146
|
def link?
|
|
@@ -292,48 +309,19 @@ module Graphiti
|
|
|
292
309
|
children.replace(associated) if track_associated
|
|
293
310
|
end
|
|
294
311
|
|
|
295
|
-
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
296
|
-
# Mirrors #future_resolve but resolves inline via the synchronous
|
|
297
|
-
# Scope#resolve / #load paths (no promises). See Scope#sync_resolve_sideloads.
|
|
298
312
|
def resolve(parents, query, graph_parent)
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if single? && parents.length > 1
|
|
302
|
-
raise Errors::SingularSideload.new(self, parents.length)
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
if self.class.scope_proc
|
|
306
|
-
sideload_scope = fire_scope(parents)
|
|
307
|
-
sideload_scope = Scope.new sideload_scope,
|
|
308
|
-
resource,
|
|
309
|
-
query,
|
|
310
|
-
parent: graph_parent,
|
|
311
|
-
sideload: self,
|
|
312
|
-
sideload_parent_length: parents.length,
|
|
313
|
-
default_paginate: false
|
|
314
|
-
sideload_scope.resolve do |sideload_results|
|
|
315
|
-
fire_assign(parents, sideload_results)
|
|
316
|
-
end
|
|
313
|
+
if Graphiti.config.concurrency
|
|
314
|
+
future_resolve(parents, query, graph_parent).value!
|
|
317
315
|
else
|
|
318
|
-
|
|
316
|
+
sync_resolve(parents, query, graph_parent)
|
|
319
317
|
end
|
|
320
318
|
end
|
|
321
319
|
|
|
322
320
|
def future_resolve(parents, query, graph_parent)
|
|
323
|
-
|
|
324
|
-
raise Errors::SingularSideload.new(self, parents.length)
|
|
325
|
-
end
|
|
321
|
+
assert_singular!(parents)
|
|
326
322
|
|
|
327
323
|
if self.class.scope_proc
|
|
328
|
-
|
|
329
|
-
sideload_scope = Scope.new sideload_scope,
|
|
330
|
-
resource,
|
|
331
|
-
query,
|
|
332
|
-
parent: graph_parent,
|
|
333
|
-
sideload: self,
|
|
334
|
-
sideload_parent_length: parents.length,
|
|
335
|
-
default_paginate: false
|
|
336
|
-
sideload_scope.future_resolve do |sideload_results|
|
|
324
|
+
build_sideload_scope(parents, query, graph_parent).future_resolve do |sideload_results|
|
|
337
325
|
fire_assign(parents, sideload_results)
|
|
338
326
|
end
|
|
339
327
|
else
|
|
@@ -401,6 +389,37 @@ module Graphiti
|
|
|
401
389
|
|
|
402
390
|
private
|
|
403
391
|
|
|
392
|
+
# Synchronous counterpart to #future_resolve, used when concurrency is off.
|
|
393
|
+
# Resolves inline via the synchronous Scope#resolve / #load paths (no
|
|
394
|
+
# promises). See Scope#sync_resolve_sideloads.
|
|
395
|
+
def sync_resolve(parents, query, graph_parent)
|
|
396
|
+
assert_singular!(parents)
|
|
397
|
+
|
|
398
|
+
if self.class.scope_proc
|
|
399
|
+
build_sideload_scope(parents, query, graph_parent).resolve do |sideload_results|
|
|
400
|
+
fire_assign(parents, sideload_results)
|
|
401
|
+
end
|
|
402
|
+
else
|
|
403
|
+
load(parents, query, graph_parent)
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def assert_singular!(parents)
|
|
408
|
+
if single? && parents.length > 1
|
|
409
|
+
raise Errors::SingularSideload.new(self, parents.length)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def build_sideload_scope(parents, query, graph_parent)
|
|
414
|
+
Scope.new fire_scope(parents),
|
|
415
|
+
resource,
|
|
416
|
+
query,
|
|
417
|
+
parent: graph_parent,
|
|
418
|
+
sideload: self,
|
|
419
|
+
sideload_parent_length: parents.length,
|
|
420
|
+
default_paginate: false
|
|
421
|
+
end
|
|
422
|
+
|
|
404
423
|
def future_load(parents, query, graph_parent)
|
|
405
424
|
proxy = build_resource_proxy(parents, query, graph_parent)
|
|
406
425
|
proxy.respond_to?(:future_resolve_data) ? proxy.future_resolve_data : Concurrent::Promises.fulfilled_future(proxy)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module SpecHelpers
|
|
3
|
+
module Errors
|
|
4
|
+
class Base < StandardError; end
|
|
5
|
+
|
|
6
|
+
class LinksNotFound < Base
|
|
7
|
+
def initialize(name)
|
|
8
|
+
@name = name
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def message
|
|
12
|
+
"Relationship with name '#{@name}' has no links!"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class NoAttribute < Base
|
|
17
|
+
def initialize(name)
|
|
18
|
+
@name = name
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def message
|
|
22
|
+
"No attribute '#{@name}' in JSON response node!"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class SideloadNotFound < Base
|
|
27
|
+
def initialize(name)
|
|
28
|
+
@name = name
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def message
|
|
32
|
+
"Relationship with name '#{@name}' not found!"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class NoResponse < Base
|
|
37
|
+
def message
|
|
38
|
+
"Cannot parse response - missing #response.body!"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class NoData < Base
|
|
43
|
+
def initialize(payload)
|
|
44
|
+
@payload = payload
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def message
|
|
48
|
+
"Payload did not contain 'data'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class NoSideloads < Base
|
|
53
|
+
def initialize(payload)
|
|
54
|
+
@payload = payload
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def message
|
|
58
|
+
"Tried to find sideload, but the payload did not contain 'included'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class NoMeta < Base
|
|
63
|
+
def initialize(payload)
|
|
64
|
+
@payload = payload
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def message
|
|
68
|
+
"Tried to find metadata, but the payload did not contain 'meta'! Payload was:\n\n#{JSON.pretty_generate(@payload)}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module SpecHelpers
|
|
3
|
+
class ErrorsProxy
|
|
4
|
+
class Error
|
|
5
|
+
attr_reader :json
|
|
6
|
+
|
|
7
|
+
def initialize(json)
|
|
8
|
+
@json = json
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def attribute
|
|
12
|
+
@json[:meta][:attribute]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# TODO: move to top-level code in errorable
|
|
16
|
+
def code
|
|
17
|
+
@json[:meta][:code]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def message
|
|
21
|
+
@json[:meta][:message]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def title
|
|
25
|
+
@json[:title]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def detail
|
|
29
|
+
@json[:detail]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def status
|
|
33
|
+
@json[:status]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
include Enumerable
|
|
38
|
+
|
|
39
|
+
def initialize(array)
|
|
40
|
+
@errors = array.map { |e| Error.new(e) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def [](key)
|
|
44
|
+
@errors[key]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def each(&blk)
|
|
48
|
+
@errors.each(&blk)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def length
|
|
52
|
+
count
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_h
|
|
56
|
+
{}.tap do |hash|
|
|
57
|
+
@errors.each do |e|
|
|
58
|
+
hash[e.attribute] = e.message
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def method_missing(id, *args, &blk)
|
|
64
|
+
matches = @errors.select { |e| e.attribute.to_sym == id }
|
|
65
|
+
matches.length == 1 ? matches[0] : matches
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Any name is answered: an attribute with no matching error yields an empty
|
|
69
|
+
# array rather than raising NoMethodError.
|
|
70
|
+
def respond_to_missing?(id, include_private = false)
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module SpecHelpers
|
|
3
|
+
module Helpers
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
def json
|
|
7
|
+
if response&.body
|
|
8
|
+
JSON.parse(response.body).with_indifferent_access
|
|
9
|
+
else
|
|
10
|
+
raise Errors::NoResponse.new
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def jsonapi_data
|
|
15
|
+
@jsonapi_data ||= if _jsonapi_data.is_a?(Hash)
|
|
16
|
+
node(from: _jsonapi_data)
|
|
17
|
+
else
|
|
18
|
+
_jsonapi_data.map { |datum| node(from: datum) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def jsonapi_included(type = nil)
|
|
23
|
+
variable = :"@jsonapi_included#{type}"
|
|
24
|
+
memo = instance_variable_get(variable)
|
|
25
|
+
return memo if memo
|
|
26
|
+
|
|
27
|
+
nodes = _jsonapi_included.map { |i| node(from: i) }
|
|
28
|
+
if type
|
|
29
|
+
nodes.select! { |n| n.jsonapi_type == type.to_s }
|
|
30
|
+
end
|
|
31
|
+
instance_variable_set(variable, nodes)
|
|
32
|
+
nodes
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def jsonapi_errors
|
|
36
|
+
@jsonapi_errors ||= ErrorsProxy.new(json["errors"] || [])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def jsonapi_meta
|
|
40
|
+
@jsonapi_errors = json["meta"] || raise(Errors::NoMeta.new(json))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def jsonapi_headers
|
|
44
|
+
media_type = "application/vnd.api+json"
|
|
45
|
+
{
|
|
46
|
+
"CONTENT_TYPE" => media_type,
|
|
47
|
+
"HTTP_ACCEPT" => media_type
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def jsonapi_get(url, params: {}, headers: {})
|
|
52
|
+
get url_for(url), params: params, headers: jsonapi_headers.merge(headers)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def jsonapi_post(url, payload, headers: {})
|
|
56
|
+
post url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def jsonapi_put(url, payload, headers: {})
|
|
60
|
+
put url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def jsonapi_patch(url, payload, headers: {})
|
|
64
|
+
patch url_for(url), params: payload.to_json, headers: jsonapi_headers.merge(headers)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def jsonapi_delete(url, headers: {})
|
|
68
|
+
delete url_for(url), headers: jsonapi_headers.merge(headers)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def json_datetime(value)
|
|
72
|
+
Graphiti::Types[:datetime][:read][value].as_json
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def json_date(value)
|
|
76
|
+
Graphiti::Types[:date][:read][value].as_json
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @api private
|
|
80
|
+
def node(from: nil)
|
|
81
|
+
from = json if from.nil?
|
|
82
|
+
data = from.has_key?("data") ? from["data"] : from
|
|
83
|
+
hash = {}
|
|
84
|
+
hash["id"] = data["id"]
|
|
85
|
+
hash["jsonapi_type"] = data["type"]
|
|
86
|
+
hash.merge!(data["attributes"]) if data.has_key?("attributes")
|
|
87
|
+
Node.new(hash, data["relationships"], self)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# @api private
|
|
93
|
+
def _jsonapi_data
|
|
94
|
+
json["data"] || raise(Errors::NoData.new(json))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @api private
|
|
98
|
+
def _jsonapi_included
|
|
99
|
+
if json.has_key?("included")
|
|
100
|
+
json["included"]
|
|
101
|
+
else
|
|
102
|
+
raise Errors::NoSideloads.new(json)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
module Graphiti
|
|
2
|
+
module SpecHelpers
|
|
3
|
+
# Assertions against a resource's DSL. The schema specs cannot cover
|
|
4
|
+
# adapter-dependent options (primary key, foreign key, and so on) because
|
|
5
|
+
# the schema does not carry them; these read the resource config directly.
|
|
6
|
+
module Matchers
|
|
7
|
+
class BaseMatcher
|
|
8
|
+
GRAPHITI_OPTS = [].freeze
|
|
9
|
+
GRAPHITI_CONFIG_KEY = ""
|
|
10
|
+
EXPECTED_ACTION = ""
|
|
11
|
+
|
|
12
|
+
def description
|
|
13
|
+
"#{self.class::EXPECTED_ACTION} #{target}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def failure_message
|
|
17
|
+
"expected that #{resource.class} would #{self.class::EXPECTED_ACTION} #{target}\n#{@opt_failures.join("\n")}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def failure_message_when_negated
|
|
21
|
+
"expected that #{resource.class} would not #{self.class::EXPECTED_ACTION} #{target}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def opt_failure_message(opt, expected, actual)
|
|
25
|
+
"expected that #{opt} would be #{expected}, was #{actual}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def does_not_match?(resource)
|
|
29
|
+
!matches?(resource)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def matches?(resource)
|
|
33
|
+
@resource = resource
|
|
34
|
+
|
|
35
|
+
expected? && expected_opts?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def with_options(opts)
|
|
39
|
+
@opts = opts
|
|
40
|
+
self
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def actual_opts
|
|
46
|
+
self.class::GRAPHITI_OPTS & opts.keys
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def config
|
|
50
|
+
@config ||= resource.class.config[self.class::GRAPHITI_CONFIG_KEY][target]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def expected_opts?
|
|
54
|
+
return false unless config
|
|
55
|
+
|
|
56
|
+
actual_opts.map { |opt| assert_opt(opt) }.all?(true)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class RelationMatcher < BaseMatcher
|
|
61
|
+
GRAPHITI_OPTS = %i[primary_key foreign_key resource readable writable link single].freeze
|
|
62
|
+
GRAPHITI_CONFIG_KEY = :sideloads
|
|
63
|
+
EXPECTED_ACTION = ""
|
|
64
|
+
|
|
65
|
+
def initialize(target)
|
|
66
|
+
@target = target
|
|
67
|
+
@opts = {}
|
|
68
|
+
@opt_failures = []
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
attr_reader :target, :opts, :resource
|
|
74
|
+
|
|
75
|
+
def assert_opt(opt)
|
|
76
|
+
asserted_opt = opt == :resource ? :resource_class : opt
|
|
77
|
+
return true if config.send(asserted_opt) == opts[opt]
|
|
78
|
+
|
|
79
|
+
@opt_failures << opt_failure_message(opt, opts[opt], config.send(asserted_opt))
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def expected?
|
|
84
|
+
config && config.type == relation_name
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# BelongsToMatcher -> :belongs_to, matching Sideload#type.
|
|
88
|
+
def relation_name
|
|
89
|
+
self.class.name.demodulize.gsub("Matcher", "").underscore.to_sym
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class BelongsToMatcher < RelationMatcher
|
|
94
|
+
EXPECTED_ACTION = "belong to"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class HasManyMatcher < RelationMatcher
|
|
98
|
+
EXPECTED_ACTION = "have many"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class HasOneMatcher < RelationMatcher
|
|
102
|
+
EXPECTED_ACTION = "have one"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
class ResourceDSLMatcher < BaseMatcher
|
|
106
|
+
def initialize(target, type)
|
|
107
|
+
@target = target
|
|
108
|
+
@type = type
|
|
109
|
+
@opts = {}
|
|
110
|
+
@opt_failures = []
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
attr_reader :target, :type, :opts, :resource
|
|
116
|
+
|
|
117
|
+
def expected?
|
|
118
|
+
config && assert_type
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def assert_type
|
|
122
|
+
return true if config[:type] == type
|
|
123
|
+
|
|
124
|
+
@opt_failures << opt_failure_message("type", type, config[:type])
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def assert_opt(opt)
|
|
129
|
+
return true if config[opt] == opts[opt]
|
|
130
|
+
|
|
131
|
+
@opt_failures << opt_failure_message(opt, opts[opt], config[opt])
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class ExposeAttributeMatcher < ResourceDSLMatcher
|
|
137
|
+
GRAPHITI_OPTS = %i[writable readable sortable filterable].freeze
|
|
138
|
+
GRAPHITI_CONFIG_KEY = :attributes
|
|
139
|
+
EXPECTED_ACTION = "expose"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
class FilterAttributeMatcher < ResourceDSLMatcher
|
|
143
|
+
GRAPHITI_OPTS = %i[allow deny single required allow_nil deny_empty].freeze
|
|
144
|
+
GRAPHITI_CONFIG_KEY = :filters
|
|
145
|
+
EXPECTED_ACTION = "filter"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @param [Symbol] relation
|
|
149
|
+
#
|
|
150
|
+
# @example expect(subject).to belong_to_resource(:user)
|
|
151
|
+
# @example expect(subject).to belong_to_resource(:user).with_options(foreign_key: :user_id, resource: UserResource)
|
|
152
|
+
# @example expect(subject).not_to belong_to_resource(:user)
|
|
153
|
+
def belong_to_resource(relation)
|
|
154
|
+
BelongsToMatcher.new(relation)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# @param [Symbol] relation
|
|
158
|
+
#
|
|
159
|
+
# @example expect(subject).to have_many_resources(:posts)
|
|
160
|
+
# @example expect(subject).to have_many_resources(:posts).with_options(foreign_key: :post_id, resource: PostResource)
|
|
161
|
+
# @example expect(subject).not_to have_many_resources(:posts)
|
|
162
|
+
def have_many_resources(relation)
|
|
163
|
+
HasManyMatcher.new(relation)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @param [Symbol] relation
|
|
167
|
+
#
|
|
168
|
+
# @example expect(subject).to have_one_resource(:post)
|
|
169
|
+
# @example expect(subject).to have_one_resource(:post).with_options(foreign_key: :post_id, resource: PostResource)
|
|
170
|
+
# @example expect(subject).not_to have_one_resource(:post)
|
|
171
|
+
def have_one_resource(relation)
|
|
172
|
+
HasOneMatcher.new(relation)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @param [Symbol] attribute
|
|
176
|
+
# @param [Symbol] type
|
|
177
|
+
#
|
|
178
|
+
# @example expect(subject).to expose_attribute(:name, :string)
|
|
179
|
+
# @example expect(subject).to expose_attribute(:name, :string).with_options(writable: false)
|
|
180
|
+
# @example expect(subject).not_to expose_attribute(:name, :string)
|
|
181
|
+
def expose_attribute(attribute, type)
|
|
182
|
+
ExposeAttributeMatcher.new(attribute, type)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# @param [Symbol] attribute
|
|
186
|
+
# @param [Symbol] type
|
|
187
|
+
#
|
|
188
|
+
# @example expect(subject).to filter_attribute(:name, :string)
|
|
189
|
+
# @example expect(subject).to filter_attribute(:name, :string).with_options(allow_nil: false)
|
|
190
|
+
# @example expect(subject).not_to filter_attribute(:name, :string)
|
|
191
|
+
def filter_attribute(attribute, type)
|
|
192
|
+
FilterAttributeMatcher.new(attribute, type)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|