graphiti 2.0.0.beta.2 → 2.0.0.beta.4
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 +60 -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 +41 -0
- data/README.md +13 -2
- data/UPGRADING.md +2 -68
- 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 +265 -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 -5
- 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 +43 -6
- 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 -23
- 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/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/persistence.rb +14 -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/node.rb +88 -0
- data/lib/graphiti/spec_helpers/rspec.rb +147 -0
- data/lib/graphiti/spec_helpers.rb +53 -0
- data/lib/graphiti/util/include_params.rb +2 -2
- data/lib/graphiti/util/persistence.rb +10 -11
- 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/{graphiti/deprecated_tasks.rb → tasks/graphiti.rake} +6 -1
- 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 +141 -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 +58 -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/gh.png +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/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/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 +245 -46
- 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/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/controller.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
data/lib/graphiti.rb
CHANGED
|
@@ -3,6 +3,12 @@ require "forwardable"
|
|
|
3
3
|
require "uri"
|
|
4
4
|
require "ostruct" unless defined?(::OpenStruct)
|
|
5
5
|
|
|
6
|
+
# The umbrella require, not just the core_ext files below: Graphiti.broadcast
|
|
7
|
+
# calls ActiveSupport::Notifications.instrument, which reaches for
|
|
8
|
+
# ActiveSupport::IsolatedExecutionState. That constant is autoloaded here and
|
|
9
|
+
# nowhere else, so without this a non-Rails app raises NameError on the first
|
|
10
|
+
# render unless something else happened to load ActiveSupport for us.
|
|
11
|
+
require "active_support"
|
|
6
12
|
require "active_support/version"
|
|
7
13
|
require "active_support/deprecation"
|
|
8
14
|
require "active_support/deprecator" if ::ActiveSupport.version >= Gem::Version.new("7.1")
|
|
@@ -14,12 +20,34 @@ require "active_support/concern"
|
|
|
14
20
|
require "active_support/time"
|
|
15
21
|
|
|
16
22
|
require "dry-types"
|
|
17
|
-
require "graphiti_errors"
|
|
18
23
|
|
|
19
24
|
require "jsonapi/serializable"
|
|
20
25
|
|
|
26
|
+
# These gems are merged into graphiti as of 2.0, but their 1.x/0.x releases
|
|
27
|
+
# still resolve against graphiti 2.x. Leaving one installed is never harmless:
|
|
28
|
+
# graphiti_spec_helpers and graphiti-rails ship files that collide with ours
|
|
29
|
+
# (lib/graphiti_spec_helpers.rb, lib/graphiti/rails.rb), so which copy a require
|
|
30
|
+
# picks up comes down to load path order, and graphiti_errors installs a second,
|
|
31
|
+
# competing exception handler on any controller that includes it. Fail loudly
|
|
32
|
+
# rather than let either happen quietly. Checked here because graphiti is loaded
|
|
33
|
+
# either way, whichever copy wins.
|
|
34
|
+
{
|
|
35
|
+
"graphiti_spec_helpers" => 'The "graphiti_spec_helpers/rspec" require and the GraphitiSpecHelpers namespace are unchanged.',
|
|
36
|
+
"graphiti-rails" => 'Graphiti::Rails and its config.graphiti options are unchanged. Drop the "graphiti-rails" require if you have one.',
|
|
37
|
+
"graphiti_errors" => "Exception handling now goes through rescue_registry. Remove `include GraphitiErrors` from your controllers — Graphiti registers its own handlers, and you can add yours with `register_exception`."
|
|
38
|
+
}.each do |gem_name, guidance|
|
|
39
|
+
next unless Gem.loaded_specs.key?(gem_name)
|
|
40
|
+
|
|
41
|
+
raise <<~MSG
|
|
42
|
+
#{gem_name} is merged into graphiti as of 2.0 and is no longer published
|
|
43
|
+
separately. Remove it from your Gemfile.
|
|
44
|
+
|
|
45
|
+
#{guidance}
|
|
46
|
+
MSG
|
|
47
|
+
end
|
|
48
|
+
|
|
21
49
|
module Graphiti
|
|
22
|
-
DEPRECATOR = ActiveSupport::Deprecation.new("
|
|
50
|
+
DEPRECATOR = ActiveSupport::Deprecation.new("3.0", "Graphiti")
|
|
23
51
|
|
|
24
52
|
# @api private
|
|
25
53
|
def self.context
|
|
@@ -203,6 +231,10 @@ require "graphiti/query"
|
|
|
203
231
|
require "graphiti/debugger"
|
|
204
232
|
require "graphiti/util/cache_debug"
|
|
205
233
|
require "graphiti/util/uri_decoder"
|
|
234
|
+
require "graphiti/error_serializers/validation"
|
|
235
|
+
require "graphiti/error_serializers/invalid_request"
|
|
236
|
+
require "graphiti/error_serializers/conflict_request"
|
|
237
|
+
require "graphiti/error_serializers/deprecated_constants"
|
|
206
238
|
|
|
207
239
|
if defined?(ActiveRecord)
|
|
208
240
|
require "graphiti/adapters/active_record"
|
|
@@ -210,14 +242,6 @@ end
|
|
|
210
242
|
|
|
211
243
|
if defined?(Rails)
|
|
212
244
|
require "graphiti/rails"
|
|
213
|
-
require "graphiti/responders"
|
|
214
|
-
|
|
215
|
-
# graphiti-rails has own Railtie
|
|
216
|
-
begin
|
|
217
|
-
require "graphiti-rails"
|
|
218
|
-
rescue LoadError
|
|
219
|
-
require "graphiti/railtie"
|
|
220
|
-
end
|
|
221
245
|
end
|
|
222
246
|
|
|
223
247
|
require "graphiti/runner"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Deprecated entry point. graphiti_errors was absorbed into graphiti in 2.0 and
|
|
2
|
+
# graphiti required it for you in 1.x, so an explicit require left in an
|
|
3
|
+
# initializer or spec_helper would otherwise raise LoadError. Only the
|
|
4
|
+
# serializers survived. See graphiti/error_serializers/deprecated_constants.rb.
|
|
5
|
+
# Remove in 3.0.
|
|
6
|
+
require "graphiti"
|
|
7
|
+
|
|
8
|
+
Graphiti::DEPRECATOR.warn(
|
|
9
|
+
'require "graphiti_errors" is no longer needed. graphiti_errors is part of graphiti as of 2.0, ' \
|
|
10
|
+
"and its exception handling is replaced by rescue_registry."
|
|
11
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Deprecated entry point. The spec helpers moved under Graphiti::SpecHelpers
|
|
2
|
+
# when the graphiti_spec_helpers gem was absorbed into graphiti in 2.0. This
|
|
3
|
+
# keeps `require "graphiti_spec_helpers"` and the old constant working.
|
|
4
|
+
# Remove in 3.0.
|
|
5
|
+
require "graphiti/spec_helpers"
|
|
6
|
+
|
|
7
|
+
GraphitiSpecHelpers = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
|
|
8
|
+
"GraphitiSpecHelpers",
|
|
9
|
+
"Graphiti::SpecHelpers",
|
|
10
|
+
Graphiti::DEPRECATOR
|
|
11
|
+
)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
namespace :graphiti do
|
|
2
|
+
include Graphiti::Rails::TestHelpers
|
|
3
|
+
|
|
2
4
|
def session
|
|
3
5
|
@session ||= ActionDispatch::Integration::Session.new(Rails.application)
|
|
4
6
|
end
|
|
@@ -19,7 +21,10 @@ namespace :graphiti do
|
|
|
19
21
|
"?cache=bust"
|
|
20
22
|
end
|
|
21
23
|
path = "#{path}&debug=true" if debug
|
|
22
|
-
|
|
24
|
+
handle_request_exceptions do
|
|
25
|
+
headers = {Authorization: ENV["AUTHORIZATION_HEADER"]}.compact
|
|
26
|
+
session.get(path.to_s, headers: headers)
|
|
27
|
+
end
|
|
23
28
|
JSON.parse(session.response.body)
|
|
24
29
|
end
|
|
25
30
|
|