diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed6bf837..f17af7ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,19 +15,19 @@ jobs: runs-on: ubuntu-latest steps: - - name: "Install Dependent libraries" - run: | - sudo apt-get update - sudo apt-get -yqq install libpq-dev libvips libvips-dev + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: libpq-dev libvips libvips-dev + version: 1.0 - name: "Checkout code" uses: actions/checkout@v1 - - name: "Install Ruby 3.4.4" + - name: "Install Ruby" uses: ruby/setup-ruby@v1 with: bundler-cache: true - ruby-version: 3.4.4 + ruby-version: 3.4.7 - name: "Run Rubocop" env: @@ -61,19 +61,19 @@ jobs: options: --entrypoint redis-server steps: - - name: "Install Dependent libraries" - run: | - sudo apt-get update - sudo apt-get -yqq install libpq-dev libvips libvips-dev + - uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: libpq-dev libvips libvips-dev + version: 1.0 - name: "Checkout code" uses: actions/checkout@v1 - - name: "Install Ruby 3.4.4" + - name: "Install Ruby" uses: ruby/setup-ruby@v1 with: bundler-cache: true - ruby-version: 3.4.4 + ruby-version: 3.4.7 - name: "Run API specs" env: diff --git a/.rubocop.yml b/.rubocop.yml index a1ba7b1b..f008986c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -446,6 +446,11 @@ Rails/EnvLocal: Rails/ExpandedDateRange: Enabled: true +Rails/Exit: + Exclude: + - spec/spec_helper.rb + - spec/rails_helper.rb + Rails/FilePath: Enabled: false @@ -489,6 +494,12 @@ Rails/MultipleRoutePaths: Rails/NegateInclude: Enabled: false +Rails/Output: + Exclude: + - "lib/tasks/**/*.rake" + - spec/rails_helper.rb + - spec/spec_helper.rb + # That's what html_safe is for. Rails/OutputSafety: Enabled: false diff --git a/.ruby-version b/.ruby-version index f9892605..2aa51319 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.4.4 +3.4.7 diff --git a/Dockerfile b/Dockerfile index fde93015..de1496cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN --mount=type=secret,id=maxmind_account_id \ GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/maxmind_license_key \ /usr/bin/geoipupdate -FROM ruby:3.4.4-bookworm +FROM ruby:3.4.7-bookworm ENV DEBIAN_FRONTEND=noninteractive @@ -79,4 +79,4 @@ COPY --from=maxmind /usr/share/GeoIP /usr/share/GeoIP EXPOSE 8080 -CMD ["bin/puma", "-C", "config/puma.rb"] +CMD ["bin/pitchfork", "-c", "config/pitchfork.rb"] diff --git a/Gemfile b/Gemfile index 2fe28bfa..ca1bdb19 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" -ruby "3.4.4" +ruby "3.4.7" # stdlib gem "csv", "~> 3.3.5" @@ -10,19 +10,20 @@ gem "fiddle", "~> 1.1.8" gem "ostruct", "~> 0.6.2", require: false gem "set", "~> 1.1.2" gem "sorted_set", "~> 1.0.3" -gem "stringio", "~> 3.1.7" +gem "stringio", "~> 3.1.8" gem "syslog", "~> 0.3.0", require: false gem "activesupport", "~> 7.2" gem "activerecord", "~> 7.2" # Rails / database -gem "rails", "7.2.2.2" +gem "rails", "7.2.3" gem "pg", "~> 1.6.2" gem "activerecord-cte", "~> 0.4.0" gem "active_record_distinct_on", "1.9.0" gem "after_commit_everywhere", "~> 1.6.0" -gem "closure_tree", "~> 9.1.1" +gem "async", "~> 2.34.0" +gem "closure_tree", "~> 9.2.0" gem "frozen_record", "~> 0.27.1" gem "pghero", "~> 3.7.0" gem "pg_query", "~> 6.1.0" @@ -32,16 +33,16 @@ gem "scenic", "~> 1.9.0" gem "store_model", "~> 4.3.0" # Redis / Jobs -gem "good_job", "~> 4.12.0" +gem "good_job", "~> 4.12.1" gem "redis", "~> 5.4.1" gem "redis-objects", ">= 2.0.0.beta" gem "job-iteration", "~> 1.11.0" # GraphQL -gem "graphql", "2.5.11" -gem "graphql-batch", "~> 0.6.0" +gem "graphql", "2.5.14" +gem "graphql-batch", "~> 0.6.1" gem "graphql-client", "~> 0.26.0" -gem "graphql-fragment_cache", "~> 1.22.0" +gem "graphql-fragment_cache", "~> 1.22.2" gem "search_object_graphql", "~> 1.0.5", require: %w[search_object search_object/plugin/graphql] # dry-rb @@ -63,7 +64,7 @@ gem "dry-validation", "~> 1.11.1" # Keycloak / Auth gem "bcrypt", "~> 3.1.20" -gem "keycloak-admin", "~> 1.1.3" +gem "keycloak-admin", "~> 1.1.4" gem "keycloak_rack", "1.2.0" # Metadata Parsing @@ -74,52 +75,49 @@ gem "niso-jats", github: "scryptmouse/niso-jats", require: false # Misc gem "absolute_time", "~> 1.0.0" -gem "acts_as_list", "~> 1.2.4" +gem "acts_as_list", "~> 1.2.6" gem "addressable", ">= 2.8.0" -gem "ahoy_matey", "~> 5.4.0" +gem "ahoy_matey", "~> 5.4.1" gem "anystyle", "~> 1.6.0" gem "anyway_config", "~> 2.7.2" -gem "autotuner", "~> 1.0.2" +gem "autotuner", "~> 1.1.0" gem "down", "~> 5.4.2" -gem "faraday", "~> 2.13.4" -gem "faraday-follow_redirects", "~> 0.3.0" +gem "faraday", "~> 2.14.0" +gem "faraday-follow_redirects", "~> 0.4.0" gem "faraday-retry", "~> 2.3.2" gem "ffi", "~> 1.17.2" -gem "fugit", "~> 1.11.1" +gem "fugit", "~> 1.12.1" gem "geocoder", "~> 1.8.0" gem "groupdate", "~> 6.7.0" gem "hashids", "~> 1.0.6" gem "htmlbeautifier", "~> 1.4.3" gem "iso-639", "~> 0.3.8" -gem "jbuilder", "~> 2.13.0" +gem "jbuilder", "~> 2.14.1" gem "json-ld", "~> 3.3.2" -gem "json-schema", "~> 2.8.1" -gem "json_schemer", "~> 0.2.18" +gem "json_schemer", "~> 2.4.0" gem "jwt", "~> 2.8.2" gem "kramdown", "~> 2.5.1" gem "link-header-parser", "~> 7.0.1" -gem "liquid", "~> 5.8.1" +gem "liquid", "~> 5.10.0" gem "maxminddb", "~> 0.1.22" gem "namae", "~> 1.2.0" gem "naught", "~> 1.1.0" gem "nokogiri", "~> 1.18.9" gem "oai", "~> 1.3.0" -gem "oj", "3.16.11" +gem "oj", "3.16.12" gem "openid_connect", "~> 2.3.0" gem "ox", "~> 2.14.18" gem "pundit", "~> 2.5.0" gem "redcarpet", "~> 3.6.0" gem "reverse_markdown", "~> 3.0.0" gem "ruby-limiter", "~> 2.3.0" -gem "rufus-scheduler", "~> 3.9.2" gem "sanitize", "~> 7.0.0" gem "semantic", "~> 1.6.1" gem "shale", "~> 1.2.1" -gem "sinatra", "~> 4.1.1", require: "sinatra/base" -gem "sinatra-contrib", "~> 4.1.1", require: false +gem "sinatra", "~> 4.2.1", require: "sinatra/base" +gem "sinatra-contrib", "~> 4.2.1", require: false gem "statesman", "~> 12.1.0" gem "strip_attributes", "~> 2.0.0" -gem "sucker_punch", "~> 3.2.0" gem "tomlib", "~> 0.7.2" gem "validate_url", "~> 1.0.15" gem "with_advisory_lock", "~> 7.0.2" @@ -128,31 +126,31 @@ gem "with_advisory_lock", "~> 7.0.2" gem "aws-sdk-s3", "~> 1.196.1" gem "content_disposition", "~> 1.0.0" gem "image_processing", "~> 1.14.0" -gem "marcel", "~> 1.0.4" -gem "shrine", "~> 3.6.0" +gem "marcel", "~> 1.1.0" +gem "shrine", "~> 3.6.0", require: %w[shrine shrine/storage/s3 shrine/storage/memory shrine/storage/url] gem "shrine-tus", "~> 2.1.1" gem "shrine-url", "~> 2.4.1" gem "mediainfo", "~> 1.5.0" -gem "tus-server", "~> 2.3.0" +gem "tus-server", "~> 2.3.0", require: %w[tus/server tus/storage/s3] gem "zaru", "~> 1.0.0" # Servers / Rack -gem "puma", "~> 6.6.0" -gem "puma-rufus-scheduler", "~> 0.1.0" -gem "puma_worker_killer", "~> 1.0.0", require: false -gem "rack", "~> 3.1.16" +gem "pitchfork", "~> 0.18.1" +gem "rack", "~> 3.2.4" gem "rack-cors", "~> 3.0.0" # Debugging / system-level things -gem "bootsnap", ">= 1.18.6", require: false +gem "bootsnap", ">= 1.19.0", require: false gem "pry-rails", "~> 0.3.11" gem "pry", "~> 0.15.2" +gem "ruby-prof", "~> 1.7.2", require: false +gem "stackprof", "~> 0.2.25", require: false group :development, :test do gem "factory_bot_rails", "~> 6.5.0" gem "faker", "~> 3.5.2" - gem "rspec", "~> 3.13.1" - gem "rspec-rails", "~> 8.0.1" + gem "rspec", "~> 3.13.2" + gem "rspec-rails", "~> 8.0.2" gem "yard", "~> 0.9.34" gem "yard-activerecord", "~> 0.0.16" gem "yard-activesupport-concern", "~> 0.0.1" @@ -165,8 +163,6 @@ group :development do gem "rubocop-rails", "2.32.0", require: false gem "rubocop-rspec", "3.6.0", require: false gem "rubocop-rspec_rails", "2.31.0", require: false - gem "ruby-prof", "~> 1.7.2", require: false - gem "stackprof", "~> 0.2.25", require: false end group :test do @@ -178,5 +174,5 @@ group :test do gem "simplecov", "~> 0.22.0", require: false gem "test-prof", "~> 1.4.4" gem "timecop", "~> 0.9.8" - gem "webmock", "3.25.1" + gem "webmock", "3.26.1" end diff --git a/Gemfile.lock b/Gemfile.lock index 774647c2..1a4b1f90 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,70 +12,72 @@ GEM remote: https://rubygems.org/ specs: absolute_time (1.0.0) - actioncable (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + actioncable (7.2.3) + actionpack (= 7.2.3) + activesupport (= 7.2.3) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailbox (7.2.3) + actionpack (= 7.2.3) + activejob (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) mail (>= 2.8.0) - actionmailer (7.2.2.2) - actionpack (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionmailer (7.2.3) + actionpack (= 7.2.3) + actionview (= 7.2.3) + activejob (= 7.2.3) + activesupport (= 7.2.3) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (7.2.2.2) - actionview (= 7.2.2.2) - activesupport (= 7.2.2.2) + actionpack (7.2.3) + actionview (= 7.2.3) + activesupport (= 7.2.3) + cgi nokogiri (>= 1.8.5) racc - rack (>= 2.2.4, < 3.2) + rack (>= 2.2.4, < 3.3) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (7.2.2.2) - actionpack (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + actiontext (7.2.3) + actionpack (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.2.2.2) - activesupport (= 7.2.2.2) + actionview (7.2.3) + activesupport (= 7.2.3) builder (~> 3.1) + cgi erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) active_record_distinct_on (1.9.0) activerecord (>= 6.1) - activejob (7.2.2.2) - activesupport (= 7.2.2.2) + activejob (7.2.3) + activesupport (= 7.2.3) globalid (>= 0.3.6) - activemodel (7.2.2.2) - activesupport (= 7.2.2.2) - activerecord (7.2.2.2) - activemodel (= 7.2.2.2) - activesupport (= 7.2.2.2) + activemodel (7.2.3) + activesupport (= 7.2.3) + activerecord (7.2.3) + activemodel (= 7.2.3) + activesupport (= 7.2.3) timeout (>= 0.4.0) activerecord-cte (0.4.0) activerecord - activestorage (7.2.2.2) - actionpack (= 7.2.2.2) - activejob (= 7.2.2.2) - activerecord (= 7.2.2.2) - activesupport (= 7.2.2.2) + activestorage (7.2.3) + actionpack (= 7.2.3) + activejob (= 7.2.3) + activerecord (= 7.2.3) + activesupport (= 7.2.3) marcel (~> 1.0) - activesupport (7.2.2.2) + activesupport (7.2.3) base64 benchmark (>= 0.3) bigdecimal @@ -87,7 +89,7 @@ GEM minitest (>= 5.1) securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) - acts_as_list (1.2.4) + acts_as_list (1.2.6) activerecord (>= 6.1) activesupport (>= 6.1) addressable (2.8.7) @@ -96,7 +98,7 @@ GEM after_commit_everywhere (1.6.0) activerecord (>= 4.2) activesupport - ahoy_matey (5.4.0) + ahoy_matey (5.4.1) activesupport (>= 7.1) device_detector (>= 1) safely_block (>= 0.4) @@ -109,8 +111,14 @@ GEM anyway_config (2.7.2) ruby-next-core (~> 1.0) ast (2.4.3) + async (2.34.0) + console (~> 1.29) + fiber-annotation + io-event (~> 1.11) + metrics (~> 0.12) + traces (~> 0.18) attr_required (1.0.2) - autotuner (1.0.2) + autotuner (1.1.0) aws-eventstream (1.4.0) aws-partitions (1.1143.0) aws-sdk-core (3.229.0) @@ -133,25 +141,30 @@ GEM base64 (0.3.0) bcp47_spec (0.2.1) bcrypt (3.1.20) - benchmark (0.4.1) + benchmark (0.5.0) bibtex-ruby (6.2.0) latex-decode (~> 0.0) logger (~> 1.7) racc (~> 1.7) - bigdecimal (3.3.0) + bigdecimal (3.3.1) bindata (2.5.1) - bootsnap (1.18.6) + bootsnap (1.19.0) msgpack (~> 1.2) builder (3.3.0) - closure_tree (9.1.1) + cgi (0.5.0) + closure_tree (9.2.0) activerecord (>= 7.2.0) with_advisory_lock (>= 7.0.0) zeitwerk (~> 2.7) coderay (1.1.3) concurrent-ruby (1.3.5) connection_pool (2.5.4) + console (1.34.2) + fiber-annotation + fiber-local (~> 1.1) + json content_disposition (1.0.0) - crack (1.0.0) + crack (1.0.1) bigdecimal rexml crass (1.0.6) @@ -163,7 +176,7 @@ GEM database_cleaner-redis (2.0.0) database_cleaner-core (~> 2.0.0) redis - date (3.4.1) + date (3.5.0) device_detector (1.1.3) diff-lcs (1.6.2) docile (1.4.1) @@ -240,31 +253,29 @@ GEM dry-initializer (~> 3.2) dry-schema (~> 1.14) zeitwerk (~> 2.6) - ecma-re-validator (0.4.0) - regexp_parser (~> 2.2) edtf (3.2.0) activesupport (>= 3.0, < 9.0) email_validator (2.2.4) activemodel - erb (5.0.3) + erb (6.0.0) erubi (1.13.1) et-orbi (1.4.0) tzinfo - factory_bot (6.5.4) + factory_bot (6.5.6) activesupport (>= 6.1.0) - factory_bot_rails (6.5.0) + factory_bot_rails (6.5.1) factory_bot (~> 6.5) railties (>= 6.1.0) faker (3.5.2) i18n (>= 1.8.11, < 2) - faraday (2.13.4) + faraday (2.14.0) faraday-net_http (>= 2.0, < 3.5) json logger - faraday-follow_redirects (0.3.0) + faraday-follow_redirects (0.4.0) faraday (>= 1, < 3) - faraday-net_http (3.4.1) - net-http (>= 0.5.0) + faraday-net_http (3.4.2) + net-http (~> 0.5) faraday-retry (2.3.2) faraday (~> 2.0) ffi (1.17.2) @@ -280,58 +291,58 @@ GEM ffi-compiler (1.3.2) ffi (>= 1.15.5) rake + fiber-annotation (0.2.0) + fiber-local (1.1.0) + fiber-storage fiber-storage (1.0.1) fiddle (1.1.8) frozen_record (0.27.4) activemodel - fugit (1.11.1) - et-orbi (~> 1, >= 1.2.11) + fugit (1.12.1) + et-orbi (~> 1.4) raabro (~> 1.4) - geocoder (1.8.5) + geocoder (1.8.6) base64 (>= 0.1.0) csv (>= 3.0.0) - get_process_mem (1.0.0) - bigdecimal (>= 2.0) - ffi (~> 1.0) - globalid (1.2.1) + globalid (1.3.0) activesupport (>= 6.1) - good_job (4.12.0) + good_job (4.12.1) activejob (>= 6.1.0) activerecord (>= 6.1.0) concurrent-ruby (>= 1.3.1) fugit (>= 1.11.0) railties (>= 6.1.0) thor (>= 1.0.0) - google-protobuf (4.31.1) + google-protobuf (4.33.1) bigdecimal rake (>= 13) - google-protobuf (4.31.1-aarch64-linux-gnu) + google-protobuf (4.33.1-aarch64-linux-gnu) bigdecimal rake (>= 13) - google-protobuf (4.31.1-aarch64-linux-musl) + google-protobuf (4.33.1-aarch64-linux-musl) bigdecimal rake (>= 13) - google-protobuf (4.31.1-arm64-darwin) + google-protobuf (4.33.1-arm64-darwin) bigdecimal rake (>= 13) - google-protobuf (4.31.1-x86-linux-gnu) + google-protobuf (4.33.1-x86-linux-gnu) bigdecimal rake (>= 13) - google-protobuf (4.31.1-x86_64-darwin) + google-protobuf (4.33.1-x86_64-darwin) bigdecimal rake (>= 13) - google-protobuf (4.31.1-x86_64-linux-gnu) + google-protobuf (4.33.1-x86_64-linux-gnu) bigdecimal rake (>= 13) - google-protobuf (4.31.1-x86_64-linux-musl) + google-protobuf (4.33.1-x86_64-linux-musl) bigdecimal rake (>= 13) - graphql (2.5.11) + graphql (2.5.14) base64 fiber-storage logger - graphql-batch (0.6.0) - graphql (>= 1.12.18, < 3) + graphql-batch (0.6.1) + graphql (>= 1.13, < 3) promise.rb (~> 0.7.2) graphql-client (0.26.0) activesupport (>= 3.0) @@ -344,7 +355,7 @@ GEM hashdiff (1.2.1) hashids (1.0.6) htmlbeautifier (1.4.3) - htmlentities (4.3.4) + htmlentities (4.4.0) http (5.3.1) addressable (~> 2.8) http-cookie (~> 1.0) @@ -361,21 +372,22 @@ GEM mini_magick (>= 4.9.5, < 6) ruby-vips (>= 2.0.17, < 3) io-console (0.8.1) - irb (1.15.2) + io-event (1.14.2) + irb (1.15.3) pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) iso-639 (0.3.8) csv - jbuilder (2.13.0) - actionview (>= 5.0.0) - activesupport (>= 5.0.0) + jbuilder (2.14.1) + actionview (>= 7.0.0) + activesupport (>= 7.0.0) jmespath (1.6.2) job-iteration (1.11.0) activejob (>= 6.1) - json (2.13.2) + json (2.16.0) json-canonicalization (1.0.0) - json-jwt (1.16.7) + json-jwt (1.17.0) activesupport (>= 4.2) aes_key_wrap base64 @@ -390,17 +402,14 @@ GEM rack (>= 2.2, < 4) rdf (~> 3.3) rexml (~> 3.2) - json-schema (2.8.1) - addressable (>= 2.4) - json_schemer (0.2.25) - ecma-re-validator (~> 0.3) + json_schemer (2.4.0) + bigdecimal hana (~> 1.3) regexp_parser (~> 2.0) simpleidn (~> 0.2) - uri_template (~> 0.7) jwt (2.8.2) base64 - keycloak-admin (1.1.3) + keycloak-admin (1.1.4) http-cookie (~> 1.0, >= 1.0.3) rest-client (~> 2.0) keycloak_rack (1.2.0) @@ -426,7 +435,7 @@ GEM addressable (~> 2.8) link_header (0.0.8) lint_roller (1.1.0) - liquid (5.8.7) + liquid (5.10.0) bigdecimal strscan (>= 3.1.1) listen (3.9.0) @@ -445,24 +454,26 @@ GEM lutaml-xsd (1.0.4) lutaml-model (~> 0.7) zeitwerk - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop net-smtp - marcel (1.0.4) + marcel (1.1.0) maxminddb (0.1.22) mediainfo (1.5.0) method_source (1.1.0) + metrics (0.15.0) mime-types (3.7.0) logger mime-types-data (~> 3.2025, >= 3.2025.0507) - mime-types-data (3.2025.0805) + mime-types-data (3.2025.0924) mini_magick (5.3.1) logger mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.26.0) + minitest (5.26.2) mods (3.0.5) csv (~> 3.3) edtf (~> 3.0) @@ -477,8 +488,8 @@ GEM namae (1.2.0) racc (~> 1.7) naught (1.1.0) - net-http (0.6.0) - uri + net-http (0.8.0) + uri (>= 0.11.1) net-imap (0.5.12) date net-protocol @@ -489,7 +500,7 @@ GEM net-smtp (0.5.1) net-protocol netrc (0.11.0) - nio4r (2.7.4) + nio4r (2.7.5) nokogiri (1.18.10) mini_portile2 (~> 2.8.2) racc (~> 1.4) @@ -517,7 +528,7 @@ GEM faraday (< 3) faraday-follow_redirects (>= 0.3.0, < 2) rexml - oj (3.16.11) + oj (3.16.12) bigdecimal (>= 3.0) ostruct (>= 0.2) openid_connect (2.3.1) @@ -537,7 +548,7 @@ GEM ox (2.14.23) bigdecimal (>= 3.0) parallel (1.27.0) - parser (3.3.9.0) + parser (3.3.10.0) ast (~> 2.4.1) racc pg (1.6.2) @@ -554,10 +565,13 @@ GEM activesupport (>= 6.1) pghero (3.7.0) activerecord (>= 7.1) + pitchfork (0.18.1) + logger + rack (>= 2.0) pp (0.6.3) prettyprint prettyprint (0.2.0) - prism (1.5.1) + prism (1.6.0) promise.rb (0.7.4) pry (0.15.2) coderay (~> 1.1) @@ -568,13 +582,6 @@ GEM date stringio public_suffix (6.0.2) - puma (6.6.1) - nio4r (~> 2.0) - puma-rufus-scheduler (0.1.0) - puma_worker_killer (1.0.0) - bigdecimal (>= 2.0) - get_process_mem (>= 0.2) - puma (>= 2.7) pundit (2.5.2) activesupport (>= 3.0.0) pundit-matchers (4.0.0) @@ -584,18 +591,18 @@ GEM rspec-support (~> 3.12) raabro (1.4.0) racc (1.8.1) - rack (3.1.17) + rack (3.2.4) rack-cors (3.0.0) logger rack (>= 3.0.14) - rack-oauth2 (2.2.1) + rack-oauth2 (2.3.0) activesupport attr_required faraday (~> 2.0) faraday-follow_redirects json-jwt (>= 1.11.0) rack (>= 2.1.0) - rack-protection (4.1.1) + rack-protection (4.2.1) base64 (>= 0.1.0) logger (>= 1.6.0) rack (>= 3.0.0, < 4) @@ -606,20 +613,20 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (7.2.2.2) - actioncable (= 7.2.2.2) - actionmailbox (= 7.2.2.2) - actionmailer (= 7.2.2.2) - actionpack (= 7.2.2.2) - actiontext (= 7.2.2.2) - actionview (= 7.2.2.2) - activejob (= 7.2.2.2) - activemodel (= 7.2.2.2) - activerecord (= 7.2.2.2) - activestorage (= 7.2.2.2) - activesupport (= 7.2.2.2) + rails (7.2.3) + actioncable (= 7.2.3) + actionmailbox (= 7.2.3) + actionmailer (= 7.2.3) + actionpack (= 7.2.3) + actiontext (= 7.2.3) + actionview (= 7.2.3) + activejob (= 7.2.3) + activemodel (= 7.2.3) + activerecord (= 7.2.3) + activestorage (= 7.2.3) + activesupport (= 7.2.3) bundler (>= 1.15.0) - railties (= 7.2.2.2) + railties (= 7.2.3) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest @@ -627,16 +634,18 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (7.2.2.2) - actionpack (= 7.2.2.2) - activesupport (= 7.2.2.2) + railties (7.2.3) + actionpack (= 7.2.3) + activesupport (= 7.2.3) + cgi irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) + tsort (>= 0.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.3.0) + rake (13.3.1) rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) @@ -648,7 +657,7 @@ GEM logger (~> 1.5) ostruct (~> 0.6) readline (~> 0.0) - rdoc (6.15.0) + rdoc (6.15.1) erb psych (>= 4.0.0) tsort @@ -662,7 +671,7 @@ GEM redis-objects (2.0.0.beta) redis (~> 5.0) regexp_parser (2.11.3) - reline (0.6.2) + reline (0.6.3) io-console (~> 0.5) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) @@ -673,24 +682,24 @@ GEM reverse_markdown (3.0.0) nokogiri rexml (3.4.4) - roda (3.96.0) + roda (3.98.0) rack - rspec (3.13.1) + rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) rspec-collection_matchers (1.2.1) rspec-expectations (>= 2.99.0.beta1) - rspec-core (3.13.5) + rspec-core (3.13.6) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-json_expectations (2.2.0) - rspec-mocks (3.13.5) + rspec-mocks (3.13.7) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (8.0.1) + rspec-rails (8.0.2) actionpack (>= 7.2) activesupport (>= 7.2) railties (>= 7.2) @@ -710,7 +719,7 @@ GEM rubocop-ast (>= 1.46.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) - rubocop-ast (1.46.0) + rubocop-ast (1.48.0) parser (>= 3.3.7.2) prism (~> 1.4) rubocop-factory_bot (2.27.1) @@ -738,8 +747,6 @@ GEM ffi (~> 1.12) logger ruby2_keywords (0.0.5) - rufus-scheduler (3.9.2) - fugit (~> 1.1, >= 1.11.1) safely_block (0.5.0) sanitize (7.0.0) crass (~> 1.0.2) @@ -774,18 +781,18 @@ GEM simplecov-html (0.13.2) simplecov_json_formatter (0.1.4) simpleidn (0.2.3) - sinatra (4.1.1) + sinatra (4.2.1) logger (>= 1.6.0) mustermann (~> 3.0) rack (>= 3.0.0, < 4) - rack-protection (= 4.1.1) + rack-protection (= 4.2.1) rack-session (>= 2.0.0, < 3) tilt (~> 2.0) - sinatra-contrib (4.1.1) + sinatra-contrib (4.2.1) multi_json (>= 0.0.2) mustermann (~> 3.0) - rack-protection (= 4.1.1) - sinatra (= 4.1.1) + rack-protection (= 4.2.1) + sinatra (= 4.2.1) tilt (~> 2.0) sorted_set (1.0.3) rbtree @@ -794,12 +801,10 @@ GEM statesman (12.1.0) store_model (4.3.0) activerecord (>= 7.0) - stringio (3.1.7) + stringio (3.1.8) strip_attributes (2.0.1) activemodel (>= 3.0, < 9.0) strscan (3.1.5) - sucker_punch (3.2.0) - concurrent-ruby (~> 1.0) swd (2.0.3) activesupport (>= 3) attr_required (>= 0.0.5) @@ -811,9 +816,10 @@ GEM thor (1.4.0) tilt (2.6.1) timecop (0.9.10) - timeout (0.4.3) + timeout (0.4.4) tomlib (0.7.3) bigdecimal + traces (0.18.2) tsort (0.2.0) tus-server (2.3.0) content_disposition (~> 1.0) @@ -823,8 +829,7 @@ GEM unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.1.0) - uri (1.0.4) - uri_template (0.7.0) + uri (1.1.1) useragent (0.16.11) validate_url (1.0.15) activemodel (>= 3.0.0) @@ -836,7 +841,7 @@ GEM activesupport faraday (~> 2.0) faraday-follow_redirects - webmock (3.25.1) + webmock (3.26.1) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -876,17 +881,18 @@ DEPENDENCIES activerecord (~> 7.2) activerecord-cte (~> 0.4.0) activesupport (~> 7.2) - acts_as_list (~> 1.2.4) + acts_as_list (~> 1.2.6) addressable (>= 2.8.0) after_commit_everywhere (~> 1.6.0) - ahoy_matey (~> 5.4.0) + ahoy_matey (~> 5.4.1) anystyle (~> 1.6.0) anyway_config (~> 2.7.2) - autotuner (~> 1.0.2) + async (~> 2.34.0) + autotuner (~> 1.1.0) aws-sdk-s3 (~> 1.196.1) bcrypt (~> 3.1.20) - bootsnap (>= 1.18.6) - closure_tree (~> 9.1.1) + bootsnap (>= 1.19.0) + closure_tree (~> 9.2.0) content_disposition (~> 1.0.0) csv (~> 3.3.5) database_cleaner-active_record (~> 2.2.2) @@ -909,39 +915,38 @@ DEPENDENCIES dry-validation (~> 1.11.1) factory_bot_rails (~> 6.5.0) faker (~> 3.5.2) - faraday (~> 2.13.4) - faraday-follow_redirects (~> 0.3.0) + faraday (~> 2.14.0) + faraday-follow_redirects (~> 0.4.0) faraday-retry (~> 2.3.2) ffi (~> 1.17.2) fiddle (~> 1.1.8) frozen_record (~> 0.27.1) - fugit (~> 1.11.1) + fugit (~> 1.12.1) geocoder (~> 1.8.0) - good_job (~> 4.12.0) - graphql (= 2.5.11) - graphql-batch (~> 0.6.0) + good_job (~> 4.12.1) + graphql (= 2.5.14) + graphql-batch (~> 0.6.1) graphql-client (~> 0.26.0) - graphql-fragment_cache (~> 1.22.0) + graphql-fragment_cache (~> 1.22.2) groupdate (~> 6.7.0) hashids (~> 1.0.6) htmlbeautifier (~> 1.4.3) image_processing (~> 1.14.0) iso-639 (~> 0.3.8) - jbuilder (~> 2.13.0) + jbuilder (~> 2.14.1) job-iteration (~> 1.11.0) json-ld (~> 3.3.2) - json-schema (~> 2.8.1) - json_schemer (~> 0.2.18) + json_schemer (~> 2.4.0) jwt (~> 2.8.2) - keycloak-admin (~> 1.1.3) + keycloak-admin (~> 1.1.4) keycloak_rack (= 1.2.0) kramdown (~> 2.5.1) link-header-parser (~> 7.0.1) - liquid (~> 5.8.1) + liquid (~> 5.10.0) listen (~> 3.9.0) lutaml-model (~> 0.7.3) lutaml-xsd (~> 1.0.3) - marcel (~> 1.0.4) + marcel (~> 1.1.0) maxminddb (~> 0.1.22) mediainfo (~> 1.5.0) mods (~> 3.0.4) @@ -950,7 +955,7 @@ DEPENDENCIES niso-jats! nokogiri (~> 1.18.9) oai (~> 1.3.0) - oj (= 3.16.11) + oj (= 3.16.12) openid_connect (~> 2.3.0) ostruct (~> 0.6.2) ox (~> 2.14.18) @@ -958,25 +963,23 @@ DEPENDENCIES pg_query (~> 6.1.0) pg_search (~> 2.3.7) pghero (~> 3.7.0) + pitchfork (~> 0.18.1) pry (~> 0.15.2) pry-rails (~> 0.3.11) - puma (~> 6.6.0) - puma-rufus-scheduler (~> 0.1.0) - puma_worker_killer (~> 1.0.0) pundit (~> 2.5.0) pundit-matchers (~> 4.0.0) - rack (~> 3.1.16) + rack (~> 3.2.4) rack-cors (~> 3.0.0) - rails (= 7.2.2.2) + rails (= 7.2.3) redcarpet (~> 3.6.0) redis (~> 5.4.1) redis-objects (>= 2.0.0.beta) retryable (~> 3.0.5) reverse_markdown (~> 3.0.0) - rspec (~> 3.13.1) + rspec (~> 3.13.2) rspec-collection_matchers (~> 1.2.0) rspec-json_expectations (~> 2.2.0) - rspec-rails (~> 8.0.1) + rspec-rails (~> 8.0.2) rubocop (= 1.79.2) rubocop-factory_bot (= 2.27.1) rubocop-rails (= 2.32.0) @@ -984,7 +987,6 @@ DEPENDENCIES rubocop-rspec_rails (= 2.31.0) ruby-limiter (~> 2.3.0) ruby-prof (~> 1.7.2) - rufus-scheduler (~> 3.9.2) sanitize (~> 7.0.0) scenic (~> 1.9.0) search_object_graphql (~> 1.0.5) @@ -995,22 +997,21 @@ DEPENDENCIES shrine-tus (~> 2.1.1) shrine-url (~> 2.4.1) simplecov (~> 0.22.0) - sinatra (~> 4.1.1) - sinatra-contrib (~> 4.1.1) + sinatra (~> 4.2.1) + sinatra-contrib (~> 4.2.1) sorted_set (~> 1.0.3) stackprof (~> 0.2.25) statesman (~> 12.1.0) store_model (~> 4.3.0) - stringio (~> 3.1.7) + stringio (~> 3.1.8) strip_attributes (~> 2.0.0) - sucker_punch (~> 3.2.0) syslog (~> 0.3.0) test-prof (~> 1.4.4) timecop (~> 0.9.8) tomlib (~> 0.7.2) tus-server (~> 2.3.0) validate_url (~> 1.0.15) - webmock (= 3.25.1) + webmock (= 3.26.1) with_advisory_lock (~> 7.0.2) yard (~> 0.9.34) yard-activerecord (~> 0.0.16) @@ -1018,7 +1019,7 @@ DEPENDENCIES zaru (~> 1.0.0) RUBY VERSION - ruby 3.4.4p34 + ruby 3.4.7p58 BUNDLED WITH 2.7.2 diff --git a/Procfile b/Procfile deleted file mode 100644 index c36738f4..00000000 --- a/Procfile +++ /dev/null @@ -1,2 +0,0 @@ -web: bin/start-pgbouncer bin/puma -C config/puma.rb -p ${PORT:-6222} -w ${WEB_CONCURRENCY:-2} -release: bin/rake release:post_deploy diff --git a/README.md b/README.md index 5977d211..4f7741cb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# WDP-API +# Meru-API ## Setting up locally diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ae208167..e4419eb2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,6 +6,13 @@ class ApplicationController < ActionController::API include OperationHelpers + before_action :attach_request_id! + + # @return [void] + def attach_request_id! + Support::Requests::Current.request_id = request.request_id + end + # {#perform_operation Perform} the {Users::Authenticate authenticate operation} # for the current request. # @@ -15,6 +22,8 @@ class ApplicationController < ActionController::API def authenticate_user! perform_operation "users.authenticate", request.env do |m| m.success do |user| + Support::Requests::Current.current_user = user + @current_user = user end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index c382e66f..b8c43ba7 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -4,7 +4,7 @@ class GraphQLController < ApplicationController before_action :authenticate_user! - # This method handles all GraphQL requests that WDP-API receives. + # This method handles all GraphQL requests that Meru-API receives. # # @see APISchema # @return [void] @@ -17,19 +17,23 @@ def execute request_state = Support::Requests::State.new + request_state.set_up_timer!( + query:, + operation_name:, + variables:, + ) + context = { ahoy:, current_user: @current_user, request_state:, } - result = request_state.wrap do - Utility::RequestTimer.measure!(query:, operation_name:, variables:) do - APISchema.execute(query, variables:, context:, operation_name:) - end + json = request_state.wrap do + APISchema.execute(query, variables:, context:, operation_name:) end - render json: result + render(json:) rescue StandardError => e # :nocov: raise e unless Rails.env.development? diff --git a/app/graphql/api_schema.rb b/app/graphql/api_schema.rb index 6e3b3698..27a789e9 100644 --- a/app/graphql/api_schema.rb +++ b/app/graphql/api_schema.rb @@ -5,7 +5,13 @@ # @see GraphqlController#execute # @subsystem GraphQL class APISchema < GraphQL::Schema - use GraphQL::Batch + if MeruConfig.experimental_dataloader? + use CustomDataloader + else + use GraphQL::Batch + end + + trace_with(GraphQL::Tracing::ActiveSupportNotificationsTrace) use GraphQL::FragmentCache @@ -33,6 +39,46 @@ class APISchema < GraphQL::Schema end extra_types( + Types::SchemaPropertyKindType, + Types::Schematic::AssetPropertyType, + Types::Schematic::AssetsPropertyType, + Types::Schematic::BooleanPropertyType, + Types::Schematic::ContributorPropertyType, + Types::Schematic::ContributorsPropertyType, + Types::Schematic::ControlledVocabulariesPropertyType, + Types::Schematic::ControlledVocabularyPropertyType, + Types::Schematic::DatePropertyType, + Types::Schematic::EmailPropertyType, + Types::Schematic::EntitiesPropertyType, + Types::Schematic::EntityPropertyType, + Types::Schematic::FloatPropertyType, + Types::Schematic::FullTextPropertyType, + Types::Schematic::GroupPropertyType, + Types::Schematic::HasAvailableEntitiesType, + Types::Schematic::HasControlledVocabularyType, + Types::Schematic::IntegerPropertyType, + Types::Schematic::MarkdownPropertyType, + Types::Schematic::MultiselectPropertyType, + Types::Schematic::OptionablePropertyType, + Types::Schematic::ScalarPropertyType, + Types::Schematic::SchemaPropertyType, + Types::Schematic::SelectOptionType, + Types::Schematic::SelectPropertyType, + Types::Schematic::StringPropertyType, + Types::Schematic::TagsPropertyType, + Types::Schematic::TimestampPropertyType, + Types::Schematic::UnknownPropertyType, + Types::Schematic::URLPropertyType, + Types::Schematic::VariableDatePropertyType, + Types::AnyChildEntityType, + Types::AnyContributableType, + Types::AnyContributionType, + Types::AnyContributorAttributionType, + Types::AnyContributorType, + Types::AnyEntityType, + Types::AnyOrderingPathType, + Types::AnyScalarPropertyType, + Types::AnySchemaPropertyType, Types::ContributorType, Types::ContributorBaseType, Types::ContributorAttributionType, @@ -40,11 +86,50 @@ class APISchema < GraphQL::Schema Types::ContributorAttributionEdgeType, Types::ContributorCollectionAttributionType, Types::ContributorItemAttributionType, + Types::EntityType, + Types::OrderingPathType, Types::OrganizationContributorType, Types::PersonContributorType, + Types::SearchResultType, + Types::SchemaPropertyTypeType, + Types::SchemaInstanceType, Types::TemplateContributionType ) + orphan_types( + *Types::AnyOrderingPathType.possible_types, + Types::Schematic::AssetPropertyType, + Types::Schematic::AssetsPropertyType, + Types::Schematic::BooleanPropertyType, + Types::Schematic::ContributorPropertyType, + Types::Schematic::ContributorsPropertyType, + Types::Schematic::ControlledVocabulariesPropertyType, + Types::Schematic::ControlledVocabularyPropertyType, + Types::Schematic::DatePropertyType, + Types::Schematic::EmailPropertyType, + Types::Schematic::EntitiesPropertyType, + Types::Schematic::EntityPropertyType, + Types::Schematic::FloatPropertyType, + Types::Schematic::FullTextPropertyType, + Types::Schematic::GroupPropertyType, + Types::Schematic::IntegerPropertyType, + Types::Schematic::MarkdownPropertyType, + Types::Schematic::MultiselectPropertyType, + Types::Schematic::SelectOptionType, + Types::Schematic::SelectPropertyType, + Types::Schematic::StringPropertyType, + Types::Schematic::TagsPropertyType, + Types::Schematic::TimestampPropertyType, + Types::Schematic::UnknownPropertyType, + Types::Schematic::URLPropertyType, + Types::Schematic::VariableDatePropertyType, + Types::ContributorCollectionAttributionType, + Types::ContributorItemAttributionType, + Types::OrganizationContributorType, + Types::PersonContributorType, + Types::SearchResultType + ) + class << self def id_from_object(object, type_definition, query_context) Support::System["relay_node.id_from_object"].call(object, type_definition:, query_context:) do |m| diff --git a/app/graphql/mutations/base_mutation.rb b/app/graphql/mutations/base_mutation.rb index b1fdfda4..81a253fb 100644 --- a/app/graphql/mutations/base_mutation.rb +++ b/app/graphql/mutations/base_mutation.rb @@ -36,7 +36,17 @@ def perform_operation(name, **args) operation = resolve_container_value name - middleware.call operation, **args + wrap_mutation_call do + middleware.call operation, **args + end + end + + def wrap_mutation_call + return yield unless MeruConfig.experimental_dataloader? + + Sync(annotation: "mutation.resolve") do + yield + end end class << self diff --git a/app/graphql/sources/active_record_association.rb b/app/graphql/sources/active_record_association.rb new file mode 100644 index 00000000..19e23efd --- /dev/null +++ b/app/graphql/sources/active_record_association.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Sources + class ActiveRecordAssociation < GraphQL::Dataloader::ActiveRecordAssociationSource + # @param [Class] model_class + # @param [Symbol] association_name + def initialize(model, association_name) + super(association_name, nil) + + @model = model + @association_name = association_name + + validate! + end + + # @param [] records + # @return [] + # def fetch(records) + # preload_association!(records) + + # records.map { read_association(_1) } + # end + + private + + # @param [ApplicationRecord] record + # def association_loaded?(record) + # record.association(@association_name).loaded? + # end + + # @param [] records + # @return [void] + # def preload_association!(records) + # ::ActiveRecord::Associations::Preloader.new(records:, associations: [@association_name]).call + # end + + # @param [ApplicationRecord] record + # @return [ActiveRecord::Relation, ApplicationRecord, nil] + # def read_association(record) + # record.public_send(@association_name) + # end + + # @return [void] + def validate! + # :nocov: + raise ArgumentError, "No association #{@association_name} on #{@model}" unless @model.reflect_on_association(@association_name) + # :nocov: + end + end +end diff --git a/app/graphql/sources/ancestor_of_type.rb b/app/graphql/sources/ancestor_of_type.rb new file mode 100644 index 00000000..0b6ddfc9 --- /dev/null +++ b/app/graphql/sources/ancestor_of_type.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Sources + class AncestorOfType < GraphQL::Dataloader::Source + # @param [String] schema + def initialize(schema) + @schema = schema + end + + # @param [Array] entities + # @return [Array] + def fetch(entities) + ancestors = {} + + EntityBreadcrumb.for_ancestor_of_type(@schema, *entities).each do |breadcrumb| + ancestors[breadcrumb.entity_id] = breadcrumb.crumb + end + + entities.map do |entity| + ancestors[entity.id] || nil + end + end + end +end diff --git a/app/graphql/sources/contextual_permission.rb b/app/graphql/sources/contextual_permission.rb new file mode 100644 index 00000000..0885eeeb --- /dev/null +++ b/app/graphql/sources/contextual_permission.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Sources + class ContextualPermission < GraphQL::Dataloader::Source + # @return [User, AnonymousUser] + attr_reader :user + + # @param [User, AnonymousUser] user + def initialize(user) + @user = user || AnonymousUser.new + end + + # @param [] entities + def fetch(entities) + permissions = {} + + ContextualPermission.scope_to(@user, entities).find_each do |record| + permissions[record.entity_id] = record + end + + entities.each do |entity| + # :nocov: + permissions[entity.id] ||= ContextualPermission.empty_permission_for(@user, entity) + # :nocov: + end + + entities.map { |entity| permissions[entity.id] } + end + end +end diff --git a/app/graphql/sources/controlled_vocabulary_provider.rb b/app/graphql/sources/controlled_vocabulary_provider.rb new file mode 100644 index 00000000..cb504da1 --- /dev/null +++ b/app/graphql/sources/controlled_vocabulary_provider.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Sources + class ControlledVocabularyProvider < GraphQL::Dataloader::Source + # @param [Array] wants + def fetch(wants) + wants.map { ControlledVocabularySource.providing(_1) } + end + end +end diff --git a/app/graphql/sources/entity_layouts.rb b/app/graphql/sources/entity_layouts.rb new file mode 100644 index 00000000..775b79df --- /dev/null +++ b/app/graphql/sources/entity_layouts.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Sources + class EntityLayouts < GraphQL::Dataloader::Source + # @param [] entities + def fetch(entities) + threads = entities.map do |entity| + Async do + Thread.new do + ApplicationRecord.connection_pool.with_connection do + entity.check_layouts.value_or(nil) + end + end + end + end + + dataloader.yield + + threads.map(&:wait).map(&:value) + end + end +end diff --git a/app/graphql/sources/ordering_by_identifier.rb b/app/graphql/sources/ordering_by_identifier.rb new file mode 100644 index 00000000..81cf8536 --- /dev/null +++ b/app/graphql/sources/ordering_by_identifier.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Sources + class OrderingByIdentifier < GraphQL::Dataloader::Source + def initialize(identifier) + @identifier = identifier + end + + # @param [] entities + def fetch(entities) + hsh = {} + + Ordering.by_entity(entities).by_identifier(@identifier).find_each do |ordering| + hsh[ordering.entity] = ordering + end + + entities.map { |entity| hsh[entity] } + end + end +end diff --git a/app/graphql/sources/ordering_by_schema.rb b/app/graphql/sources/ordering_by_schema.rb new file mode 100644 index 00000000..54ace781 --- /dev/null +++ b/app/graphql/sources/ordering_by_schema.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Sources + class OrderingBySchema < GraphQL::Dataloader::Source + def initialize(slug) + @slug = slug + end + + # @param [] entities + def fetch(entities) + hsh = {} + + Ordering.by_entity(entities).by_handled_schema_definition(@slug).find_each do |ordering| + hsh[ordering.entity] = ordering + end + + entities.map { |entity| hsh[entity] } + end + end +end diff --git a/app/graphql/sources/ordering_entry_count.rb b/app/graphql/sources/ordering_entry_count.rb new file mode 100644 index 00000000..c5d56830 --- /dev/null +++ b/app/graphql/sources/ordering_entry_count.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Sources + class OrderingEntryCount < GraphQL::Dataloader::Source + # @param [] orderings + def fetch(orderings) + counts = OrderingEntry.visible_count_for orderings + + orderings.map { |ordering| counts[ordering.id] || 0 } + end + end +end diff --git a/app/graphql/sources/schema_property_context.rb b/app/graphql/sources/schema_property_context.rb new file mode 100644 index 00000000..07ef5e65 --- /dev/null +++ b/app/graphql/sources/schema_property_context.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Sources + class SchemaPropertyContext < GraphQL::Dataloader::Source + include MeruAPI::Deps[to_context: "schemas.instances.read_property_context"] + + # @param [] records + def fetch(records) + records.map { |record| to_context(record) } + end + + private + + def to_context(record) + MeruAPI::Container["schemas.instances.read_property_context"].(record) + end + end +end diff --git a/app/graphql/types/abstract_model.rb b/app/graphql/types/abstract_model.rb index b3782d5f..eeac6aed 100644 --- a/app/graphql/types/abstract_model.rb +++ b/app/graphql/types/abstract_model.rb @@ -4,7 +4,7 @@ module Types # @abstract class AbstractModel < Types::BaseObject implements GraphQL::Types::Relay::Node - implements Types::Sluggable + implements ::Types::SluggableType global_id_field :id diff --git a/app/graphql/types/access_grant_subject_type.rb b/app/graphql/types/access_grant_subject_type.rb index 8fdd4116..a1926329 100644 --- a/app/graphql/types/access_grant_subject_type.rb +++ b/app/graphql/types/access_grant_subject_type.rb @@ -9,8 +9,9 @@ module AccessGrantSubjectType (and all its children) has been granted. TEXT - field :all_access_grants, resolver: Resolvers::AccessGrants::SubjectResolver, - description: "A polymorphic connection for access grants from a subject" + field :all_access_grants, resolver: Resolvers::AccessGrants::SubjectResolver do + description "A polymorphic connection for access grants from a subject" + end field :primary_role, Types::RoleType, null: true do description "The primary role associated with this subject." @@ -26,14 +27,8 @@ module AccessGrantSubjectType TEXT end - # @return [Promise] - def assignable_roles - Support::Loaders::AssociationLoader.for(object.class, :assignable_roles).load(object) - end + load_association! :assignable_roles - # @return [Promise(Role), Promise(nil)] - def primary_role - Support::Loaders::AssociationLoader.for(object.class, :primary_role).load(object) - end + load_association! :primary_role end end diff --git a/app/graphql/types/access_grant_type.rb b/app/graphql/types/access_grant_type.rb index d6380142..0a39bc88 100644 --- a/app/graphql/types/access_grant_type.rb +++ b/app/graphql/types/access_grant_type.rb @@ -10,7 +10,7 @@ module AccessGrantType as its accessControlList defines. TEXT - field :entity, Types::AnyEntityType, null: false, method: :accessible do + field :entity, Types::EntityType, null: false, method: :accessible do description "The polymorphic entity to which access has been granted" end diff --git a/app/graphql/types/announcement_type.rb b/app/graphql/types/announcement_type.rb index d7e07c55..4ab61355 100644 --- a/app/graphql/types/announcement_type.rb +++ b/app/graphql/types/announcement_type.rb @@ -11,7 +11,7 @@ class AnnouncementType < Types::AbstractModel to provide time-sensensitive information and news about a specific entity in the system. TEXT - field :entity, "Types::AnyEntityType", null: false, + field :entity, "Types::EntityType", null: false, description: "The entity that owns the announcement" field :published_on, GraphQL::Types::ISO8601Date, null: false, diff --git a/app/graphql/types/asset_type.rb b/app/graphql/types/asset_type.rb index 84baddc6..e5f19bde 100644 --- a/app/graphql/types/asset_type.rb +++ b/app/graphql/types/asset_type.rb @@ -3,13 +3,14 @@ module Types module AssetType include Types::BaseInterface - include GraphQL::Types::Relay::NodeBehaviors - include Types::Sluggable + + implements ::GraphQL::Types::Relay::Node + implements ::Types::SluggableType description "A generic asset type, implemented by all the more specific kinds" field :alt_text, String, null: true - field :attachable, Types::AnyAttachableType, null: false + field :attachable, Types::AttachableType, null: false field :name, String, null: false field :caption, String, null: true field :kind, Types::AssetKindType, null: false diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index ca282169..ea3e2108 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -17,7 +17,7 @@ def call_operation!(name, ...) end # @api private - # @param [Promise(HierarchicalEntity)] promise + # @param [HierarchicalEntity, nil] promise # @return [void] def track_entity_event!(promise, name: "entity.view", **data) promise.then do |entity| diff --git a/app/graphql/types/child_entity_type.rb b/app/graphql/types/child_entity_type.rb index 34a487b5..67b8d1de 100644 --- a/app/graphql/types/child_entity_type.rb +++ b/app/graphql/types/child_entity_type.rb @@ -38,7 +38,7 @@ module ChildEntityType field :root, Boolean, null: false, method: :root? field :leaf, Boolean, null: false, method: :leaf? - field :ancestor_by_name, Types::AnyEntityType, null: true do + field :ancestor_by_name, Types::EntityType, null: true do description <<~TEXT Directly fetch a defined named ancestor by its name. It can be null, either because an invalid name was provided, the schema hierarchy is @@ -48,7 +48,7 @@ module ChildEntityType argument :name, String, required: true, description: "The name of the ancestor to fetch" end - field :ancestor_of_type, Types::AnyEntityType, null: true do + field :ancestor_of_type, Types::EntityType, null: true do description <<~TEXT Look up an ancestor for this entity that implements a specific type. It ascends from this entity, so it will first check the parent, then the grandparent, and so on. @@ -89,7 +89,11 @@ def ancestor_by_name(name:) # @param [String] schema # @return [HierarchicalEntity, nil] def ancestor_of_type(schema:) - Loaders::AncestorOfTypeLoader.for(schema).load(object) + if MeruConfig.experimental_dataloader? + dataloader.with(Sources::AncestorOfType, schema).load(object) + else + Loaders::AncestorOfTypeLoader.for(schema).load(object) + end end end end diff --git a/app/graphql/types/collection_contribution_type.rb b/app/graphql/types/collection_contribution_type.rb index 6724dd32..c614e99a 100644 --- a/app/graphql/types/collection_contribution_type.rb +++ b/app/graphql/types/collection_contribution_type.rb @@ -7,6 +7,6 @@ class CollectionContributionType < Types::AbstractModel description "A contribution to a collection" field :collection, "Types::CollectionType", null: false - field :contributor, "Types::AnyContributorType", null: false + field :contributor, "Types::ContributorType", null: false end end diff --git a/app/graphql/types/collection_type.rb b/app/graphql/types/collection_type.rb index 4d02aac7..94078a48 100644 --- a/app/graphql/types/collection_type.rb +++ b/app/graphql/types/collection_type.rb @@ -14,6 +14,7 @@ class CollectionType < Types::AbstractModel implements Types::HasSchemaPropertiesType implements Types::PermalinkableType implements Types::AttachableType + implements Types::OrderingEntryableType implements Types::SchemaInstanceType implements Types::SearchableType diff --git a/app/graphql/types/community_type.rb b/app/graphql/types/community_type.rb index 0c9b81e9..dbe52f62 100644 --- a/app/graphql/types/community_type.rb +++ b/app/graphql/types/community_type.rb @@ -10,13 +10,15 @@ class CommunityType < Types::AbstractModel implements Types::HasSchemaPropertiesType implements Types::PermalinkableType implements Types::AttachableType + implements Types::OrderingEntryableType implements Types::SchemaInstanceType implements Types::SearchableType description "A community of users" - field :hero_image_layout, Types::HeroImageLayoutType, null: false, - description: "The layout to use when rendering this community's hero image." + field :hero_image_layout, Types::HeroImageLayoutType, null: false do + description "The layout to use when rendering this community's hero image." + end field :tagline, String, null: true diff --git a/app/graphql/types/contextual_permission_type.rb b/app/graphql/types/contextual_permission_type.rb index 8f5458df..f2224d76 100644 --- a/app/graphql/types/contextual_permission_type.rb +++ b/app/graphql/types/contextual_permission_type.rb @@ -7,27 +7,31 @@ class ContextualPermissionType < Types::AbstractModel description "A contextual permission for a user, role, and entity" field :access_control_list, Types::AccessControlListType, null: true do - description "Derived access control list" + description <<~TEXT + The derived access control list for this user and entity. + TEXT end - field :access_grants, [Types::AnyUserAccessGrantType, { null: false }], null: false, - description: "The access grants that correspond to this contextual permission" - - field :roles, [Types::RoleType, { null: false }], null: false, - description: "The roles that correspond to this contextual permission" - - field :user, Types::UserType, null: false - - def access_grants - object.association(:access_grants).loaded? ? object.access_grants : Support::Loaders::AssociationLoader.for(object.class, :access_grants).load(object) + field :access_grants, [Types::UserAccessGrantType, { null: false }], null: false do + description <<~TEXT + The access grants that correspond to this contextual permission. + TEXT end - def roles - object.association(:roles).loaded? ? object.roles : Support::Loaders::AssociationLoader.for(object.class, :roles).load(object) + field :roles, [Types::RoleType, { null: false }], null: false do + description <<~TEXT + The roles that correspond to this contextual permission. + TEXT end - def user - object.association(:user).loaded? ? object.user : Support::Loaders::AssociationLoader.for(object.class, :user).load(object) + field :user, Types::UserType, null: false do + description <<~TEXT + The user that has the contextual permission. + TEXT end + + load_association! :access_grants + load_association! :roles + load_association! :user end end diff --git a/app/graphql/types/contribution_base_type.rb b/app/graphql/types/contribution_base_type.rb index 7b5b830d..0d6b51f4 100644 --- a/app/graphql/types/contribution_base_type.rb +++ b/app/graphql/types/contribution_base_type.rb @@ -69,7 +69,7 @@ def role role_label end - # @return [Promise(String)] + # @return [String] def role_label contribution_role.then(&:label) end diff --git a/app/graphql/types/contribution_type.rb b/app/graphql/types/contribution_type.rb index f4782661..d6e8a053 100644 --- a/app/graphql/types/contribution_type.rb +++ b/app/graphql/types/contribution_type.rb @@ -14,7 +14,7 @@ module ContributionType implements Types::ContributionBaseType - field :contributor, "Types::AnyContributorType", null: false do + field :contributor, "Types::ContributorType", null: false do description <<~TEXT The contributor, loaded via union for the most control. TEXT diff --git a/app/graphql/types/contributor_attribution_edge_type.rb b/app/graphql/types/contributor_attribution_edge_type.rb index 6a10691e..a2e775ac 100644 --- a/app/graphql/types/contributor_attribution_edge_type.rb +++ b/app/graphql/types/contributor_attribution_edge_type.rb @@ -4,6 +4,6 @@ module Types class ContributorAttributionEdgeType < Types::BaseEdge graphql_name "ContributorAttributionEdge" - node_type Types::AnyContributorAttributionType + node_type Types::ContributorAttributionType end end diff --git a/app/graphql/types/contributor_base_type.rb b/app/graphql/types/contributor_base_type.rb index d7e10402..ec29d63e 100644 --- a/app/graphql/types/contributor_base_type.rb +++ b/app/graphql/types/contributor_base_type.rb @@ -14,7 +14,7 @@ module ContributorBaseType TEXT implements Types::HasHarvestModificationStatusType - implements Types::Sluggable + implements ::Types::SluggableType field :kind, Types::ContributorKindType, null: false diff --git a/app/graphql/types/entity_base_type.rb b/app/graphql/types/entity_base_type.rb index 3d18caf4..cc24aab5 100644 --- a/app/graphql/types/entity_base_type.rb +++ b/app/graphql/types/entity_base_type.rb @@ -9,7 +9,7 @@ module EntityBaseType but no ability to traverse the hierarchy. TEXT - implements Types::Sluggable + implements ::Types::SluggableType field :title, String, null: false do description <<~TEXT diff --git a/app/graphql/types/entity_breadcrumb_type.rb b/app/graphql/types/entity_breadcrumb_type.rb index a043c2a4..84982d4b 100644 --- a/app/graphql/types/entity_breadcrumb_type.rb +++ b/app/graphql/types/entity_breadcrumb_type.rb @@ -6,15 +6,13 @@ class EntityBreadcrumbType < Types::BaseObject global_id_field :id - field :crumb, Types::AnyEntityType, null: false + field :crumb, Types::EntityType, null: false field :depth, Int, null: false field :label, String, null: false field :kind, Types::EntityKindType, null: false field :slug, String, null: false - def crumb - Support::Loaders::AssociationLoader.for(EntityBreadcrumb, :crumb).load(object) - end + load_association! :crumb def label crumb.then do |crumb| diff --git a/app/graphql/types/entity_descendant_type.rb b/app/graphql/types/entity_descendant_type.rb index 178a03b2..27556adb 100644 --- a/app/graphql/types/entity_descendant_type.rb +++ b/app/graphql/types/entity_descendant_type.rb @@ -5,7 +5,7 @@ module Types class EntityDescendantType < Types::BaseObject description "A descendant of an `Entity`." - field :descendant, "Types::AnyEntityType", null: false do + field :descendant, "Types::EntityType", null: false do description "The actual descendant entity" end @@ -17,9 +17,6 @@ class EntityDescendantType < Types::BaseObject description "The scope of this entity relative to its ancestor" end - # @return [HierarchicalEntity] - def descendant - Support::Loaders::AssociationLoader.for(object.class, :descendant).load(object) - end + load_association! :descendant end end diff --git a/app/graphql/types/entity_link_type.rb b/app/graphql/types/entity_link_type.rb index b9ab09b6..6614538f 100644 --- a/app/graphql/types/entity_link_type.rb +++ b/app/graphql/types/entity_link_type.rb @@ -4,8 +4,10 @@ module Types class EntityLinkType < Types::AbstractModel description "A link between different entities" - field :source, Types::AnyEntityType, null: false - field :target, Types::AnyEntityType, null: false + implements Types::OrderingEntryableType + + field :source, Types::EntityType, null: false + field :target, Types::EntityType, null: false field :operator, Types::EntityLinkOperatorType, null: false field :scope, Types::EntityLinkScopeType, null: false diff --git a/app/graphql/types/entity_select_option_type.rb b/app/graphql/types/entity_select_option_type.rb index e522bf7f..6112d674 100644 --- a/app/graphql/types/entity_select_option_type.rb +++ b/app/graphql/types/entity_select_option_type.rb @@ -15,9 +15,11 @@ class EntitySelectOptionType < Types::BaseObject field :kind, Types::EntityKindType, null: false, method: :to_schematic_referent_kind - field :entity, Types::AnyEntityType, null: false + field :entity, "Types::EntityType", null: false - field :schema_version, Types::SchemaVersionType, null: false + field :schema_version, "Types::SchemaVersionType", null: false + + load_association! :schema_version # @return [HierarchicalEntity] def entity @@ -31,10 +33,5 @@ def entity # :nocov: end end - - # @return [SchemaVersion] - def schema_version - Support::Loaders::AssociationLoader.for(object.class, :schema_version).load(object) - end end end diff --git a/app/graphql/types/entity_type.rb b/app/graphql/types/entity_type.rb index 873c8442..a6de6e8b 100644 --- a/app/graphql/types/entity_type.rb +++ b/app/graphql/types/entity_type.rb @@ -10,7 +10,7 @@ module EntityType implements Types::HasEntityBreadcrumbs implements Types::HasSchemaPropertiesType implements Types::SearchableType - implements Types::Sluggable + implements ::Types::SluggableType description "An entity that exists in the hierarchy." @@ -109,10 +109,13 @@ module EntityType # @!group Contextual Permission Support - # @see Loaders::ContextualPermissionLoader - # @return [Promise(ContextualPermission)] + # @return [ContextualPermission] def contextual_permission - Loaders::ContextualPermissionLoader.for(context[:current_user]).load(object) + if MeruConfig.experimental_dataloader? + dataloader.with(Sources::ContextualPermission, context[:current_user]).load(object) + else + Loaders::ContextualPermissionLoader.for(context[:current_user]).load(object) + end end # This surfaces the `access_control_list` from the associated {#contextual_permission}. @@ -128,47 +131,59 @@ def access_control_list # This surfaces the `allowed_actions` from the associated {#contextual_permission}. # # @see ContextualPermission#allowed_actions - # @return [Promise] + # @return [String] def allowed_actions contextual_permission.then(&:allowed_actions) end - # @return [Promise] + # @return [Role] def assignable_roles contextual_permission.then(&:assignable_roles) end - # @return [Promise()] + # @return [] def applicable_roles contextual_permission.then(&:roles) end - # @see Entities::CheckLayouts - # @see Entities::LayoutsChecker - # @see Types::EntityLayoutsType - # @return [Promise(Entities::LayoutsProxy)] - # @return [Promise(nil)] - def layouts - Loaders::EntityLayoutsLoader.load(object) - end - - # @return [Promise] + # @return [] def permissions contextual_permission.then(&:permissions) end # @!endgroup + # @see Entities::CheckLayouts + # @see Entities::LayoutsChecker + # @see Sources::EntityLayouts + # @see Types::EntityLayoutsType + # @return [Entities::LayoutsProxy, nil] + def layouts + if MeruConfig.experimental_dataloader? + dataloader.with(Sources::EntityLayouts).load(object) + else + Loaders::EntityLayoutsLoader.load(object) + end + end + # @param [String] identifier # @return [Ordering, nil] def ordering(identifier:) - Loaders::OrderingByIdentifierLoader.for(identifier).load(object) + if MeruConfig.experimental_dataloader? + dataloader.with(Sources::OrderingByIdentifier, identifier).load(object) + else + Loaders::OrderingByIdentifierLoader.for(identifier).load(object) + end end # @param [String] slug # @return [Ordering, nil] def ordering_for_schema(slug:) - Loaders::OrderingBySchemaLoader.for(slug).load(object) + if MeruConfig.experimental_dataloader? + dataloader.with(Sources::OrderingBySchema, slug).load(object) + else + Loaders::OrderingBySchemaLoader.for(slug).load(object) + end end # @param [String] slug @@ -180,7 +195,7 @@ def page(slug:) # @param [String] slug # @return [Announcement, nil] def announcement(slug:) - Support::Loaders::RecordLoader.for(Announcement).load(slug) + load_record_with(::Announcement, slug) end end end diff --git a/app/graphql/types/harvest_target_type.rb b/app/graphql/types/harvest_target_type.rb index a9501d0b..5919ebc4 100644 --- a/app/graphql/types/harvest_target_type.rb +++ b/app/graphql/types/harvest_target_type.rb @@ -12,7 +12,7 @@ module HarvestTargetType TEXT implements Types::EntityBaseType - implements Types::Sluggable + implements ::Types::SluggableType field :harvest_target_kind, Types::HarvestTargetKindType, null: false do description <<~TEXT diff --git a/app/graphql/types/has_entity_breadcrumbs.rb b/app/graphql/types/has_entity_breadcrumbs.rb index 5993d48c..3d88402f 100644 --- a/app/graphql/types/has_entity_breadcrumbs.rb +++ b/app/graphql/types/has_entity_breadcrumbs.rb @@ -9,11 +9,6 @@ module HasEntityBreadcrumbs description "Previous entries in the hierarchy" end - # @see HierarchicalEntity#entity_breadcrumbs - # @see Support::Loaders::AssociationLoader - # @return [] - def breadcrumbs - Support::Loaders::AssociationLoader.for(object.class, :entity_breadcrumbs).load(object) - end + load_association! :entity_breadcrumbs, as: :breadcrumbs end end diff --git a/app/graphql/types/has_schema_properties_type.rb b/app/graphql/types/has_schema_properties_type.rb index 8be124bd..2f163adf 100644 --- a/app/graphql/types/has_schema_properties_type.rb +++ b/app/graphql/types/has_schema_properties_type.rb @@ -4,8 +4,11 @@ module Types module HasSchemaPropertiesType include Types::BaseInterface - field :schema_properties, [Types::AnySchemaPropertyType, { null: false }], null: false, - description: "A list of schema properties associated with this instance or version." + field :schema_properties, ["::Types::Schematic::SchemaPropertyType", { null: false }], null: false do + description <<~TEXT + A list of schema properties associated with this instance or version. + TEXT + end # @see Schemas::Instances::ReadProperties def schema_properties diff --git a/app/graphql/types/hierarchical_schema_rank_type.rb b/app/graphql/types/hierarchical_schema_rank_type.rb index 26f84c9c..30761453 100644 --- a/app/graphql/types/hierarchical_schema_rank_type.rb +++ b/app/graphql/types/hierarchical_schema_rank_type.rb @@ -12,31 +12,32 @@ class HierarchicalSchemaRankType < Types::BaseObject navigation or calculate statistics about what various entities contain. TEXT - field :count, Integer, null: false, method: :schema_count, - description: "The number of entities that implement this schema from this point in the hierarchy." - - field :rank, Integer, null: false, method: :schema_rank, - description: "The rank of this schema at this point in the hierarchy, based on the statistical mode of its depth relative to the parent." - - field :distinct_version_count, Integer, null: false, - description: "A count of distinct versions of this specific schema type from this point of the hierarchy." + field :count, Integer, null: false, method: :schema_count do + description "The number of entities that implement this schema from this point in the hierarchy." + end - field :schema_definition, Types::SchemaDefinitionType, null: false, - description: "A reference to the discrete schema definition" + field :rank, Integer, null: false, method: :schema_rank do + description "The rank of this schema at this point in the hierarchy, based on the statistical mode of its depth relative to the parent." + end - field :slug, String, null: false, - description: "A fully-qualified unique value that can be used to refer to this schema within the system", - method: :schema_slug + field :distinct_version_count, Integer, null: false do + description "A count of distinct versions of this specific schema type from this point of the hierarchy." + end - field :version_ranks, [Types::HierarchicalSchemaVersionRankType, { null: false }], null: false, - description: "A reference to the schema versions from this ranking" + field :schema_definition, Types::SchemaDefinitionType, null: false do + description "A reference to the discrete schema definition" + end - def schema_definition - Support::Loaders::AssociationLoader.for(object.class, :schema_definition).load(object) + field :slug, String, null: false, method: :schema_slug do + description "A fully-qualified unique value that can be used to refer to this schema within the system" end - def version_ranks - Support::Loaders::AssociationLoader.for(object.class, :hierarchical_schema_version_ranks).load(object) + field :version_ranks, [Types::HierarchicalSchemaVersionRankType, { null: false }], null: false do + description "A reference to the schema versions from this ranking" end + + load_association! :schema_definition + + load_association! :hierarchical_schema_version_ranks, as: :version_ranks end end diff --git a/app/graphql/types/item_contribution_type.rb b/app/graphql/types/item_contribution_type.rb index 2a31c43a..66d38af2 100644 --- a/app/graphql/types/item_contribution_type.rb +++ b/app/graphql/types/item_contribution_type.rb @@ -6,7 +6,7 @@ class ItemContributionType < Types::AbstractModel description "A contribution to an item" - field :contributor, "Types::AnyContributorType", null: false + field :contributor, "Types::ContributorType", null: false field :item, "Types::ItemType", null: false end end diff --git a/app/graphql/types/item_type.rb b/app/graphql/types/item_type.rb index 3f73711f..f0941b1b 100644 --- a/app/graphql/types/item_type.rb +++ b/app/graphql/types/item_type.rb @@ -13,6 +13,7 @@ class ItemType < Types::AbstractModel implements Types::HasSchemaPropertiesType implements Types::PermalinkableType implements Types::AttachableType + implements Types::OrderingEntryableType implements Types::SchemaInstanceType implements Types::SearchableType diff --git a/app/graphql/types/layout_definition_type.rb b/app/graphql/types/layout_definition_type.rb index ae27ac1d..7da90547 100644 --- a/app/graphql/types/layout_definition_type.rb +++ b/app/graphql/types/layout_definition_type.rb @@ -17,7 +17,7 @@ module LayoutDefinitionType def templates all_templates = object.template_definition_names.map { __send__(_1) } - Promise.all(all_templates).then do + maybe_await(all_templates).then do object.template_definitions end end diff --git a/app/graphql/types/layout_instance_type.rb b/app/graphql/types/layout_instance_type.rb index dfe4ecad..18045092 100644 --- a/app/graphql/types/layout_instance_type.rb +++ b/app/graphql/types/layout_instance_type.rb @@ -28,7 +28,7 @@ module LayoutInstanceType field :layout_kind, Types::LayoutKindType, null: false - field :entity, Types::AnyEntityType, null: false do + field :entity, Types::EntityType, null: false do description <<~TEXT The associated entity for this layout instance. TEXT @@ -41,7 +41,7 @@ module LayoutInstanceType def templates all_templates = object.template_instance_names.map { __send__(_1) } - Promise.all(all_templates).then do + maybe_await(all_templates).then do object.template_instances end end diff --git a/app/graphql/types/link_target_candidate_type.rb b/app/graphql/types/link_target_candidate_type.rb index 5edc5699..c626df16 100644 --- a/app/graphql/types/link_target_candidate_type.rb +++ b/app/graphql/types/link_target_candidate_type.rb @@ -8,7 +8,7 @@ class LinkTargetCandidateType < Types::BaseObject description "A candidate for a link target, scoped to a parent source" - field :target, "Types::AnyLinkTargetType", null: false, + field :target, "Types::EntityType", null: false, description: "The actual target" field :target_id, ID, null: false, diff --git a/app/graphql/types/named_ancestor_type.rb b/app/graphql/types/named_ancestor_type.rb index d84dd354..1d2a65c1 100644 --- a/app/graphql/types/named_ancestor_type.rb +++ b/app/graphql/types/named_ancestor_type.rb @@ -15,7 +15,7 @@ class NamedAncestorType < Types::BaseObject description "The name of the ancestor. Guaranteed to be unique for this specific entity." end - field :ancestor, Types::AnyEntityType, null: false do + field :ancestor, Types::EntityType, null: false do description "The actual ancestor" end diff --git a/app/graphql/types/node_type.rb b/app/graphql/types/node_type.rb index c71ec3ee..39a9020d 100644 --- a/app/graphql/types/node_type.rb +++ b/app/graphql/types/node_type.rb @@ -3,6 +3,7 @@ module Types module NodeType include Types::BaseInterface + # Add the `id` field include GraphQL::Types::Relay::NodeBehaviors end diff --git a/app/graphql/types/ordering_entry_type.rb b/app/graphql/types/ordering_entry_type.rb index cc02b068..78dacefa 100644 --- a/app/graphql/types/ordering_entry_type.rb +++ b/app/graphql/types/ordering_entry_type.rb @@ -14,7 +14,7 @@ class OrderingEntryType < Types::AbstractModel TEXT end - field :entry, "Types::AnyOrderingEntryType", null: false do + field :entry, "Types::OrderingEntryableType", null: false do description <<~TEXT The actual element being ordered. At present, this will only be a `Community`, `Collection`, or `Item`, but future implementations of orderings may include other content, such as presentation elements. @@ -80,14 +80,12 @@ class OrderingEntryType < Types::AbstractModel # @!group Delegated Entity Methods - # @return [Promise(String)] - # @return [Promise(nil)] + # @return [String, nil] def entry_slug entry.then { |entity| entity.id if entity.kind_of?(HierarchicalEntity) } end - # @return [Promise(String)] - # @return [Promise(nil)] + # @return [String, nil] def entry_title entry.then { |entity| entity.title if entity.respond_to?(:title) } end diff --git a/app/graphql/types/ordering_entryable_type.rb b/app/graphql/types/ordering_entryable_type.rb new file mode 100644 index 00000000..fb494100 --- /dev/null +++ b/app/graphql/types/ordering_entryable_type.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Types + module OrderingEntryableType + include Types::BaseInterface + + implements GraphQL::Types::Relay::Node + + description <<~TEXT + An entity or link which can appear in an ordering. + TEXT + end +end diff --git a/app/graphql/types/ordering_type.rb b/app/graphql/types/ordering_type.rb index cb110276..fef62700 100644 --- a/app/graphql/types/ordering_type.rb +++ b/app/graphql/types/ordering_type.rb @@ -7,7 +7,7 @@ class OrderingType < Types::AbstractModel implements Types::SearchableType - field :entity, Types::AnyEntityType, null: false do + field :entity, Types::EntityType, null: false do description "The entity that owns the ordering" end @@ -87,10 +87,15 @@ class OrderingType < Types::AbstractModel TEXT end - # @see Loaders::OrderingEntryCountLoader + load_association! :entity + # @return [Integer] def count - Loaders::OrderingEntryCountLoader.load object + if MeruConfig.experimental_dataloader? + dataloader.with(Sources::OrderingEntryCount).load(object) + else + Loaders::OrderingEntryCountLoader.load(object) + end end # @see Schemas::Orderings::Definition#filter @@ -99,15 +104,6 @@ def filter object.definition.filter end - # @return [Promise] - def entity - if object.association(:entity).loaded? - Promise.resolve(object.entity) - else - Support::Loaders::AssociationLoader.for(object.class, :entity).load(object) - end - end - # @see Schemas::Orderings::Definition#select # @return [Schemas::Orderings::SelectDefinition] def select diff --git a/app/graphql/types/page_type.rb b/app/graphql/types/page_type.rb index 470cd2cc..b32a86e2 100644 --- a/app/graphql/types/page_type.rb +++ b/app/graphql/types/page_type.rb @@ -8,7 +8,7 @@ class PageType < Types::BaseObject global_id_field :id - field :entity, "Types::AnyEntityType", null: false + field :entity, "Types::EntityType", null: false field :position, Integer, null: true field :title, String, null: false diff --git a/app/graphql/types/queries_contrib.rb b/app/graphql/types/queries_contrib.rb index 3f2eb087..0b86bb76 100644 --- a/app/graphql/types/queries_contrib.rb +++ b/app/graphql/types/queries_contrib.rb @@ -14,13 +14,13 @@ module QueriesContrib argument :slug, Types::SlugType, required: true end - field :contributor, Types::AnyContributorType, null: true do + field :contributor, Types::ContributorType, null: true do description "Look up a contributor by slug" argument :slug, Types::SlugType, required: true end - field :contributor_lookup, Types::AnyContributorType, null: true do + field :contributor_lookup, Types::ContributorType, null: true do description <<~TEXT Look up a contributor `by` a certain `value`. TEXT @@ -66,7 +66,7 @@ module QueriesContrib end def collection_contribution(slug:) - Support::Loaders::RecordLoader.for(CollectionContribution).load(slug) + load_record_with(::CollectionContribution, slug) end # @param [Contributable, nil] contributable @@ -76,13 +76,15 @@ def contribution_roles(contributable: nil) end def contributor(slug:) - Support::Loaders::RecordLoader.for(Contributor).load(slug) + load_record_with(::Contributor, slug) end def contributor_lookup(**options) call_operation "contributors.lookup", **options do |m| - m.success do |contributor| - contributor + m.success do |mapped_options| + mapped_options => { value:, find_by:, } + + load_record_with(::Contributor, value, find_by:) end m.failure(:invalid) do |_, reason| @@ -98,7 +100,7 @@ def contributor_lookup(**options) end def item_contribution(slug:) - Support::Loaders::RecordLoader.for(ItemContribution).load(slug) + load_record_with(::ItemContribution, slug) end end end diff --git a/app/graphql/types/queries_controlled_vocabulary.rb b/app/graphql/types/queries_controlled_vocabulary.rb index 7dedea38..8a785a35 100644 --- a/app/graphql/types/queries_controlled_vocabulary.rb +++ b/app/graphql/types/queries_controlled_vocabulary.rb @@ -26,7 +26,7 @@ module QueriesControlledVocabulary # @param [String] slug # @return [ControlledVocabulary, nil] def controlled_vocabulary(slug:) - Support::Loaders::RecordLoader.for(ControlledVocabulary).load(slug) + load_record_with(::ControlledVocabulary, slug) end end end diff --git a/app/graphql/types/queries_controlled_vocabulary_source.rb b/app/graphql/types/queries_controlled_vocabulary_source.rb index 136cebe0..b485bfd7 100644 --- a/app/graphql/types/queries_controlled_vocabulary_source.rb +++ b/app/graphql/types/queries_controlled_vocabulary_source.rb @@ -26,7 +26,7 @@ module QueriesControlledVocabularySource # @param [String] slug # @return [ControlledVocabularySource, nil] def controlled_vocabulary_source(slug:) - Support::Loaders::RecordLoader.for(ControlledVocabularySource).load(slug) + load_record_with(::ControlledVocabularySource, slug) end end end diff --git a/app/graphql/types/queries_entities.rb b/app/graphql/types/queries_entities.rb index 25bd93d5..17c6c248 100644 --- a/app/graphql/types/queries_entities.rb +++ b/app/graphql/types/queries_entities.rb @@ -8,7 +8,7 @@ module QueriesEntities Fields for querying all entities from the top level of the hierarchy. TEXT - field :asset, Types::AnyAssetType, null: true do + field :asset, Types::AssetType, null: true do description "Look up an asset by slug" argument :slug, Types::SlugType, required: true @@ -43,17 +43,17 @@ module QueriesEntities end def asset(slug:) - Support::Loaders::RecordLoader.for(Asset).load(slug) + load_record_with(::Asset, slug) end def collection(slug:) - Support::Loaders::RecordLoader.for(Collection).load(slug).tap do |collection| + load_record_with(::Collection, slug).tap do |collection| track_entity_event! collection end end def community(slug:) - Support::Loaders::RecordLoader.for(Community).load(slug).tap do |community| + load_record_with(::Community, slug).tap do |community| track_entity_event! community end end @@ -67,7 +67,7 @@ def community_by_title(title:) end def item(slug:) - Support::Loaders::RecordLoader.for(Item).load(slug).tap do |item| + load_record_with(::Item, slug).tap do |item| track_entity_event! item end end diff --git a/app/graphql/types/queries_harvest_attempt.rb b/app/graphql/types/queries_harvest_attempt.rb index ad5dd650..867a8e90 100644 --- a/app/graphql/types/queries_harvest_attempt.rb +++ b/app/graphql/types/queries_harvest_attempt.rb @@ -30,7 +30,7 @@ module QueriesHarvestAttempt # @param [String] slug # @return [HarvestAttempt, nil] def harvest_attempt(slug:) - Support::Loaders::RecordLoader.for(HarvestAttempt).load(slug) + load_record_with(::HarvestAttempt, slug) end end end diff --git a/app/graphql/types/queries_harvest_mapping.rb b/app/graphql/types/queries_harvest_mapping.rb index c7c75622..4ebbf753 100644 --- a/app/graphql/types/queries_harvest_mapping.rb +++ b/app/graphql/types/queries_harvest_mapping.rb @@ -30,7 +30,7 @@ module QueriesHarvestMapping # @param [String] slug # @return [HarvestMapping, nil] def harvest_mapping(slug:) - Support::Loaders::RecordLoader.for(HarvestMapping).load(slug) + load_record_with(::HarvestMapping, slug) end end end diff --git a/app/graphql/types/queries_harvest_record.rb b/app/graphql/types/queries_harvest_record.rb index 003b64a4..c22f6d39 100644 --- a/app/graphql/types/queries_harvest_record.rb +++ b/app/graphql/types/queries_harvest_record.rb @@ -30,7 +30,7 @@ module QueriesHarvestRecord # @param [String] slug # @return [HarvestRecord, nil] def harvest_record(slug:) - Support::Loaders::RecordLoader.for(HarvestRecord).load(slug) + load_record_with(::HarvestRecord, slug) end end end diff --git a/app/graphql/types/queries_harvest_set.rb b/app/graphql/types/queries_harvest_set.rb index 96fb7bc4..ac68116b 100644 --- a/app/graphql/types/queries_harvest_set.rb +++ b/app/graphql/types/queries_harvest_set.rb @@ -26,7 +26,7 @@ module QueriesHarvestSet # @param [String] slug # @return [HarvestSet, nil] def harvest_set(slug:) - Support::Loaders::RecordLoader.for(HarvestSet).load(slug) + load_record_with(::HarvestSet, slug) end end end diff --git a/app/graphql/types/queries_harvest_source.rb b/app/graphql/types/queries_harvest_source.rb index 141fd1a9..bd06d2c1 100644 --- a/app/graphql/types/queries_harvest_source.rb +++ b/app/graphql/types/queries_harvest_source.rb @@ -26,7 +26,7 @@ module QueriesHarvestSource # @param [String] slug # @return [HarvestSource, nil] def harvest_source(slug:) - Support::Loaders::RecordLoader.for(HarvestSource).load(slug) + load_record_with(::HarvestSource, slug) end end end diff --git a/app/graphql/types/queries_permalink.rb b/app/graphql/types/queries_permalink.rb index 38ee4e44..68e3498c 100644 --- a/app/graphql/types/queries_permalink.rb +++ b/app/graphql/types/queries_permalink.rb @@ -38,13 +38,13 @@ module QueriesPermalink # @param [String] slug # @return [Permalink, nil] def permalink(slug:) - Support::Loaders::RecordLoader.for(Permalink).load(slug) + load_record_with(::Permalink, slug) end # @param [String] uri # @return [Permalink, nil] def permalink_by_uri(uri:) - Support::Loaders::RecordLoader.for(Permalink, column: :uri).load(uri) + load_record_with(::Permalink, uri, find_by: :uri) end end end diff --git a/app/graphql/types/queries_schemas.rb b/app/graphql/types/queries_schemas.rb index a20dfe98..aad51199 100644 --- a/app/graphql/types/queries_schemas.rb +++ b/app/graphql/types/queries_schemas.rb @@ -8,7 +8,7 @@ module QueriesSchemas Fields for querying schemas and schema-related data. TEXT - field :ordering_paths, [Types::AnyOrderingPathType, { null: false }], null: false do + field :ordering_paths, [Types::OrderingPathType, { null: false }], null: false do description "A list of ordering paths for creating and updating orderings." argument :schemas, [Types::OrderingSchemaFilterInputType, { null: false }], required: false do diff --git a/app/graphql/types/queries_user.rb b/app/graphql/types/queries_user.rb index bf866a1e..540dba56 100644 --- a/app/graphql/types/queries_user.rb +++ b/app/graphql/types/queries_user.rb @@ -24,7 +24,7 @@ module QueriesUser # @param [String] slug # @return [User, nil] def user(slug:) - Support::Loaders::RecordLoader.for(User, column: :keycloak_id).load(slug) + load_record_with(::User, slug, find_by: :keycloak_id) end # @return [User, AnonymousUser] diff --git a/app/graphql/types/references_global_entity_dates_type.rb b/app/graphql/types/references_global_entity_dates_type.rb index a5c957bf..d53c7d17 100644 --- a/app/graphql/types/references_global_entity_dates_type.rb +++ b/app/graphql/types/references_global_entity_dates_type.rb @@ -15,13 +15,11 @@ module ReferencesGlobalEntityDatesType description "The date this entity was published" end - def load_named_variable_dates - Support::Loaders::AssociationLoader.for(object.class, :named_variable_dates) - end + load_association! :named_variable_dates def published - load_named_variable_dates.then do - Promise.resolve object.published + named_variable_dates.then do + object.published end.then do |value| VariablePrecisionDate.parse value end diff --git a/app/graphql/types/schema_instance_context_type.rb b/app/graphql/types/schema_instance_context_type.rb index aff3ff3a..0f6bd871 100644 --- a/app/graphql/types/schema_instance_context_type.rb +++ b/app/graphql/types/schema_instance_context_type.rb @@ -9,28 +9,35 @@ class SchemaInstanceContextType < Types::BaseObject field :contributors, [Types::ContributorSelectOptionType, { null: false }], null: false - field :default_values, GraphQL::Types::JSON, null: false, - description: "Not yet populated. May be used in the future." + field :default_values, GraphQL::Types::JSON, null: false do + description "Not yet populated. May be used in the future." + end - field :entity_id, ID, null: false, - description: "The entity ID for this schema instance." + field :entity_id, ID, null: false do + description "The entity ID for this schema instance." + end - field :field_values, GraphQL::Types::JSON, null: false, - description: "The values for the schema form on this instance" + field :field_values, GraphQL::Types::JSON, null: false do + description "The values for the schema form on this instance" + end - field :schema_version_slug, String, null: false, - description: "The slug for the current schema version" + field :schema_version_slug, String, null: false do + description "The slug for the current schema version" + end - field :validity, Types::SchemaInstanceValidationType, null: true, - description: "Information about the validity of the schema instance" + field :validity, Types::SchemaInstanceValidationType, null: true do + description "Information about the validity of the schema instance" + end # @return [] def assets instance = object.instance + # :nocov: return [] if instance.blank? + # :nocov: - Support::Loaders::AssociationLoader.for(instance.class, :assets).load(instance).then do |assets| + association_loader_for(:assets, klass: instance.class, record: instance).then do |assets| assets.map(&:to_schematic_referent_option) end end diff --git a/app/graphql/types/schema_instance_type.rb b/app/graphql/types/schema_instance_type.rb index 32f793ac..e87a9403 100644 --- a/app/graphql/types/schema_instance_type.rb +++ b/app/graphql/types/schema_instance_type.rb @@ -11,25 +11,34 @@ module SchemaInstanceType and the necessary context. TEXT - field :available_entities_for, [Types::EntitySelectOptionType, { null: false }], null: false do + field :available_entities_for, ["Types::EntitySelectOptionType", { null: false }], null: false do description <<~TEXT Expose all available entities for this schema property. TEXT - argument :full_path, String, required: true, - description: "The full path to the schema property. Please note, paths are snake_case, not camelCase." + argument :full_path, String, required: true do + description <<~TEXT + The full path to the schema property. Please note, paths are snake_case, not camelCase. + TEXT + end end - field :schema_instance_context, Types::SchemaInstanceContextType, null: false, - description: "The context for our schema instance. Includes form values and necessary referents." + field :schema_instance_context, "Types::SchemaInstanceContextType", null: false do + description <<~TEXT + The context for our schema instance. Includes form values and necessary referents. + TEXT + end - field :schema_property, Types::AnySchemaPropertyType, null: true do + field :schema_property, "::Types::Schematic::SchemaPropertyType", null: true do description <<~TEXT Read a single schema property by its full path. TEXT - argument :full_path, String, required: true, - description: "The full path to the schema property. Please note, paths are snake_case, not camelCase." + argument :full_path, String, required: true do + description <<~TEXT + The full path to the schema property. Please note, paths are snake_case, not camelCase. + TEXT + end end def available_entities_for(full_path:) @@ -40,9 +49,12 @@ def available_entities_for(full_path:) end end - # @see Loaders::SchemaPropertyContextLoader def schema_instance_context - Loaders::SchemaPropertyContextLoader.for(object.class).load(object) + if MeruConfig.experimental_dataloader? + dataloader.with(Sources::SchemaPropertyContext).load(object) + else + Loaders::SchemaPropertyContextLoader.for(object.class).load(object) + end end # @see Schemas::Instances::ReadProperties @@ -61,17 +73,21 @@ def schema_property(full_path:) end end + load_association! :schematic_collected_references + load_association! :schematic_scalar_references + load_association! :schematic_texts + private def with_schema_associations_loaded - Promise.all( - [ - schema_instance_context, - Support::Loaders::AssociationLoader.for(object.class, :schematic_collected_references).load(object), - Support::Loaders::AssociationLoader.for(object.class, :schematic_scalar_references).load(object), - Support::Loaders::AssociationLoader.for(object.class, :schematic_texts).load(object) - ] - ) + associations = [ + schema_instance_context, + schematic_collected_references, + schematic_scalar_references, + schematic_texts, + ] + + maybe_await(associations) end end end diff --git a/app/graphql/types/schema_version_type.rb b/app/graphql/types/schema_version_type.rb index 7e67afba..1bf670f8 100644 --- a/app/graphql/types/schema_version_type.rb +++ b/app/graphql/types/schema_version_type.rb @@ -29,7 +29,7 @@ class SchemaVersionType < Types::AbstractModel TEXT end - field :searchable_properties, [Types::AnySearchablePropertyType, { null: false }], null: false do + field :searchable_properties, [Types::SearchablePropertyType, { null: false }], null: false do description <<~TEXT A subset of properties that can be searched for this schema. TEXT @@ -83,13 +83,9 @@ class SchemaVersionType < Types::AbstractModel description "A boolean for the logic on `enforcedChildVersions`." end - def enforced_parent_versions - Support::Loaders::AssociationLoader.for(object.class, :enforced_parent_versions).load(object) - end + load_association! :enforced_parent_versions - def enforced_child_versions - Support::Loaders::AssociationLoader.for(object.class, :enforced_child_versions).load(object) - end + load_association! :enforced_child_versions # @see Schemas::Versions::Configuration#render # @return [Schemas::Versions::RenderDefinition] diff --git a/app/graphql/types/schematic/asset_property_type.rb b/app/graphql/types/schematic/asset_property_type.rb index 4be6687f..77d338ee 100644 --- a/app/graphql/types/schematic/asset_property_type.rb +++ b/app/graphql/types/schematic/asset_property_type.rb @@ -3,9 +3,10 @@ module Types module Schematic class AssetPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType - field :asset, Types::AnyAssetType, null: true, method: :value + field :asset, "Types::AssetType", null: true, method: :value end end end diff --git a/app/graphql/types/schematic/assets_property_type.rb b/app/graphql/types/schematic/assets_property_type.rb index a5748099..c0e348c2 100644 --- a/app/graphql/types/schematic/assets_property_type.rb +++ b/app/graphql/types/schematic/assets_property_type.rb @@ -3,9 +3,10 @@ module Types module Schematic class AssetsPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType - field :assets, [Types::AnyAssetType, { null: false }], null: false, method: :value + field :assets, ["Types::AssetType", { null: false }], null: false, method: :value end end end diff --git a/app/graphql/types/schematic/boolean_property_type.rb b/app/graphql/types/schematic/boolean_property_type.rb index 36208f70..5f60f1c4 100644 --- a/app/graphql/types/schematic/boolean_property_type.rb +++ b/app/graphql/types/schematic/boolean_property_type.rb @@ -3,7 +3,8 @@ module Types module Schematic class BooleanPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType implements Types::SearchablePropertyType field :checked_by_default, Boolean, null: true, method: :default diff --git a/app/graphql/types/schematic/contributor_property_type.rb b/app/graphql/types/schematic/contributor_property_type.rb index 184a0f3b..ea76989d 100644 --- a/app/graphql/types/schematic/contributor_property_type.rb +++ b/app/graphql/types/schematic/contributor_property_type.rb @@ -3,9 +3,10 @@ module Types module Schematic class ContributorPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType - field :contributor, Types::AnyContributorType, null: true, method: :value + field :contributor, "Types::ContributorType", null: true, method: :value end end end diff --git a/app/graphql/types/schematic/contributors_property_type.rb b/app/graphql/types/schematic/contributors_property_type.rb index 6690d0b5..44ad540f 100644 --- a/app/graphql/types/schematic/contributors_property_type.rb +++ b/app/graphql/types/schematic/contributors_property_type.rb @@ -3,9 +3,10 @@ module Types module Schematic class ContributorsPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType - field :contributors, [Types::AnyContributorType, { null: false }], null: false, method: :value + field :contributors, ["Types::ContributorType", { null: false }], null: false, method: :value end end end diff --git a/app/graphql/types/schematic/controlled_vocabularies_property_type.rb b/app/graphql/types/schematic/controlled_vocabularies_property_type.rb index f8baf954..3730998d 100644 --- a/app/graphql/types/schematic/controlled_vocabularies_property_type.rb +++ b/app/graphql/types/schematic/controlled_vocabularies_property_type.rb @@ -3,10 +3,11 @@ module Types module Schematic class ControlledVocabulariesPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements HasControlledVocabularyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::Schematic::HasControlledVocabularyType - field :controlled_vocabulary_items, [Types::ControlledVocabularyItemType, { null: false }], null: false, method: :value + field :controlled_vocabulary_items, ["Types::ControlledVocabularyItemType", { null: false }], null: false, method: :value end end end diff --git a/app/graphql/types/schematic/controlled_vocabulary_property_type.rb b/app/graphql/types/schematic/controlled_vocabulary_property_type.rb index a8c3bdb8..094116d7 100644 --- a/app/graphql/types/schematic/controlled_vocabulary_property_type.rb +++ b/app/graphql/types/schematic/controlled_vocabulary_property_type.rb @@ -3,10 +3,11 @@ module Types module Schematic class ControlledVocabularyPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements HasControlledVocabularyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::Schematic::HasControlledVocabularyType - field :controlled_vocabulary_item, Types::ControlledVocabularyItemType, null: true, method: :value + field :controlled_vocabulary_item, "Types::ControlledVocabularyItemType", null: true, method: :value end end end diff --git a/app/graphql/types/schematic/date_property_type.rb b/app/graphql/types/schematic/date_property_type.rb index 8bde7c16..a1382828 100644 --- a/app/graphql/types/schematic/date_property_type.rb +++ b/app/graphql/types/schematic/date_property_type.rb @@ -3,8 +3,9 @@ module Types module Schematic class DatePropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements Types::SearchablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::SearchablePropertyType field :default, GraphQL::Types::ISO8601Date, null: true field :date, GraphQL::Types::ISO8601Date, null: true, method: :value diff --git a/app/graphql/types/schematic/email_property_type.rb b/app/graphql/types/schematic/email_property_type.rb index 49533293..aba253a4 100644 --- a/app/graphql/types/schematic/email_property_type.rb +++ b/app/graphql/types/schematic/email_property_type.rb @@ -3,7 +3,8 @@ module Types module Schematic class EmailPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType field :default_address, String, null: true, method: :default field :address, String, null: true, method: :value diff --git a/app/graphql/types/schematic/entities_property_type.rb b/app/graphql/types/schematic/entities_property_type.rb index b3e7159a..750766e2 100644 --- a/app/graphql/types/schematic/entities_property_type.rb +++ b/app/graphql/types/schematic/entities_property_type.rb @@ -4,14 +4,15 @@ module Types module Schematic # @see Schemas::Properties::Scalar::Entities class EntitiesPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements HasAvailableEntitiesType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::Schematic::HasAvailableEntitiesType description <<~TEXT A property that references a deterministically-ordered list of entities. TEXT - field :entities, [Types::AnyEntityType, { null: false }], null: false, method: :value do + field :entities, ["Types::EntityType", { null: false }], null: false, method: :value do description <<~TEXT A deterministically-ordered list of entities. diff --git a/app/graphql/types/schematic/entity_property_type.rb b/app/graphql/types/schematic/entity_property_type.rb index 22f08cdf..626da0f4 100644 --- a/app/graphql/types/schematic/entity_property_type.rb +++ b/app/graphql/types/schematic/entity_property_type.rb @@ -4,14 +4,15 @@ module Types module Schematic # @see Schemas::Properties::Scalar::Entity class EntityPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements HasAvailableEntitiesType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::Schematic::HasAvailableEntitiesType description <<~TEXT A property that references another entity within the system. TEXT - field :entity, Types::AnyEntityType, null: true, method: :value do + field :entity, "Types::EntityType", null: true, method: :value do description <<~TEXT A single reference to another entity within the system. TEXT diff --git a/app/graphql/types/schematic/float_property_type.rb b/app/graphql/types/schematic/float_property_type.rb index 725ca76a..2e0ec938 100644 --- a/app/graphql/types/schematic/float_property_type.rb +++ b/app/graphql/types/schematic/float_property_type.rb @@ -3,8 +3,9 @@ module Types module Schematic class FloatPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements Types::SearchablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::SearchablePropertyType field :default_float, Float, null: true, method: :default field :float_value, Float, null: true, method: :value diff --git a/app/graphql/types/schematic/full_text_property_type.rb b/app/graphql/types/schematic/full_text_property_type.rb index 5b78169e..b7de412c 100644 --- a/app/graphql/types/schematic/full_text_property_type.rb +++ b/app/graphql/types/schematic/full_text_property_type.rb @@ -3,8 +3,9 @@ module Types module Schematic class FullTextPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements Types::SearchablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::SearchablePropertyType field :full_text, Types::FullTextType, null: true, method: :value end diff --git a/app/graphql/types/schematic/group_property_type.rb b/app/graphql/types/schematic/group_property_type.rb index 707d0edb..264d7e3d 100644 --- a/app/graphql/types/schematic/group_property_type.rb +++ b/app/graphql/types/schematic/group_property_type.rb @@ -3,11 +3,23 @@ module Types module Schematic class GroupPropertyType < Types::AbstractObjectType - implements SchemaPropertyType + implements ::Types::Schematic::SchemaPropertyType - field :legend, String, null: true - field :required, Boolean, null: false - field :properties, [Types::AnyScalarPropertyType, { null: false }], null: false + description <<~TEXT + A schema property that groups other schema properties together underneath a `path`. + TEXT + + field :legend, String, null: true do + description "The legend / label for this group property." + end + + field :required, Boolean, null: false do + description "Whether this property is required to have a value." + end + + field :properties, ["Types::Schematic::ScalarPropertyType", { null: false }], null: false do + description "The list of (scalar) schema properties contained within this group." + end end end end diff --git a/app/graphql/types/schematic/has_available_entities_type.rb b/app/graphql/types/schematic/has_available_entities_type.rb index 5e2b7e67..6520e001 100644 --- a/app/graphql/types/schematic/has_available_entities_type.rb +++ b/app/graphql/types/schematic/has_available_entities_type.rb @@ -5,7 +5,7 @@ module Schematic module HasAvailableEntitiesType include Types::BaseInterface - field :available_entities, [Types::EntitySelectOptionType, { null: false }], null: false do + field :available_entities, ["Types::EntitySelectOptionType", { null: false }], null: false do description <<~TEXT Expose all available entities for this schema property. TEXT diff --git a/app/graphql/types/schematic/has_controlled_vocabulary_type.rb b/app/graphql/types/schematic/has_controlled_vocabulary_type.rb index c7b7263a..27b6ac96 100644 --- a/app/graphql/types/schematic/has_controlled_vocabulary_type.rb +++ b/app/graphql/types/schematic/has_controlled_vocabulary_type.rb @@ -5,7 +5,7 @@ module Schematic module HasControlledVocabularyType include Types::BaseInterface - field :controlled_vocabulary, ::Types::ControlledVocabularyType, null: true do + field :controlled_vocabulary, "::Types::ControlledVocabularyType", null: true do description <<~TEXT The vocabulary configured for this property, based on its `wants` value and whatever is currently configured in `ControlledVocabularySource`. @@ -20,9 +20,13 @@ module HasControlledVocabularyType TEXT end - # @return [Promise(ControlledVocabulary), nil] + # @return [ControlledVocabulary, nil] def controlled_vocabulary - Loaders::ControlledVocabularyProvider.load object.wants + if MeruConfig.experimental_dataloader? + dataloader.with(Sources::ControlledVocabularyProvider).load(object.wants) + else + ::Loaders::ControlledVocabularyProvider.load object.wants + end end end end diff --git a/app/graphql/types/schematic/integer_property_type.rb b/app/graphql/types/schematic/integer_property_type.rb index ebb4e25c..6eb033b6 100644 --- a/app/graphql/types/schematic/integer_property_type.rb +++ b/app/graphql/types/schematic/integer_property_type.rb @@ -3,8 +3,9 @@ module Types module Schematic class IntegerPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements Types::SearchablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::SearchablePropertyType field :default_integer, Integer, null: true, method: :default field :integer_value, Integer, null: true, method: :value diff --git a/app/graphql/types/schematic/markdown_property_type.rb b/app/graphql/types/schematic/markdown_property_type.rb index 334dab06..19f57501 100644 --- a/app/graphql/types/schematic/markdown_property_type.rb +++ b/app/graphql/types/schematic/markdown_property_type.rb @@ -3,8 +3,9 @@ module Types module Schematic class MarkdownPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements Types::SearchablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::SearchablePropertyType field :default, String, null: true field :content, String, null: true, method: :value diff --git a/app/graphql/types/schematic/multiselect_property_type.rb b/app/graphql/types/schematic/multiselect_property_type.rb index 82a7585d..9afb0516 100644 --- a/app/graphql/types/schematic/multiselect_property_type.rb +++ b/app/graphql/types/schematic/multiselect_property_type.rb @@ -3,9 +3,10 @@ module Types module Schematic class MultiselectPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements OptionablePropertyType - implements Types::SearchablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::Schematic::OptionablePropertyType + implements ::Types::SearchablePropertyType field :default_selections, [String, { null: false }], null: true, method: :default field :selections, [String, { null: false }], null: true, method: :value diff --git a/app/graphql/types/schematic/optionable_property_type.rb b/app/graphql/types/schematic/optionable_property_type.rb index 219e8668..48a1fd7d 100644 --- a/app/graphql/types/schematic/optionable_property_type.rb +++ b/app/graphql/types/schematic/optionable_property_type.rb @@ -5,7 +5,13 @@ module Schematic module OptionablePropertyType include ::Types::BaseInterface - field :options, [SelectOptionType, { null: false }], null: false + description <<~TEXT + An interface for properties that have a set of predefined options to choose from. + TEXT + + field :options, [SelectOptionType, { null: false }], null: false do + description "The list of predefined options available for this property." + end end end end diff --git a/app/graphql/types/schematic/scalar_property_type.rb b/app/graphql/types/schematic/scalar_property_type.rb index 443a92b8..c96a8fd9 100644 --- a/app/graphql/types/schematic/scalar_property_type.rb +++ b/app/graphql/types/schematic/scalar_property_type.rb @@ -5,7 +5,7 @@ module Schematic module ScalarPropertyType include Types::BaseInterface - implements SchemaPropertyType + implements ::Types::Schematic::SchemaPropertyType field :label, String, null: false do description <<~TEXT diff --git a/app/graphql/types/schematic/schema_property_type.rb b/app/graphql/types/schematic/schema_property_type.rb index 1b97504e..957e1ef2 100644 --- a/app/graphql/types/schematic/schema_property_type.rb +++ b/app/graphql/types/schematic/schema_property_type.rb @@ -33,13 +33,13 @@ module SchemaPropertyType TEXT end - field :kind, Types::SchemaPropertyKindType, null: false do + field :kind, "Types::SchemaPropertyKindType", null: false do description <<~TEXT Provided for introspection. This describes the underlying structure of the data type. TEXT end - field :type, Types::SchemaPropertyTypeType, null: false do + field :type, "Types::SchemaPropertyTypeType", null: false do description <<~TEXT Provided for introspection. This represents the actual data type this property uses. diff --git a/app/graphql/types/schematic/select_option_type.rb b/app/graphql/types/schematic/select_option_type.rb index 242752d0..ddb5a5dd 100644 --- a/app/graphql/types/schematic/select_option_type.rb +++ b/app/graphql/types/schematic/select_option_type.rb @@ -3,8 +3,15 @@ module Types module Schematic class SelectOptionType < Types::BaseObject - field :label, String, null: false - field :value, String, null: false + description "An option for a select-type property." + + field :label, String, null: false do + description "The display label for the option." + end + + field :value, String, null: false do + description "The underlying value for the option." + end end end end diff --git a/app/graphql/types/schematic/select_property_type.rb b/app/graphql/types/schematic/select_property_type.rb index 33ab902c..e2114157 100644 --- a/app/graphql/types/schematic/select_property_type.rb +++ b/app/graphql/types/schematic/select_property_type.rb @@ -3,8 +3,9 @@ module Types module Schematic class SelectPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements OptionablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::Schematic::OptionablePropertyType implements Types::SearchablePropertyType field :default_selection, String, null: true, method: :default diff --git a/app/graphql/types/schematic/string_property_type.rb b/app/graphql/types/schematic/string_property_type.rb index cbba33b8..2a67cbdd 100644 --- a/app/graphql/types/schematic/string_property_type.rb +++ b/app/graphql/types/schematic/string_property_type.rb @@ -3,7 +3,8 @@ module Types module Schematic class StringPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType implements Types::SearchablePropertyType field :default, String, null: true diff --git a/app/graphql/types/schematic/tags_property_type.rb b/app/graphql/types/schematic/tags_property_type.rb index ffe86749..680012f4 100644 --- a/app/graphql/types/schematic/tags_property_type.rb +++ b/app/graphql/types/schematic/tags_property_type.rb @@ -3,7 +3,8 @@ module Types module Schematic class TagsPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType field :tags, [String, { null: false }], null: false, method: :value end diff --git a/app/graphql/types/schematic/timestamp_property_type.rb b/app/graphql/types/schematic/timestamp_property_type.rb index b0bf92fb..6a57e188 100644 --- a/app/graphql/types/schematic/timestamp_property_type.rb +++ b/app/graphql/types/schematic/timestamp_property_type.rb @@ -3,8 +3,9 @@ module Types module Schematic class TimestampPropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements Types::SearchablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::SearchablePropertyType field :default, GraphQL::Types::ISO8601DateTime, null: true field :timestamp, GraphQL::Types::ISO8601DateTime, null: true, method: :value diff --git a/app/graphql/types/schematic/unknown_property_type.rb b/app/graphql/types/schematic/unknown_property_type.rb index ae7cfcd2..45600ca5 100644 --- a/app/graphql/types/schematic/unknown_property_type.rb +++ b/app/graphql/types/schematic/unknown_property_type.rb @@ -3,7 +3,8 @@ module Types module Schematic class UnknownPropertyType < Types::AbstractObjectType - implements ScalarPropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType field :default, GraphQL::Types::JSON, null: true field :unknown_value, GraphQL::Types::JSON, null: true, method: :value diff --git a/app/graphql/types/schematic/url_property_type.rb b/app/graphql/types/schematic/url_property_type.rb index 2f91a489..771734a6 100644 --- a/app/graphql/types/schematic/url_property_type.rb +++ b/app/graphql/types/schematic/url_property_type.rb @@ -3,9 +3,10 @@ module Types module Schematic class URLPropertyType < Types::AbstractObjectType - description "A schematic reference to a URL, with href, label, and optional title" + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType - implements ScalarPropertyType + description "A schematic reference to a URL, with href, label, and optional title" field :url, Types::URLReferenceType, null: true, method: :value end diff --git a/app/graphql/types/schematic/variable_date_property_type.rb b/app/graphql/types/schematic/variable_date_property_type.rb index 516b7b14..680f4980 100644 --- a/app/graphql/types/schematic/variable_date_property_type.rb +++ b/app/graphql/types/schematic/variable_date_property_type.rb @@ -3,10 +3,11 @@ module Types module Schematic class VariableDatePropertyType < Types::AbstractObjectType - implements ScalarPropertyType - implements Types::SearchablePropertyType + implements ::Types::Schematic::SchemaPropertyType + implements ::Types::Schematic::ScalarPropertyType + implements ::Types::SearchablePropertyType - field :date_with_precision, Types::VariablePrecisionDateType, null: true, method: :value + field :date_with_precision, ::Types::VariablePrecisionDateType, null: true, method: :value end end end diff --git a/app/graphql/types/search_result_type.rb b/app/graphql/types/search_result_type.rb index f31116b2..eda4e600 100644 --- a/app/graphql/types/search_result_type.rb +++ b/app/graphql/types/search_result_type.rb @@ -3,8 +3,8 @@ module Types # @see Resolvers::SearchResultResolver class SearchResultType < Types::BaseObject - implements GraphQL::Types::Relay::Node - implements Types::Sluggable + implements ::GraphQL::Types::Relay::Node + implements ::Types::SluggableType description <<~TEXT An entity that's the result of a search. @@ -16,7 +16,7 @@ class SearchResultType < Types::BaseObject TEXT end - field :entity, Types::AnyEntityType, null: false do + field :entity, "Types::EntityType", null: false do description <<~TEXT A reference to the actual entity returned by the search query. TEXT @@ -28,7 +28,7 @@ class SearchResultType < Types::BaseObject TEXT end - field :schema_version, Types::SchemaVersionType, null: false do + field :schema_version, "Types::SchemaVersionType", null: false do description <<~TEXT The schema version of the returned entity. TEXT @@ -49,7 +49,7 @@ class SearchResultType < Types::BaseObject load_association! :hierarchical, as: :entity load_association! :schema_version - # @return [Promise(String)] + # @return [String] def id entity.then do |ent| context.schema.id_from_object(ent, self.class, context) diff --git a/app/graphql/types/searchable_property_type.rb b/app/graphql/types/searchable_property_type.rb index 4fce4c35..2552628c 100644 --- a/app/graphql/types/searchable_property_type.rb +++ b/app/graphql/types/searchable_property_type.rb @@ -4,6 +4,10 @@ module Types module SearchablePropertyType include Types::BaseInterface + description <<~TEXT + An interface for properties that can be searched. + TEXT + field :label, String, null: false field :description, String, null: true field :search_path, String, null: false diff --git a/app/graphql/types/sluggable.rb b/app/graphql/types/sluggable_type.rb similarity index 92% rename from app/graphql/types/sluggable.rb rename to app/graphql/types/sluggable_type.rb index 640dd267..dc287027 100644 --- a/app/graphql/types/sluggable.rb +++ b/app/graphql/types/sluggable_type.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Types - module Sluggable + module SluggableType include BaseInterface description "Objects have a serialized slug for looking them up in the system and generating links without UUIDs" diff --git a/app/graphql/types/system_info_type.rb b/app/graphql/types/system_info_type.rb index c8c37a2e..d0188924 100644 --- a/app/graphql/types/system_info_type.rb +++ b/app/graphql/types/system_info_type.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true module Types - # A utility service for looking up information about the WDP-API ecosystem. + # A utility service for looking up information about the Meru-API ecosystem. class SystemInfoType < Types::BaseObject description <<~TEXT - A helper field that can look up various information about the WDP-API Ecosystem. + A helper field that can look up various information about the Meru-API Ecosystem. TEXT field :entity_hierarchy_exists, Boolean, null: false do diff --git a/app/graphql/types/template_entity_list_type.rb b/app/graphql/types/template_entity_list_type.rb index 9159a2b3..0d1b1a56 100644 --- a/app/graphql/types/template_entity_list_type.rb +++ b/app/graphql/types/template_entity_list_type.rb @@ -31,7 +31,7 @@ class TemplateEntityListType < Types::BaseObject TEXT end - field :entities, ["::Types::AnyEntityType", { null: false }], null: false, method: :valid_entities do + field :entities, ["::Types::EntityType", { null: false }], null: false, method: :valid_entities do description <<~TEXT The actual entity records within this list. diff --git a/app/graphql/types/template_has_ordering_pair_type.rb b/app/graphql/types/template_has_ordering_pair_type.rb index 7543ebdd..a75ecf34 100644 --- a/app/graphql/types/template_has_ordering_pair_type.rb +++ b/app/graphql/types/template_has_ordering_pair_type.rb @@ -20,11 +20,11 @@ module TemplateHasOrderingPairType load_association! :prev_sibling load_association! :next_sibling - # @return [Promise(Templates::OrderingPair)] + # @return [Templates::OrderingPair] def ordering_pair assocs = [ordering, ordering_entry_count, prev_sibling, next_sibling] - Promise.all(assocs).then do + maybe_await(assocs).then do object.ordering_pair end end diff --git a/app/graphql/types/template_instance_type.rb b/app/graphql/types/template_instance_type.rb index 601c2169..5fba4b81 100644 --- a/app/graphql/types/template_instance_type.rb +++ b/app/graphql/types/template_instance_type.rb @@ -14,7 +14,7 @@ module TemplateInstanceType implements Types::RenderableType - field :entity, Types::AnyEntityType, null: false do + field :entity, Types::EntityType, null: false do description <<~TEXT The associated entity for this template instance. TEXT diff --git a/app/jobs/access/calculate_all_granted_permissions_job.rb b/app/jobs/access/calculate_all_granted_permissions_job.rb index 96238985..71374a7c 100644 --- a/app/jobs/access/calculate_all_granted_permissions_job.rb +++ b/app/jobs/access/calculate_all_granted_permissions_job.rb @@ -14,7 +14,7 @@ class CalculateAllGrantedPermissionsJob < ApplicationJob unique_job! by: :job - queue_as :permissions + queue_as :default # @param [String] cursor # @return [void] diff --git a/app/jobs/access/calculate_granted_permissions_job.rb b/app/jobs/access/calculate_granted_permissions_job.rb index 5a136278..59054f7e 100644 --- a/app/jobs/access/calculate_granted_permissions_job.rb +++ b/app/jobs/access/calculate_granted_permissions_job.rb @@ -5,7 +5,7 @@ module Access # # @see Access::CalculateGrantedPermissions class CalculateGrantedPermissionsJob < ApplicationJob - queue_as :permissions + queue_as :default # @param [AccessGrant] access_grant # @return [void] diff --git a/app/jobs/access/calculate_role_granted_permissions_job.rb b/app/jobs/access/calculate_role_granted_permissions_job.rb index 150650df..8b3c8a86 100644 --- a/app/jobs/access/calculate_role_granted_permissions_job.rb +++ b/app/jobs/access/calculate_role_granted_permissions_job.rb @@ -9,7 +9,7 @@ class CalculateRoleGrantedPermissionsJob < ApplicationJob unique_job! by: :all_args - queue_as :permissions + queue_as :default # @param [String] cursor # @return [void] diff --git a/app/jobs/access/enforce_assignments_job.rb b/app/jobs/access/enforce_assignments_job.rb index 2f185ad5..556556eb 100644 --- a/app/jobs/access/enforce_assignments_job.rb +++ b/app/jobs/access/enforce_assignments_job.rb @@ -3,7 +3,7 @@ module Access # @see Access::EnforceAssignments class EnforceAssignmentsJob < ApplicationJob - queue_as :permissions + queue_as :default unique_job! by: :job diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index 7583c997..8bcb5013 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -9,18 +9,10 @@ class ApplicationJob < ActiveJob::Base include GoodJob::ActiveJobExtensions::Concurrency - JobTimeoutError = Class.new(StandardError) - - defines :max_runtime, type: Support::Types.Instance(ActiveSupport::Duration) - - max_runtime 10.minutes - retry_on ActiveRecord::QueryCanceled, wait: :polynomially_longer, attempts: 10 retry_on ActiveRecord::StatementInvalid, wait: :polynomially_longer, attempts: 10 - retry_on JobTimeoutError, wait: :polynomially_longer, attempts: 10 - retry_on GoodJob::InterruptError, wait: :polynomially_longer, attempts: Float::INFINITY retry_on GoodJob::ActiveJobExtensions::Concurrency::ThrottleExceededError, wait: :polynomially_longer, attempts: 10 @@ -36,13 +28,6 @@ class ApplicationJob < ActiveJob::Base discard_on NameError unless Rails.env.test? # :nocov: - around_perform do |job, block| - # Timeout jobs after 10 minutes - Timeout.timeout(job.class.max_runtime, JobTimeoutError) do - block.call - end - end - def call_operation!(name, ...) MeruAPI::Container[name].call(...).value! end diff --git a/app/jobs/cache_warmers/run_job.rb b/app/jobs/cache_warmers/run_job.rb index e882d892..c582be71 100644 --- a/app/jobs/cache_warmers/run_job.rb +++ b/app/jobs/cache_warmers/run_job.rb @@ -5,7 +5,9 @@ module CacheWarmers # @see CacheWarmers::Run # @see CacheWarmers::Runner class RunJob < ApplicationJob - queue_as :cache_warming + queue_as :default + + queue_with_priority 800 unique_job! by: :first_arg diff --git a/app/jobs/entities/revalidate_frontend_cache_job.rb b/app/jobs/entities/revalidate_frontend_cache_job.rb index 9c7a97f3..f5eab5f3 100644 --- a/app/jobs/entities/revalidate_frontend_cache_job.rb +++ b/app/jobs/entities/revalidate_frontend_cache_job.rb @@ -3,10 +3,12 @@ module Entities # @see Entities::RevalidateFrontendCache class RevalidateFrontendCacheJob < ApplicationJob - queue_as :revalidations + queue_as :default + + queue_with_priority 400 good_job_control_concurrency_with( - total_limit: 1, + total_limit: 3, # :nocov: key: -> { "#{self.class.name}-#{arguments.first.id}" } # :nocov: diff --git a/app/jobs/local/update_web_maxmind_job.rb b/app/jobs/local/update_web_maxmind_job.rb deleted file mode 100644 index 545744f2..00000000 --- a/app/jobs/local/update_web_maxmind_job.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module Local - class UpdateWebMaxmindJob - include SuckerPunch::Job - - max_jobs 2 - - def perform - MeruAPI::Container["local.update_web_maxmind"].().value! - end - end -end diff --git a/app/jobs/testing/quick_job.rb b/app/jobs/testing/quick_job.rb deleted file mode 100644 index 7a8bcdca..00000000 --- a/app/jobs/testing/quick_job.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -module Testing - # This never runs in production. We just use it to test - # our job timeout logic. - class QuickJob < ApplicationJob - max_runtime 1.second - - discard_on JobTimeoutError - - def perform - sleep 5.0 - end - end -end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index f980e39d..293ae806 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -# @abstract The base model for WDP-API. +# @abstract The base model for Meru-API. class ApplicationRecord < ActiveRecord::Base self.abstract_class = true diff --git a/app/models/request_query.rb b/app/models/request_query.rb index b646efd3..f10357c4 100644 --- a/app/models/request_query.rb +++ b/app/models/request_query.rb @@ -6,6 +6,7 @@ class RequestQuery < ApplicationRecord pg_enum! :kind, as: :request_query_kind, default: :query, allow_blank: false + has_many :request_steps, inverse_of: :request_query, dependent: :delete_all has_many :request_timings, inverse_of: :request_query, dependent: :delete_all before_validation :derive_query_info! @@ -30,10 +31,13 @@ def derive_query_info! self.operation_name = operation_name.presence || definition.name rescue GraphQL::ParseError + # :nocov: errors.add(:query, "is not valid GraphQL") + # :nocov: end def set_kind_from_definition!(definition) + # :nocov: case definition.try(:operation_type) in "query" self.kind = :query @@ -44,5 +48,6 @@ def set_kind_from_definition!(definition) else self.kind = :other end + # :nocov: end end diff --git a/app/models/request_step.rb b/app/models/request_step.rb new file mode 100644 index 00000000..f159d55c --- /dev/null +++ b/app/models/request_step.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# A step in the processing of a GraphQL request, recorded for performance monitoring. +class RequestStep < ApplicationRecord + include HasEphemeralSystemSlug + include TimestampScopes + + belongs_to :request_query, inverse_of: :request_steps +end diff --git a/app/operations/local/update_web_maxmind.rb b/app/operations/local/update_web_maxmind.rb deleted file mode 100644 index d1e45b41..00000000 --- a/app/operations/local/update_web_maxmind.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module Local - class UpdateWebMaxmind - include Dry::Monads[:result] - - GEOIPUPDATE_PATH = "/usr/local/bin/geoipupdate" - - def call - env = GeocoderConfig.to_env - - output, pid = Open3.capture2e(env, GEOIPUPDATE_PATH) - - Rails.logger.tagged("geoipupdate").debug(output) - - # :nocov: - return Failure[:update_failed, output] unless pid.success? - # :nocov: - - Success() - end - end -end diff --git a/app/services/contributors/finder.rb b/app/services/contributors/finder.rb index 99d04d35..b771cc99 100644 --- a/app/services/contributors/finder.rb +++ b/app/services/contributors/finder.rb @@ -3,46 +3,49 @@ module Contributors # @see Contributors::Lookup class Finder + include Dry::Monads[:result, :do] include Dry::Initializer[undefined: false].define -> do option :field, Contributors::Types::LookupField option :value, Contributors::Types::String option :order, Contributors::Types::LookupOrder, default: proc { "RECENT" } end - include Dry::Monads[:result, :do] + # @return [Hash] + attr_reader :options def call return Failure[:invalid, "must provide a non-blank value"] if value.blank? - loader = yield build_loader + yield build_options - Success loader.load value + Success options end private - def build_loader - column = yield derive_column_from_field + # @return [Dry::Monads::Result] + def build_options + find_by = yield derive_find_by_from_field order = yield derive_order_expression - options = { column:, order: } - - loader = Support::Loaders::RecordLoader.for(::Contributor, **options) + @options = { find_by:, order:, value: } - Success loader + Success() end - def derive_column_from_field + def derive_find_by_from_field Success field end def derive_order_expression + # :nocov: unless order == "OLDEST" Success(created_at: :desc) else Success(created_at: :asc) end + # :nocov: end end end diff --git a/app/services/custom_dataloader.rb b/app/services/custom_dataloader.rb new file mode 100644 index 00000000..d831bef6 --- /dev/null +++ b/app/services/custom_dataloader.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Custom Dataloader that manages fiber state with connections for AR & GQL. +class CustomDataloader < GraphQL::Dataloader + self.default_fiber_limit = 10 + + self.default_nonblocking = false + + def get_fiber_variables + vars = super + + # Collect the current connection config to pass on: + vars[:connected_to] = { + role: ActiveRecord::Base.current_role, + shard: ActiveRecord::Base.current_shard, + prevent_writes: ActiveRecord::Base.current_preventing_writes + } + + vars + end + + def set_fiber_variables(vars) + connection_config = vars.delete(:connected_to) + + dry_effects_stack = vars.delete(:dry_effects_stack) + + Dry::Effects::Frame.stack = dry_effects_stack || Dry::Effects::Stack.new + + # Reset connection config from the parent fiber: + ActiveRecord::Base.connecting_to(**connection_config) + + super + end + + def cleanup_fiber + super + + # Release the current connection + ActiveRecord::Base.connection_pool.release_connection + end +end diff --git a/app/services/entities/layouts_checker.rb b/app/services/entities/layouts_checker.rb index 1632d561..68677f27 100644 --- a/app/services/entities/layouts_checker.rb +++ b/app/services/entities/layouts_checker.rb @@ -9,8 +9,6 @@ class LayoutsChecker < Support::HookBased::Actor standard_execution! - around_execute :acquire_check_lock! - # A freshly-loaded record with nothing else attached to it # that we will use to check. # @@ -46,18 +44,15 @@ def call yield entity.render_layouts - @rendered = true + if entity.stale? + # Something went wrong with the re-rerendering process. + # Mark it as invalid again so it can be retried later. + entity.invalidate_layouts! + else + @rendered = true + end super end - - private - - # @return [void] - def acquire_check_lock! - original_entity.class.with_advisory_lock!("check_layouts/#{original_entity.id}", disable_query_cache: true, timeout_seconds: 30) do - yield - end - end end end diff --git a/app/services/entities/layouts_renderer.rb b/app/services/entities/layouts_renderer.rb index c57535be..cdd7f409 100644 --- a/app/services/entities/layouts_renderer.rb +++ b/app/services/entities/layouts_renderer.rb @@ -57,8 +57,10 @@ def call # @return [void] def acquire_render_lock! - entity.class.with_advisory_lock!(entity.render_lock_key, disable_query_cache: true, timeout_seconds: 30) do - yield + entity.class.transaction do + entity.class.with_advisory_lock!(entity.render_lock_key, disable_query_cache: true, timeout_seconds: 30, transaction: true) do + yield + end end end diff --git a/app/services/frontend/cache/abstract_revalidator.rb b/app/services/frontend/cache/abstract_revalidator.rb index 98b38495..8168a109 100644 --- a/app/services/frontend/cache/abstract_revalidator.rb +++ b/app/services/frontend/cache/abstract_revalidator.rb @@ -32,7 +32,7 @@ class AbstractRevalidator < Support::HookBased::Actor # @return [String] defines :uri_path, type: Types::String - base_url LocationsConfig.frontend + base_url LocationsConfig.frontend_request kind "instance" diff --git a/app/services/isolated.rb b/app/services/isolated.rb new file mode 100644 index 00000000..446df2f3 --- /dev/null +++ b/app/services/isolated.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class Isolated + def call(&block) + Async do + Thread.new do + block.call + end.value + end + end + + class << self + def isolate!(&) + new.call(&) + end + end +end diff --git a/app/services/loaders/entity_layouts_loader.rb b/app/services/loaders/entity_layouts_loader.rb index 46a3051f..be48550d 100644 --- a/app/services/loaders/entity_layouts_loader.rb +++ b/app/services/loaders/entity_layouts_loader.rb @@ -6,7 +6,9 @@ class EntityLayoutsLoader < GraphQL::Batch::Loader # @return [void] def perform(entities) entities.each do |entity| - fulfill(entity, entity.check_layouts.value_or(nil)) + layouts = entity.check_layouts.value_or(nil) + + fulfill(entity, layouts) end end diff --git a/app/services/resolvers/asset_resolver.rb b/app/services/resolvers/asset_resolver.rb index b39c00a4..c63c67b6 100644 --- a/app/services/resolvers/asset_resolver.rb +++ b/app/services/resolvers/asset_resolver.rb @@ -5,7 +5,7 @@ class AssetResolver < AbstractResolver include Resolvers::Enhancements::PageBasedPagination include Resolvers::SimplyOrdered - type Types::AnyAssetType.connection_type, null: false + type Types::AssetType.connection_type, null: false scope { object.present? ? object.assets : Asset.none } diff --git a/app/services/resolvers/search_result_resolver.rb b/app/services/resolvers/search_result_resolver.rb index 4cf545e4..2afdf309 100644 --- a/app/services/resolvers/search_result_resolver.rb +++ b/app/services/resolvers/search_result_resolver.rb @@ -25,7 +25,9 @@ class SearchResultResolver < AbstractResolver If _none_ of these are set, the search will be considered empty, and return 0 results. TEXT - scope { object.base_relation.all } + scope do + object.base_relation.all + end PREDICATES_DESC = <<~TEXT The predicates to search for, if any. diff --git a/app/services/schemas/properties/scalar/base.rb b/app/services/schemas/properties/scalar/base.rb index 724d35e1..8e08b177 100644 --- a/app/services/schemas/properties/scalar/base.rb +++ b/app/services/schemas/properties/scalar/base.rb @@ -191,6 +191,10 @@ def prefixed_path_with_type "props.#{full_path}##{type}" end + # @api private + # @return [void] + def properties=(*); end + # @!group Schema / Contract compilation def add_to_schema!(context) diff --git a/app/services/searching/encodes_join.rb b/app/services/searching/encodes_join.rb new file mode 100644 index 00000000..d3dbc55b --- /dev/null +++ b/app/services/searching/encodes_join.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Searching + # A thin wrapper around the join name encoder operation. + module EncodesJoin + # @param [String] path + # @return [String] + def encode_join(path) + MeruAPI::Container["searching.compilation.encode_join_name"].(path) + end + end +end diff --git a/app/services/searching/operator.rb b/app/services/searching/operator.rb index 8eaf8603..6b72bb45 100644 --- a/app/services/searching/operator.rb +++ b/app/services/searching/operator.rb @@ -5,9 +5,9 @@ module Searching class Operator extend ActiveModel::Callbacks + include Searching::EncodesJoin + include Dry::Core::Memoizable - include Dry::Effects.State(:joins) - include Dry::Effects.Resolve(:encode_join) include Dry::Effects::Handler.Interrupt(:skip, as: :catch_skip) include Dry::Effects.Interrupt(:skip) include Dry::Initializer[undefined: false].define -> do @@ -23,25 +23,29 @@ class Operator delegate :value_column, :type, to: :property, allow_nil: true, prefix: true + # @return [Hash] + attr_reader :joins + + # @return [Arel::Expressions, nil] attr_reader :expression # @return [Arel::Expressions, nil] - def call - skipped, _ = catch_skip do - run_callbacks :prepare do - prepare! - end + def call(joins: {}) + @joins = joins - run_callbacks :compile do - @expression = compile - end + run_callbacks :prepare do + prepare! end - # :nocov: - return if skipped - # :nocov: + run_callbacks :compile do + @expression = compile + end return @expression + rescue Searching::Skip + # :nocov: + return nil + # :nocov: end # @!attribute [r] property @@ -54,7 +58,7 @@ def call MeruAPI::Container["schemas.properties.parse_path"].(left).value_or(nil) else # :nocov: - skip + raise Searching::Skip # :nocov: end end @@ -135,13 +139,22 @@ def compile # :nocov: end + # @param [Array] operators + # @return [Arel::Expressions, nil] + def compile_nested(operators) + operators.map { _1.(joins:) }.compact_blank.reduce(&:and) + end + # @return [Arel::Nodes::TableAlias] def join_for(path) - expr = joins.compute_if_absent path do - join_name = encode_join.(path) + joins[path] ||= + begin + join_name = encode_join(path) - yield join_name - end + yield join_name + end + + expr = joins.fetch(path) expr.left end diff --git a/app/services/searching/operators/and.rb b/app/services/searching/operators/and.rb index 3b535d91..4957ae9f 100644 --- a/app/services/searching/operators/and.rb +++ b/app/services/searching/operators/and.rb @@ -5,8 +5,8 @@ module Operators class And < Searching::Operator def compile # :nocov: - left_expr = left.map(&:call).reduce(&:and) - right_expr = right.map(&:call).reduce(&:and) + left_expr = compile_nested(left) + right_expr = compile_nested(right) if left_expr.present? && right_expr.present? left_expr.and(right_expr) @@ -15,7 +15,7 @@ def compile elsif right_expr right_expr else - skip + raise Searching::Skip end # :nocov: end diff --git a/app/services/searching/operators/or.rb b/app/services/searching/operators/or.rb index c83ee9d9..99ffa6e6 100644 --- a/app/services/searching/operators/or.rb +++ b/app/services/searching/operators/or.rb @@ -5,8 +5,8 @@ module Operators class Or < Searching::Operator def compile # :nocov: - left_expr = left.map(&:call).reduce(&:and) - right_expr = right.map(&:call).reduce(&:and) + left_expr = compile_nested(left) + right_expr = compile_nested(right) if left_expr.present? && right_expr.present? left_expr.or(right_expr) @@ -15,7 +15,7 @@ def compile elsif right_expr right_expr else - skip + raise Searching::Skip end # :nocov: end diff --git a/app/services/searching/predicate_compiler.rb b/app/services/searching/predicate_compiler.rb index c63fbec7..bbf4c0c6 100644 --- a/app/services/searching/predicate_compiler.rb +++ b/app/services/searching/predicate_compiler.rb @@ -5,19 +5,21 @@ module Searching # @api private class PredicateCompiler include Dry::Effects::Handler.State(:joins) - include Dry::Effects::Handler.Resolve include Dry::Initializer[undefined: false].define -> do param :predicates, Searching::Operator::List.optional, default: proc { [] } - option :encode_join, Searching::Types::JoinEncoder, default: proc { MeruAPI::Container["searching.compilation.encode_join_name"] } - option :scope, Searching::Types::Interface(:all), default: proc { Entity.all } end + # @return [Hash] + attr_reader :joins + # @return [ActiveRecord::Relation<::Entity>, nil] def call return nil if predicates.blank? + @joins ||= {} + compiled = compile return nil if compiled[:conditions].blank? @@ -26,7 +28,7 @@ def call query.where! compiled[:conditions] - query.joins!(*compiled[:joins].values) if compiled[:joins].any? + query.joins!(*joins.values) if joins.any? return query.apply_order_to_exclude_duplicate_links end @@ -35,26 +37,18 @@ def call def compile wrap_predicate_compilation do |compiled| - compiled[:conditions] = predicates.map(&:call).compact.reduce(nil) do |expr, pred| + compiled[:conditions] = predicates.map { _1.call(joins:) }.compact.reduce(nil) do |expr, pred| expr.present? ? expr.and(pred) : pred end end end def wrap_predicate_compilation - joins = Concurrent::Map.new - - expression = {} - - joins, _ = with_joins(joins) do - provide(encode_join:) do - yield expression - end - end + compiled = {} - expression[:joins] = joins.each_pair.to_h + yield compiled - return expression + return compiled end end end diff --git a/app/services/searching/skip.rb b/app/services/searching/skip.rb new file mode 100644 index 00000000..4284f406 --- /dev/null +++ b/app/services/searching/skip.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Searching + class Skip < StandardError; end +end diff --git a/app/services/utility/request_timer.rb b/app/services/utility/request_timer.rb deleted file mode 100644 index 523a75cc..00000000 --- a/app/services/utility/request_timer.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module Utility - class RequestTimer < Support::FlexibleStruct - extend ActiveModel::Callbacks - - attribute :query, Support::Types::Coercible::String - attribute :operation_name, Support::Types::String.optional - attribute :variables, Support::Types::Hash.fallback { {} } - - define_model_callbacks :measure - - around_measure :record_duration! - - # @return [Integer] - attr_reader :duration - - # @return [RequestQuery] - attr_reader :request_query - - # @return [RequestTiming] - attr_reader :request_timing - - # @return [void] - def measure! - @request_query ||= load_request_query! - - run_callbacks :measure do - yield - end - end - - private - - # @return [RequestQuery] - def load_request_query! - RequestQuery.where(query:).first_or_create! do |rq| - rq.operation_name = operation_name - end - end - - # @return [void] - def record_duration! - @duration = AbsoluteTime.realtime do - yield - end - - @request_query.request_timings.create(variables:, duration:) - end - - class << self - def measure!(**kwargs, &) - new(**kwargs).measure!(&) - end - end - end -end diff --git a/bin/puma b/bin/pitchfork similarity index 56% rename from bin/puma rename to bin/pitchfork index 880935b2..0ee595cf 100755 --- a/bin/puma +++ b/bin/pitchfork @@ -4,18 +4,16 @@ # # This file was generated by Bundler. # -# The application 'puma' is installed as part of a gem, and +# The application 'pitchfork' is installed as part of a gem, and # this file is here to facilitate running it. # -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) -bundle_binstub = File.expand_path("../bundle", __FILE__) +bundle_binstub = File.expand_path("bundle", __dir__) if File.file?(bundle_binstub) - if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ + if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. @@ -26,4 +24,4 @@ end require "rubygems" require "bundler/setup" -load Gem.bin_path("puma", "puma") +load Gem.bin_path("pitchfork", "pitchfork") diff --git a/bin/pumactl b/bin/pumactl deleted file mode 100755 index b698e6e9..00000000 --- a/bin/pumactl +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -# -# This file was generated by Bundler. -# -# The application 'pumactl' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require "pathname" -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", - Pathname.new(__FILE__).realpath) - -bundle_binstub = File.expand_path("../bundle", __FILE__) - -if File.file?(bundle_binstub) - if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/ - load(bundle_binstub) - else - abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. -Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") - end -end - -require "rubygems" -require "bundler/setup" - -load Gem.bin_path("puma", "pumactl") diff --git a/config/application.rb b/config/application.rb index 5734ce9e..0559b988 100644 --- a/config/application.rb +++ b/config/application.rb @@ -100,6 +100,7 @@ class Application < Rails::Application if Rails.env.development? config.hosts << "www.example.com" + config.hosts << "host.docker.internal" config.hosts << /[a-z0-9.-]+\.ngrok\.io/ config.hosts << /[a-z0-9.-]+\.ngrok-free\.app/ end diff --git a/config/configs/meru_config.rb b/config/configs/meru_config.rb index 6cb0a9ad..dc7eb8dd 100644 --- a/config/configs/meru_config.rb +++ b/config/configs/meru_config.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class MeruConfig < ApplicationConfig - attr_config tenant_id: "meru", tenant_name: "Meru", include_testing_schemas: false, serialize_rendering: false + attr_config tenant_id: "meru", tenant_name: "Meru", include_testing_schemas: false, serialize_rendering: false, + experimental_dataloader: false, pool_size: 20 - coerce_types include_testing_schemas: :boolean, serialize_rendering: :boolean + coerce_types experimental_dataloader: :boolean, include_testing_schemas: :boolean, serialize_rendering: :boolean end diff --git a/config/database.yml b/config/database.yml index 18718f9c..0b7a4795 100644 --- a/config/database.yml +++ b/config/database.yml @@ -23,7 +23,7 @@ default: &default # For details on connection pooling, see Rails configuration guide # https://guides.rubyonrails.org/configuring.html#database-pooling - pool: 15 + pool: <%= MeruConfig.pool_size %> development: <<: *default diff --git a/config/initializers/001_fibers.rb b/config/initializers/001_fibers.rb new file mode 100644 index 00000000..909402ed --- /dev/null +++ b/config/initializers/001_fibers.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# GLOBAL_SCHEDULER = Async::Scheduler.new + +# Fiber.set_scheduler(GLOBAL_SCHEDULER) diff --git a/config/initializers/700_shrine_tus.rb b/config/initializers/700_shrine_tus.rb index ee587e4f..b4c783f8 100644 --- a/config/initializers/700_shrine_tus.rb +++ b/config/initializers/700_shrine_tus.rb @@ -1,10 +1,5 @@ # frozen_string_literal: true -require "shrine/storage/memory" -require "shrine/storage/s3" -require "shrine/storage/url" -require "tus/storage/s3" - require Rails.root.join("lib", "middleware", "tus_uploader") aws_credentials = S3Config.to_h diff --git a/config/initializers/900_good_job.rb b/config/initializers/900_good_job.rb index 893144e3..f5871e6b 100644 --- a/config/initializers/900_good_job.rb +++ b/config/initializers/900_good_job.rb @@ -7,10 +7,9 @@ queues = [ "maintenance:1", "rendering:1", - "+revalidations,cache_warming:1", "+purging,hierarchies,entities,orderings,invalidations,layouts:2", "+harvest_pruning,extraction,harvesting,asset_fetching:2", - "permissions,processing,default,mailers,ahoy:2", + "default,mailers,ahoy:2", ].join(?;) config.good_job.preserve_job_records = :on_unhandled_error @@ -20,10 +19,9 @@ config.good_job.execution_mode = Rails.env.test? ? :inline : :external # :nocov: config.good_job.queues = queues - config.good_job.max_threads = 10 + config.good_job.max_threads = 4 config.good_job.poll_interval = 10 # seconds config.good_job.shutdown_timeout = 25 # seconds - config.good_job.advisory_lock_heartbeat = true config.good_job.enable_cron = !Rails.env.test? config.good_job.enable_listen_notify = true config.good_job.enable_pauses = true diff --git a/config/initializers/950_autotuner.rb b/config/initializers/950_autotuner.rb index 61c6bfce..6f3f027a 100644 --- a/config/initializers/950_autotuner.rb +++ b/config/initializers/950_autotuner.rb @@ -22,9 +22,9 @@ # On Datadog this would be the distribution type. On Prometheus this would be # the histogram type. Autotuner.metrics_reporter = proc do |metrics| - tuples = metrics.map { |name, value| { name:, value: value.to_i } } + # tuples = metrics.map { |name, value| { name:, value: value.to_i } } - ::TunerMetric.insert_all(tuples) + # ::TunerMetric.insert_all(tuples) rescue Exception # intentionally left blank end diff --git a/config/initializers/960_gql.rb b/config/initializers/960_gql.rb new file mode 100644 index 00000000..98b199a2 --- /dev/null +++ b/config/initializers/960_gql.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +ActiveSupport::Notifications.subscribe(/graphql/) do |event| + name = event.name + + duration = event.duration.round(2) + + # :nocov: + next if duration < 10.0 + # :nocov: + + operation_name = Support::Requests::Current.graphql_operation_name + + query = event.payload[:query] + + field = event.payload[:field] + + current_path = query.try(:context).try(:current_path).try(:join, ?.) || field.try(:path) + + step = { + name:, + current_path:, + duration: event.duration, + } + + Support::Requests::Current.graphql_steps << step + + tags = ["graphql", operation_name, name] + + tags << current_path if name == "graphql.execute_field" && current_path.present? + + prefix = tags.compact_blank.map { |tag| "[#{tag}]" }.join + + Rails.logger.info("#{prefix} Completed in #{duration}ms") +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 1481ae84..ec2d95cd 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -5,6 +5,7 @@ # Configure sensitive parameters which will be filtered from the log file. Rails.application.config.filter_parameters += [ :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, + :query, :raw_source, :raw_metadata_source, :xml_source, :xml_metadata_source, :json_source, :json_metadata_source, diff --git a/config/locales/api.en.yml b/config/locales/api.en.yml index 9a7357f6..210fdabe 100644 --- a/config/locales/api.en.yml +++ b/config/locales/api.en.yml @@ -1,6 +1,6 @@ # These translations really only exist for API concerns, # generating error messages for tests and local development, -# etc. When WDP-API has its content translated, the keys in +# etc. When Meru-API has its content translated, the keys in # this document do not need to be included in that process. en: diff --git a/config/pitchfork.rb b/config/pitchfork.rb new file mode 100644 index 00000000..b449a9d8 --- /dev/null +++ b/config/pitchfork.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module MeruPitchfork + WEB_CONCURRENCY = ENV.fetch("WEB_CONCURRENCY", Etc.nprocessors).to_i + + # Double the number of processes to provide better throughput in DO + # WEB_CONCURRENCY=2 would result in 4 processes + BASE_PROCS = WEB_CONCURRENCY * 2 + + # Ensure there is always at least 1 process (dev uses WEB_CONCURRENCY=0) + PROCESS_COUNT = BASE_PROCS.clamp(1, 12) + + PORT = ENV.fetch("PORT", 8080).to_i +end + +worker_processes MeruPitchfork::PROCESS_COUNT + +listen MeruPitchfork::PORT, tcp_nopush: true, reuseport: true + +timeout 60 + +max_consecutive_spawn_errors 5 + +# Should improve performance +rewindable_input false + +refork_after [50, 100, 1000] + +before_fork do |server| + ActiveSupport.on_load(:active_record) do + ActiveRecord::Base.connection.disconnect! + end +end + +after_worker_fork do |server, worker| + ActiveSupport.on_load(:active_record) do + ActiveRecord::Base.establish_connection + end +end + +after_mold_fork do |server, mold| + Process.warmup +end diff --git a/config/puma.rb b/config/puma.rb deleted file mode 100644 index 8c91cbca..00000000 --- a/config/puma.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -# Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers: a minimum and maximum. -# Any libraries that use thread pools should be configured to match -# the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum; this matches the default thread size of Active Record. -# -max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } -min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } -threads min_threads_count, max_threads_count - -# Specifies the `worker_timeout` threshold that Puma will use to wait before -# terminating a worker in development environments. -# -worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development" - -# Specifies the `port` that Puma will listen on to receive requests; default is 3000. -# -port ENV.fetch("PORT") { 6333 } - -# Specifies the `environment` that Puma will run in. -# -environment ENV.fetch("RAILS_ENV") { "development" } - -# Specifies the `pidfile` that Puma will use. -pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } - -# Specifies the number of `workers` to boot in clustered mode. -# Workers are forked web server processes. If using threads and workers together -# the concurrency of the application would be max `threads` * `workers`. -# Workers do not work on JRuby or Windows (both of which do not support -# processes). -# -workers ENV.fetch("WEB_CONCURRENCY") { 2 } - -# Use the `preload_app!` method when specifying a `workers` number. -# This directive tells Puma to first boot the application and load code -# before forking the application. This takes advantage of Copy On Write -# process behavior so workers use less memory. -# -preload_app! - -plugin "rufus-scheduler" - -# Allow puma to be restarted by `rails restart` command. -plugin :tmp_restart - -fork_worker 1000 -wait_for_less_busy_worker 0.001 - -before_fork do - 3.times { GC.start } - - GC.compact -end - -on_worker_fork do - ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.connection.disconnect! - end -end - -on_worker_boot do - # Worker specific setup for Rails 4.1+ - # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot - ActiveSupport.on_load(:active_record) do - ActiveRecord::Base.establish_connection - end -end diff --git a/config/routes.rb b/config/routes.rb index b59a5ab6..939d0efb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,7 +29,7 @@ get "/ping", to: "status#ping" - get "/up", to: "status#ping", as: :health_check + get "/up" => "rails/health#show", as: :rails_health_check root to: "status#root" end diff --git a/config/scheduler.rb b/config/scheduler.rb deleted file mode 100644 index a004596e..00000000 --- a/config/scheduler.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -# This file is used by the puma-rufus-scheduler plugin and otherwise ignored. - -scheduler = Rufus::Scheduler.new - -scheduler.every "1w" do - Local::UpdateWebMaxmindJob.perform_async -rescue StandardError => e - warn e -end - -# This will attach scheduler thread to Puma's background thread. -scheduler.join diff --git a/db/migrate/20251114195823_create_request_steps.rb b/db/migrate/20251114195823_create_request_steps.rb new file mode 100644 index 00000000..24c1bd5c --- /dev/null +++ b/db/migrate/20251114195823_create_request_steps.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class CreateRequestSteps < ActiveRecord::Migration[7.2] + def change + change_table :request_timings do |t| + t.uuid :request_id, null: true + end + + create_table :request_steps, id: :uuid do |t| + t.references :request_query, type: :uuid, null: false, foreign_key: { on_delete: :cascade } + + t.uuid :request_id, null: true + + t.text :name, null: false + t.text :current_path, null: true + + t.decimal :duration, null: false + + t.timestamps null: false, default: -> { "CURRENT_TIMESTAMP" } + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 1072ee7a..1e44f92e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2883,17 +2883,17 @@ COMMENT ON OPERATOR public.# (NONE, public.variable_precision_date) IS 'Normaliz CREATE OPERATOR public.&& ( FUNCTION = public.vpdate_overlaps, - LEFTARG = public.variable_precision_date, + LEFTARG = daterange, RIGHTARG = public.variable_precision_date, COMMUTATOR = OPERATOR(public.&&) ); -- --- Name: OPERATOR && (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR && (daterange, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.&& (public.variable_precision_date, public.variable_precision_date) IS 'overlaps'; +COMMENT ON OPERATOR public.&& (daterange, public.variable_precision_date) IS 'overlaps'; -- @@ -2921,17 +2921,17 @@ COMMENT ON OPERATOR public.&& (public.variable_precision_date, daterange) IS 'ov CREATE OPERATOR public.&& ( FUNCTION = public.vpdate_overlaps, - LEFTARG = daterange, + LEFTARG = public.variable_precision_date, RIGHTARG = public.variable_precision_date, COMMUTATOR = OPERATOR(public.&&) ); -- --- Name: OPERATOR && (daterange, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR && (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.&& (daterange, public.variable_precision_date) IS 'overlaps'; +COMMENT ON OPERATOR public.&& (public.variable_precision_date, public.variable_precision_date) IS 'overlaps'; -- @@ -3022,18 +3022,6 @@ CREATE OPERATOR public.<> ( ); --- --- Name: <@; Type: OPERATOR; Schema: public; Owner: - --- - -CREATE OPERATOR public.<@ ( - FUNCTION = public.vpdate_contained_by, - LEFTARG = public.variable_precision_date, - RIGHTARG = public.variable_precision_date, - COMMUTATOR = OPERATOR(public.@>) -); - - -- -- Name: <@; Type: OPERATOR; Schema: public; Owner: - -- @@ -3084,6 +3072,18 @@ CREATE OPERATOR public.<@ ( COMMENT ON OPERATOR public.<@ (public.variable_precision_date, daterange) IS 'contained by'; +-- +-- Name: <@; Type: OPERATOR; Schema: public; Owner: - +-- + +CREATE OPERATOR public.<@ ( + FUNCTION = public.vpdate_contained_by, + LEFTARG = public.variable_precision_date, + RIGHTARG = public.variable_precision_date, + COMMUTATOR = OPERATOR(public.@>) +); + + -- -- Name: =; Type: OPERATOR; Schema: public; Owner: - -- @@ -3123,8 +3123,7 @@ CREATE OPERATOR public.>= ( CREATE OPERATOR public.?! ( FUNCTION = public.vpdate_precision_neq, LEFTARG = public.variable_precision_date, - RIGHTARG = public.variable_precision_date, - COMMUTATOR = OPERATOR(public.?!), + RIGHTARG = public.date_precision, NEGATOR = OPERATOR(public.?=), RESTRICT = neqsel, JOIN = neqjoinsel @@ -3132,10 +3131,10 @@ CREATE OPERATOR public.?! ( -- --- Name: OPERATOR ?! (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?! (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?! (public.variable_precision_date, public.variable_precision_date) IS 'A.precision <> B.precision'; +COMMENT ON OPERATOR public.?! (public.variable_precision_date, public.date_precision) IS 'A.precision <> B'; -- @@ -3145,7 +3144,8 @@ COMMENT ON OPERATOR public.?! (public.variable_precision_date, public.variable_p CREATE OPERATOR public.?! ( FUNCTION = public.vpdate_precision_neq, LEFTARG = public.variable_precision_date, - RIGHTARG = public.date_precision, + RIGHTARG = public.variable_precision_date, + COMMUTATOR = OPERATOR(public.?!), NEGATOR = OPERATOR(public.?=), RESTRICT = neqsel, JOIN = neqjoinsel @@ -3153,10 +3153,10 @@ CREATE OPERATOR public.?! ( -- --- Name: OPERATOR ?! (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?! (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?! (public.variable_precision_date, public.date_precision) IS 'A.precision <> B'; +COMMENT ON OPERATOR public.?! (public.variable_precision_date, public.variable_precision_date) IS 'A.precision <> B.precision'; -- @@ -3221,7 +3221,7 @@ COMMENT ON OPERATOR public.?- (public.variable_precision_date, public.variable_p CREATE OPERATOR public.?< ( FUNCTION = public.vpdate_precision_lt, LEFTARG = public.variable_precision_date, - RIGHTARG = public.variable_precision_date, + RIGHTARG = public.date_precision, COMMUTATOR = OPERATOR(public.?>), NEGATOR = OPERATOR(public.?>=), RESTRICT = scalarltsel, @@ -3230,10 +3230,10 @@ CREATE OPERATOR public.?< ( -- --- Name: OPERATOR ?< (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?< (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?< (public.variable_precision_date, public.variable_precision_date) IS 'A.precision < B.precision, with none always pushed to the end'; +COMMENT ON OPERATOR public.?< (public.variable_precision_date, public.date_precision) IS 'A.precision < B, with none always pushed to the end'; -- @@ -3243,7 +3243,7 @@ COMMENT ON OPERATOR public.?< (public.variable_precision_date, public.variable_p CREATE OPERATOR public.?< ( FUNCTION = public.vpdate_precision_lt, LEFTARG = public.variable_precision_date, - RIGHTARG = public.date_precision, + RIGHTARG = public.variable_precision_date, COMMUTATOR = OPERATOR(public.?>), NEGATOR = OPERATOR(public.?>=), RESTRICT = scalarltsel, @@ -3252,10 +3252,10 @@ CREATE OPERATOR public.?< ( -- --- Name: OPERATOR ?< (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?< (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?< (public.variable_precision_date, public.date_precision) IS 'A.precision < B, with none always pushed to the end'; +COMMENT ON OPERATOR public.?< (public.variable_precision_date, public.variable_precision_date) IS 'A.precision < B.precision, with none always pushed to the end'; -- @@ -3265,7 +3265,7 @@ COMMENT ON OPERATOR public.?< (public.variable_precision_date, public.date_preci CREATE OPERATOR public.?<= ( FUNCTION = public.vpdate_precision_le, LEFTARG = public.variable_precision_date, - RIGHTARG = public.variable_precision_date, + RIGHTARG = public.date_precision, COMMUTATOR = OPERATOR(public.?>=), NEGATOR = OPERATOR(public.?>), RESTRICT = scalarlesel, @@ -3274,10 +3274,10 @@ CREATE OPERATOR public.?<= ( -- --- Name: OPERATOR ?<= (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?<= (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?<= (public.variable_precision_date, public.variable_precision_date) IS 'A.precision <= B.precision, with none always pushed to the end'; +COMMENT ON OPERATOR public.?<= (public.variable_precision_date, public.date_precision) IS 'A.precision <= B, with none always pushed to the end'; -- @@ -3287,7 +3287,7 @@ COMMENT ON OPERATOR public.?<= (public.variable_precision_date, public.variable_ CREATE OPERATOR public.?<= ( FUNCTION = public.vpdate_precision_le, LEFTARG = public.variable_precision_date, - RIGHTARG = public.date_precision, + RIGHTARG = public.variable_precision_date, COMMUTATOR = OPERATOR(public.?>=), NEGATOR = OPERATOR(public.?>), RESTRICT = scalarlesel, @@ -3296,10 +3296,10 @@ CREATE OPERATOR public.?<= ( -- --- Name: OPERATOR ?<= (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?<= (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?<= (public.variable_precision_date, public.date_precision) IS 'A.precision <= B, with none always pushed to the end'; +COMMENT ON OPERATOR public.?<= (public.variable_precision_date, public.variable_precision_date) IS 'A.precision <= B.precision, with none always pushed to the end'; -- @@ -3309,21 +3309,18 @@ COMMENT ON OPERATOR public.?<= (public.variable_precision_date, public.date_prec CREATE OPERATOR public.?= ( FUNCTION = public.vpdate_precision_eq, LEFTARG = public.variable_precision_date, - RIGHTARG = public.variable_precision_date, - COMMUTATOR = OPERATOR(public.?=), + RIGHTARG = public.date_precision, NEGATOR = OPERATOR(public.?!), - MERGES, - HASHES, RESTRICT = eqsel, JOIN = eqjoinsel ); -- --- Name: OPERATOR ?= (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?= (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?= (public.variable_precision_date, public.variable_precision_date) IS 'A.precision = B.precision'; +COMMENT ON OPERATOR public.?= (public.variable_precision_date, public.date_precision) IS 'A.precision = B'; -- @@ -3333,18 +3330,21 @@ COMMENT ON OPERATOR public.?= (public.variable_precision_date, public.variable_p CREATE OPERATOR public.?= ( FUNCTION = public.vpdate_precision_eq, LEFTARG = public.variable_precision_date, - RIGHTARG = public.date_precision, + RIGHTARG = public.variable_precision_date, + COMMUTATOR = OPERATOR(public.?=), NEGATOR = OPERATOR(public.?!), + MERGES, + HASHES, RESTRICT = eqsel, JOIN = eqjoinsel ); -- --- Name: OPERATOR ?= (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?= (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?= (public.variable_precision_date, public.date_precision) IS 'A.precision = B'; +COMMENT ON OPERATOR public.?= (public.variable_precision_date, public.variable_precision_date) IS 'A.precision = B.precision'; -- @@ -3354,7 +3354,7 @@ COMMENT ON OPERATOR public.?= (public.variable_precision_date, public.date_preci CREATE OPERATOR public.?> ( FUNCTION = public.vpdate_precision_gt, LEFTARG = public.variable_precision_date, - RIGHTARG = public.variable_precision_date, + RIGHTARG = public.date_precision, COMMUTATOR = OPERATOR(public.?<), NEGATOR = OPERATOR(public.?<=), RESTRICT = scalargtsel, @@ -3363,10 +3363,10 @@ CREATE OPERATOR public.?> ( -- --- Name: OPERATOR ?> (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?> (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?> (public.variable_precision_date, public.variable_precision_date) IS 'A.precision > B.precision, with none always pushed to the end'; +COMMENT ON OPERATOR public.?> (public.variable_precision_date, public.date_precision) IS 'A.precision > B, with none always pushed to the end'; -- @@ -3376,7 +3376,7 @@ COMMENT ON OPERATOR public.?> (public.variable_precision_date, public.variable_p CREATE OPERATOR public.?> ( FUNCTION = public.vpdate_precision_gt, LEFTARG = public.variable_precision_date, - RIGHTARG = public.date_precision, + RIGHTARG = public.variable_precision_date, COMMUTATOR = OPERATOR(public.?<), NEGATOR = OPERATOR(public.?<=), RESTRICT = scalargtsel, @@ -3385,10 +3385,10 @@ CREATE OPERATOR public.?> ( -- --- Name: OPERATOR ?> (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?> (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?> (public.variable_precision_date, public.date_precision) IS 'A.precision > B, with none always pushed to the end'; +COMMENT ON OPERATOR public.?> (public.variable_precision_date, public.variable_precision_date) IS 'A.precision > B.precision, with none always pushed to the end'; -- @@ -3398,7 +3398,7 @@ COMMENT ON OPERATOR public.?> (public.variable_precision_date, public.date_preci CREATE OPERATOR public.?>= ( FUNCTION = public.vpdate_precision_ge, LEFTARG = public.variable_precision_date, - RIGHTARG = public.variable_precision_date, + RIGHTARG = public.date_precision, COMMUTATOR = OPERATOR(public.?<=), NEGATOR = OPERATOR(public.?<), RESTRICT = scalargesel, @@ -3407,10 +3407,10 @@ CREATE OPERATOR public.?>= ( -- --- Name: OPERATOR ?>= (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?>= (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?>= (public.variable_precision_date, public.variable_precision_date) IS 'A.precision >= B.precision, with none always pushed to the end'; +COMMENT ON OPERATOR public.?>= (public.variable_precision_date, public.date_precision) IS 'A.precision >= B, with none always pushed to the end'; -- @@ -3420,7 +3420,7 @@ COMMENT ON OPERATOR public.?>= (public.variable_precision_date, public.variable_ CREATE OPERATOR public.?>= ( FUNCTION = public.vpdate_precision_ge, LEFTARG = public.variable_precision_date, - RIGHTARG = public.date_precision, + RIGHTARG = public.variable_precision_date, COMMUTATOR = OPERATOR(public.?<=), NEGATOR = OPERATOR(public.?<), RESTRICT = scalargesel, @@ -3429,10 +3429,10 @@ CREATE OPERATOR public.?>= ( -- --- Name: OPERATOR ?>= (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ?>= (public.variable_precision_date, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.?>= (public.variable_precision_date, public.date_precision) IS 'A.precision >= B, with none always pushed to the end'; +COMMENT ON OPERATOR public.?>= (public.variable_precision_date, public.variable_precision_date) IS 'A.precision >= B.precision, with none always pushed to the end'; -- @@ -3524,7 +3524,7 @@ COMMENT ON OPERATOR public.@& (NONE, public.variable_precision_date) IS 'Transfo CREATE OPERATOR public.@> ( FUNCTION = public.vpdate_contains, - LEFTARG = public.variable_precision_date, + LEFTARG = daterange, RIGHTARG = public.variable_precision_date, COMMUTATOR = OPERATOR(public.<@) ); @@ -3560,7 +3560,7 @@ CREATE OPERATOR public.@> ( CREATE OPERATOR public.@> ( FUNCTION = public.vpdate_contains, - LEFTARG = daterange, + LEFTARG = public.variable_precision_date, RIGHTARG = public.variable_precision_date, COMMUTATOR = OPERATOR(public.<@) ); @@ -3626,16 +3626,16 @@ COMMENT ON OPERATOR public.~>? (public.date_precision, public.date_precision) IS CREATE OPERATOR public.~>? ( FUNCTION = public.vpdate_cmp_precision, - LEFTARG = public.variable_precision_date, - RIGHTARG = public.date_precision + LEFTARG = public.date_precision, + RIGHTARG = public.variable_precision_date ); -- --- Name: OPERATOR ~>? (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ~>? (public.date_precision, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.~>? (public.variable_precision_date, public.date_precision) IS 'Compare variable precision dates by precision *only*'; +COMMENT ON OPERATOR public.~>? (public.date_precision, public.variable_precision_date) IS 'Compare variable precision dates by precision *only*'; -- @@ -3644,16 +3644,16 @@ COMMENT ON OPERATOR public.~>? (public.variable_precision_date, public.date_prec CREATE OPERATOR public.~>? ( FUNCTION = public.vpdate_cmp_precision, - LEFTARG = public.date_precision, - RIGHTARG = public.variable_precision_date + LEFTARG = public.variable_precision_date, + RIGHTARG = public.date_precision ); -- --- Name: OPERATOR ~>? (public.date_precision, public.variable_precision_date); Type: COMMENT; Schema: public; Owner: - +-- Name: OPERATOR ~>? (public.variable_precision_date, public.date_precision); Type: COMMENT; Schema: public; Owner: - -- -COMMENT ON OPERATOR public.~>? (public.date_precision, public.variable_precision_date) IS 'Compare variable precision dates by precision *only*'; +COMMENT ON OPERATOR public.~>? (public.variable_precision_date, public.date_precision) IS 'Compare variable precision dates by precision *only*'; -- @@ -6661,6 +6661,22 @@ CREATE TABLE public.request_queries ( ); +-- +-- Name: request_steps; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.request_steps ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + request_query_id uuid NOT NULL, + request_id uuid, + name text NOT NULL, + current_path text, + duration numeric NOT NULL, + created_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + -- -- Name: request_timings; Type: TABLE; Schema: public; Owner: - -- @@ -6671,7 +6687,8 @@ CREATE TABLE public.request_timings ( duration numeric NOT NULL, variables jsonb, created_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL + updated_at timestamp(6) without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + request_id uuid ); @@ -8742,6 +8759,14 @@ ALTER TABLE ONLY public.request_queries ADD CONSTRAINT request_queries_pkey PRIMARY KEY (id); +-- +-- Name: request_steps request_steps_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.request_steps + ADD CONSTRAINT request_steps_pkey PRIMARY KEY (id); + + -- -- Name: request_timings request_timings_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -12374,6 +12399,13 @@ CREATE INDEX index_rendering_template_logs_on_schema_version_id ON public.render CREATE INDEX index_rendering_template_logs_on_template_definition ON public.rendering_template_logs USING btree (template_definition_type, template_definition_id); +-- +-- Name: index_request_steps_on_request_query_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_request_steps_on_request_query_id ON public.request_steps USING btree (request_query_id); + + -- -- Name: index_request_timings_on_request_query_id; Type: INDEX; Schema: public; Owner: - -- @@ -14467,6 +14499,14 @@ ALTER TABLE ONLY public.assets ADD CONSTRAINT fk_rails_a8a9ebb434 FOREIGN KEY (collection_id) REFERENCES public.collections(id) ON DELETE RESTRICT; +-- +-- Name: request_steps fk_rails_aaed006f41; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.request_steps + ADD CONSTRAINT fk_rails_aaed006f41 FOREIGN KEY (request_query_id) REFERENCES public.request_queries(id) ON DELETE CASCADE; + + -- -- Name: harvest_attempt_transitions fk_rails_abb01db8f3; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -15010,6 +15050,7 @@ ALTER TABLE ONLY public.templates_ordering_instances SET search_path TO "$user", public; INSERT INTO "schema_migrations" (version) VALUES +('20251114195823'), ('20251009055637'), ('20251009053907'), ('20251009053741'), diff --git a/docker-compose.yml b/docker-compose.yml index 803dea3c..d8f6f8fb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -85,6 +85,41 @@ services: timeout: 10s retries: 3 start_period: 15s + localprd: + command: bin/pitchfork -c config/pitchfork.rb + build: + context: . + secrets: + - maxmind_account_id + - maxmind_license_key + depends_on: + db: + condition: service_healthy + migrations: + condition: service_completed_successfully + redis: + condition: service_healthy + env_file: + - docker/compose.env + - docker/local-prd.env + logging: + driver: json-file + options: + max-size: "10m" + max-file: "10" + volumes: + - ./:/srv/app + - bundle_cache:/bundle + - minio_storage:/minio + ports: + - "6122:8080" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "-s", "-o", "/dev/null", "http://localhost:8080/up"] + interval: 1m30s + timeout: 10s + retries: 3 + start_period: 15s worker: build: context: . diff --git a/docker/compose.env b/docker/compose.env index b3666752..1110edec 100644 --- a/docker/compose.env +++ b/docker/compose.env @@ -7,6 +7,7 @@ S3_SECRET_ACCESS_KEY=minio123 UPLOAD_HOST=http://localhost:10042 UPLOAD_BUCKET=meru-api WEB_CONCURRENCY=0 +FRONTEND_REVALIDATE_SECRET=revalidatenow PGPORT=5432 PGHOST=db diff --git a/docker/local-prd.env b/docker/local-prd.env new file mode 100644 index 00000000..7da88c1e --- /dev/null +++ b/docker/local-prd.env @@ -0,0 +1,5 @@ +RAILS_ENV=production +WEB_CONCURRENCY=2 +FRONTEND_REVALIDATE_SECRET=revalidatenow +RAILS_MAX_THREADS=8 +DATABASE_URL=postgresql://postgres:@db/wdp_api_development diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile index 43f79467..b74677b6 100644 --- a/docker/production/Dockerfile +++ b/docker/production/Dockerfile @@ -10,7 +10,7 @@ RUN --mount=type=secret,id=maxmind_account_id \ GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/maxmind_license_key \ /usr/bin/geoipupdate -FROM ruby:3.4.4-bookworm +FROM ruby:3.4.7-bookworm RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ build-essential \ @@ -57,7 +57,6 @@ ENV RUBY_GC_HEAP_2_INIT_SLOTS=455808 ENV RUBY_GC_HEAP_3_INIT_SLOTS=19968 ENV RUBY_GC_HEAP_4_INIT_SLOTS=9265 - RUN gem update --system && gem install bundler:2.7.2 COPY Gemfile Gemfile.lock ./ @@ -80,4 +79,4 @@ COPY --from=maxmind /usr/share/GeoIP/ /usr/share/GeoIP EXPOSE 8080 -CMD ["bin/puma", "-C", "config/puma.rb"] +CMD ["bin/pitchfork", "-c", "config/pitchfork.rb"] diff --git a/lib/frozen_record/static_cached_columns.yml b/lib/frozen_record/static_cached_columns.yml index 09547bcb..ed1c9bfb 100644 --- a/lib/frozen_record/static_cached_columns.yml +++ b/lib/frozen_record/static_cached_columns.yml @@ -16579,6 +16579,112 @@ default_function: CURRENT_TIMESTAMP has_default: true virtual: false +- id: RequestStep#id + model_name: RequestStep + table_name: request_steps + name: id + type: uuid + 'null': false + sql_type_metadata: + sql_type: uuid + type: uuid + default: + default_function: gen_random_uuid() + has_default: true + virtual: false +- id: RequestStep#request_query_id + model_name: RequestStep + table_name: request_steps + name: request_query_id + type: uuid + 'null': false + sql_type_metadata: + sql_type: uuid + type: uuid + default: + default_function: + has_default: false + virtual: false +- id: RequestStep#request_id + model_name: RequestStep + table_name: request_steps + name: request_id + type: uuid + 'null': true + sql_type_metadata: + sql_type: uuid + type: uuid + default: + default_function: + has_default: false + virtual: false +- id: RequestStep#name + model_name: RequestStep + table_name: request_steps + name: name + type: text + 'null': false + sql_type_metadata: + sql_type: text + type: text + default: + default_function: + has_default: false + virtual: false +- id: RequestStep#current_path + model_name: RequestStep + table_name: request_steps + name: current_path + type: text + 'null': true + sql_type_metadata: + sql_type: text + type: text + default: + default_function: + has_default: false + virtual: false +- id: RequestStep#duration + model_name: RequestStep + table_name: request_steps + name: duration + type: decimal + 'null': false + sql_type_metadata: + sql_type: numeric + type: decimal + default: + default_function: + has_default: false + virtual: false +- id: RequestStep#created_at + model_name: RequestStep + table_name: request_steps + name: created_at + type: datetime + 'null': false + sql_type_metadata: + sql_type: timestamp(6) without time zone + type: datetime + precision: 6 + default: + default_function: CURRENT_TIMESTAMP + has_default: true + virtual: false +- id: RequestStep#updated_at + model_name: RequestStep + table_name: request_steps + name: updated_at + type: datetime + 'null': false + sql_type_metadata: + sql_type: timestamp(6) without time zone + type: datetime + precision: 6 + default: + default_function: CURRENT_TIMESTAMP + has_default: true + virtual: false - id: RequestTiming#id model_name: RequestTiming table_name: request_timings @@ -16659,6 +16765,19 @@ default_function: CURRENT_TIMESTAMP has_default: true virtual: false +- id: RequestTiming#request_id + model_name: RequestTiming + table_name: request_timings + name: request_id + type: uuid + 'null': true + sql_type_metadata: + sql_type: uuid + type: uuid + default: + default_function: + has_default: false + virtual: false - id: Role#id model_name: Role table_name: roles diff --git a/lib/generators/model_interface/templates/query_interface.rb.tt b/lib/generators/model_interface/templates/query_interface.rb.tt index a491a34b..adebddd9 100644 --- a/lib/generators/model_interface/templates/query_interface.rb.tt +++ b/lib/generators/model_interface/templates/query_interface.rb.tt @@ -37,7 +37,7 @@ module Types # @param [String] slug # @return [<%= model_name %>, nil] def <%= field_single_name %>(slug:) - Support::Loaders::RecordLoader.for(<%= model_name %>).load(slug) + load_record_with(<%= model_name %>, slug) end <%- if skip_resolver? -%> diff --git a/lib/namespaces/loaders.rb b/lib/namespaces/loaders.rb deleted file mode 100644 index 6fa01d43..00000000 --- a/lib/namespaces/loaders.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# Loaders are used by the GraphQL schema to avoid N+1 and other such performance issues -# since queries can potentially load many levels of models and their associations within -# the same request. -# -# @subsystem GraphQL -module Loaders -end diff --git a/lib/schemas/metaschemas/entity-definition/1.0.0.json b/lib/schemas/metaschemas/entity-definition/1.0.0.json index 85640e62..97a7cb76 100644 --- a/lib/schemas/metaschemas/entity-definition/1.0.0.json +++ b/lib/schemas/metaschemas/entity-definition/1.0.0.json @@ -2,25 +2,25 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "WDP Entity Definition Schema", "description": "A meta-schema used by the NGLP Web Delivery Platform for validating its entity schemas.", - "$defs": { + "definitions": { "Association": { "type": "object", "description": "A hierarchical declaration that determines what types of schemas can be above and below this schema.", "properties": { "namespace": { - "$ref": "#/$defs/Namespace" + "$ref": "#/definitions/Namespace" }, "identifier": { - "$ref": "#/$defs/Identifier" + "$ref": "#/definitions/Identifier" }, "version": { "description": "An optional declaration of the supported versions of the associated schema", "oneOf": [ { - "$ref": "#/$defs/VersionRequirement" + "$ref": "#/definitions/VersionRequirement" }, { - "$ref": "#/$defs/VersionRequirements" + "$ref": "#/definitions/VersionRequirements" } ] } @@ -33,10 +33,10 @@ "additionalProperties": false, "properties": { "namespace": { - "$ref": "#/$defs/Namespace" + "$ref": "#/definitions/Namespace" }, "identifier": { - "$ref": "#/$defs/Identifier" + "$ref": "#/definitions/Identifier" } }, "required": ["namespace", "identifier"] @@ -46,7 +46,7 @@ "description": "Limit the selection to entities that consume these schemas", "items": { "description": "The acceptable schema(s) for this association type", - "$ref": "#/$defs/Association" + "$ref": "#/definitions/Association" } }, "CollectedReferenceProperties": { @@ -75,7 +75,7 @@ "title": "Entity Reference Scoping", "type": "object", "properties": { - "scope": { "$ref": "#/$defs/EntityReferenceSelectionScope" } + "scope": { "$ref": "#/definitions/EntityReferenceSelectionScope" } }, "required": ["scope"] }, @@ -84,18 +84,18 @@ "description": "When referencing an entity, we need to be able to declare a scope to select from", "type": "object", "properties": { - "origin": { "$ref": "#/$defs/EntityReferenceSelectionScopeOrigin" }, - "schemas": { "$ref": "#/$defs/EntityReferenceSelectionSchemas" }, - "target": { "$ref": "#/$defs/EntityReferenceSelectionScopeTarget" } + "origin": { "$ref": "#/definitions/EntityReferenceSelectionScopeOrigin" }, + "schemas": { "$ref": "#/definitions/EntityReferenceSelectionSchemas" }, + "target": { "$ref": "#/definitions/EntityReferenceSelectionScopeTarget" } }, "required": ["origin", "target"], "dependencies": { "target": { "oneOf": [ - { "$ref": "#/$defs/EntityReferenceSelectionScopeTargets/Any" }, - { "$ref": "#/$defs/EntityReferenceSelectionScopeTargets/Descendants" }, - { "$ref": "#/$defs/EntityReferenceSelectionScopeTargets/Links" }, - { "$ref": "#/$defs/EntityReferenceSelectionScopeTargets/Siblings" } + { "$ref": "#/definitions/EntityReferenceSelectionScopeTargets/Any" }, + { "$ref": "#/definitions/EntityReferenceSelectionScopeTargets/Descendants" }, + { "$ref": "#/definitions/EntityReferenceSelectionScopeTargets/Links" }, + { "$ref": "#/definitions/EntityReferenceSelectionScopeTargets/Siblings" } ] } } @@ -104,7 +104,7 @@ "type": "array", "items": { "description": "What scopes are available to the entity reference", - "$ref": "#/$defs/EntityReferenceSelectionScope" + "$ref": "#/definitions/EntityReferenceSelectionScope" }, "minItems": 1 }, @@ -114,15 +114,15 @@ "description": "The originating schema instance to combine with the target and any other options", "default": "self", "oneOf": [ - { "$ref": "#/$defs/EntityReferenceSelectionScopeOrigins/Ancestor" }, - { "$ref": "#/$defs/EntityReferenceSelectionScopeOrigins/Community" }, - { "$ref": "#/$defs/EntityReferenceSelectionScopeOrigins/Parent" }, - { "$ref": "#/$defs/EntityReferenceSelectionScopeOrigins/Self" } + { "$ref": "#/definitions/EntityReferenceSelectionScopeOrigins/Ancestor" }, + { "$ref": "#/definitions/EntityReferenceSelectionScopeOrigins/Community" }, + { "$ref": "#/definitions/EntityReferenceSelectionScopeOrigins/Parent" }, + { "$ref": "#/definitions/EntityReferenceSelectionScopeOrigins/Self" } ] }, "EntityReferenceSelectionScopeOrigins": { "Ancestor": { - "$ref": "#/$defs/NamedAncestorReference", + "$ref": "#/definitions/NamedAncestorReference", "title": "Ancestor Reference", "description": "A named ancestor on the current schema instance" }, @@ -151,7 +151,7 @@ "default": [], "items": { "description": "The acceptable schema(s) for this entity reference", - "$ref": "#/$defs/Association" + "$ref": "#/definitions/Association" }, "maxItems": 5 }, @@ -200,7 +200,7 @@ "description": "The referenced entity must be linked to or from the schema instance", "properties": { "target": { "const": "links" }, - "direction": { "$ref": "#/$defs/LinkReferenceDirection" } + "direction": { "$ref": "#/definitions/LinkReferenceDirection" } } }, "Siblings": { @@ -252,7 +252,7 @@ "type": "object", "properties": { "name": { - "$ref": "#/$defs/Identifier", + "$ref": "#/definitions/Identifier", "title": "Name", "description": "The name of this ancestor association. It must be unique within the current schema." }, @@ -265,7 +265,7 @@ }, "required": ["name"] }, - { "$ref": "#/$defs/Association" } + { "$ref": "#/definitions/Association" } ] }, "NamedAncestorReference": { @@ -276,7 +276,7 @@ "type": "array", "items": { "description": "A list of (uniquely) named ancestors", - "$ref": "#/$defs/NamedAncestor" + "$ref": "#/definitions/NamedAncestor" }, "uniqueItems": true }, @@ -301,7 +301,7 @@ "properties": { "path": { "title": "Path", - "$ref": "#/$defs/OrderPath" + "$ref": "#/definitions/OrderPath" }, "constant": { "type": "boolean", @@ -354,7 +354,7 @@ "properties": { "id": { "title": "Identifier", - "$ref": "#/$defs/Identifier" + "$ref": "#/definitions/Identifier" }, "name": { "title": "Name", @@ -381,7 +381,7 @@ "handles": { "title": "Handles Schema", "description": "An ordering can be set up to handle rendering certain schemas in the explore browser on the frontend.", - "$ref": "#/$defs/AssociationSansVersion" + "$ref": "#/definitions/AssociationSansVersion" }, "filter": { "type": "object", @@ -393,7 +393,7 @@ "type": "array", "items": { "description": "These strings should be existing Schema IDs", - "$ref": "#/$defs/OrderingSchemaFilter" + "$ref": "#/definitions/OrderingSchemaFilter" } } } @@ -401,7 +401,7 @@ "order": { "description": "Specifies how to order the ordering", "items": { - "$ref": "#/$defs/Order" + "$ref": "#/definitions/Order" }, "minItems": 1, "maxItems": 7, @@ -412,7 +412,7 @@ "title": "Render Options", "description": "Options for controlling how entities in an ordering render. These can have larger effects on the rest of the ordering", "properties": { - "mode": { "$ref": "#/$defs/OrderingRenderMode" } + "mode": { "$ref": "#/definitions/OrderingRenderMode" } } }, "select": { @@ -478,12 +478,12 @@ "title": "Order Path", "description": "A path to an attribute that can be used to sort an ordering", "oneOf": [ - { "$ref": "#/$defs/OrderPaths/AncestorPath" }, - { "$ref": "#/$defs/OrderPaths/AncestorSchemaPropertyPath" }, - { "$ref": "#/$defs/OrderPaths/EntityPath" }, - { "$ref": "#/$defs/OrderPaths/LinkPath" }, - { "$ref": "#/$defs/OrderPaths/SchemaPath" }, - { "$ref": "#/$defs/OrderPaths/SchemaPropertyPath" } + { "$ref": "#/definitions/OrderPaths/AncestorPath" }, + { "$ref": "#/definitions/OrderPaths/AncestorSchemaPropertyPath" }, + { "$ref": "#/definitions/OrderPaths/EntityPath" }, + { "$ref": "#/definitions/OrderPaths/LinkPath" }, + { "$ref": "#/definitions/OrderPaths/SchemaPath" }, + { "$ref": "#/definitions/OrderPaths/SchemaPropertyPath" } ] }, "OrderPaths": { @@ -545,19 +545,19 @@ "description": "A declaration of a specific schema that can be included within an order", "properties": { "namespace": { - "$ref": "#/$defs/Namespace" + "$ref": "#/definitions/Namespace" }, "identifier": { - "$ref": "#/$defs/Identifier" + "$ref": "#/definitions/Identifier" }, "version": { "description": "An optional declaration of the supported versions of the associated schema", "oneOf": [ { - "$ref": "#/$defs/VersionRequirement" + "$ref": "#/definitions/VersionRequirement" }, { - "$ref": "#/$defs/VersionRequirements" + "$ref": "#/definitions/VersionRequirements" } ] } @@ -597,7 +597,7 @@ "path": { "title": "Path", "description": "The machine-readable name of this property", - "$ref": "#/$defs/PropertyPath" + "$ref": "#/definitions/PropertyPath" }, "legend": { "title": "Form Legend", @@ -605,7 +605,7 @@ "type": "string" }, "description": { - "$ref": "#/$defs/PropertyDescription" + "$ref": "#/definitions/PropertyDescription" }, "properties": { "type": "array", @@ -613,7 +613,7 @@ "description": "The group's properties", "default": [{ "type": "string" }], "items": { - "$ref": "#/$defs/ScalarProperty" + "$ref": "#/definitions/ScalarProperty" } } } @@ -644,7 +644,7 @@ "description": "Not Yet Used", "type": "array", "uniqueItems": true, - "items": { "$ref": "#/$defs/PropertyMapping" } + "items": { "$ref": "#/definitions/PropertyMapping" } }, "PropertyPath": { "description": "A path can reference a schema property", @@ -658,19 +658,19 @@ "path": { "title": "Path", "description": "The accessible path to this property. When contained in a group, the full path will be prefixed by the group's path", - "$ref": "#/$defs/PropertyPath" + "$ref": "#/definitions/PropertyPath" }, "type": { "title": "Type", - "$ref": "#/$defs/ScalarPropertyType" + "$ref": "#/definitions/ScalarPropertyType" }, "label": { "title": "Label", "description": "The human-readable label for this property. The path will be titlecased if this value is not provided.", "type": "string" }, - "description": { "$ref": "#/$defs/PropertyDescription" }, - "mappings": { "$ref": "#/$defs/PropertyMappings" }, + "description": { "$ref": "#/definitions/PropertyDescription" }, + "mappings": { "$ref": "#/definitions/PropertyMappings" }, "required": { "title": "Required?", "description": "Whether the property is required for base validity", @@ -689,76 +689,76 @@ "type": "boolean", "default": false }, - "function": { "$ref": "#/$defs/PropertyFunction" } + "function": { "$ref": "#/definitions/PropertyFunction" } }, "dependencies": { "type": { "oneOf": [ { - "$ref": "#/$defs/ScalarPropertyDefinitions/Asset" + "$ref": "#/definitions/ScalarPropertyDefinitions/Asset" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Assets" + "$ref": "#/definitions/ScalarPropertyDefinitions/Assets" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Boolean" + "$ref": "#/definitions/ScalarPropertyDefinitions/Boolean" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Contributor" + "$ref": "#/definitions/ScalarPropertyDefinitions/Contributor" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Contributors" + "$ref": "#/definitions/ScalarPropertyDefinitions/Contributors" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/ControlledVocabulary" + "$ref": "#/definitions/ScalarPropertyDefinitions/ControlledVocabulary" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/ControlledVocabularies" + "$ref": "#/definitions/ScalarPropertyDefinitions/ControlledVocabularies" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Date" + "$ref": "#/definitions/ScalarPropertyDefinitions/Date" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Email" + "$ref": "#/definitions/ScalarPropertyDefinitions/Email" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Entities" + "$ref": "#/definitions/ScalarPropertyDefinitions/Entities" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Entity" + "$ref": "#/definitions/ScalarPropertyDefinitions/Entity" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Float" + "$ref": "#/definitions/ScalarPropertyDefinitions/Float" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/FullText" + "$ref": "#/definitions/ScalarPropertyDefinitions/FullText" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Integer" + "$ref": "#/definitions/ScalarPropertyDefinitions/Integer" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Markdown" + "$ref": "#/definitions/ScalarPropertyDefinitions/Markdown" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Multiselect" + "$ref": "#/definitions/ScalarPropertyDefinitions/Multiselect" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Select" + "$ref": "#/definitions/ScalarPropertyDefinitions/Select" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/String" + "$ref": "#/definitions/ScalarPropertyDefinitions/String" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Tags" + "$ref": "#/definitions/ScalarPropertyDefinitions/Tags" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/Timestamp" + "$ref": "#/definitions/ScalarPropertyDefinitions/Timestamp" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/URL" + "$ref": "#/definitions/ScalarPropertyDefinitions/URL" }, { - "$ref": "#/$defs/ScalarPropertyDefinitions/VariableDate" + "$ref": "#/definitions/ScalarPropertyDefinitions/VariableDate" } ] } @@ -781,7 +781,7 @@ }, "required": ["path", "type"], "allOf": [ - { "$ref": "#/$defs/CollectedReferenceProperties" } + { "$ref": "#/definitions/CollectedReferenceProperties" } ] }, "Boolean": { @@ -808,7 +808,7 @@ }, "required": ["path", "type"], "allOf": [ - { "$ref": "#/$defs/CollectedReferenceProperties" } + { "$ref": "#/definitions/CollectedReferenceProperties" } ] }, "ControlledVocabulary": { @@ -835,7 +835,7 @@ }, "required": ["path", "type", "wants"], "allOf": [ - { "$ref": "#/$defs/CollectedReferenceProperties" } + { "$ref": "#/definitions/CollectedReferenceProperties" } ] }, "Date": { @@ -867,8 +867,8 @@ }, "required": ["path", "type"], "allOf": [ - { "$ref": "#/$defs/EntityReferenceScopeProperties" }, - { "$ref": "#/$defs/CollectedReferenceProperties" } + { "$ref": "#/definitions/EntityReferenceScopeProperties" }, + { "$ref": "#/definitions/CollectedReferenceProperties" } ] }, "Entity": { @@ -879,7 +879,7 @@ }, "required": ["path", "type"], "allOf": [ - { "$ref": "#/$defs/EntityReferenceScopeProperties" } + { "$ref": "#/definitions/EntityReferenceScopeProperties" } ] }, "Float": { @@ -989,7 +989,7 @@ "options": { "title": "Select Options", "items": { - "$ref": "#/$defs/SelectOption" + "$ref": "#/definitions/SelectOption" }, "minItems": 1, "type": "array", @@ -1025,7 +1025,7 @@ "options": { "title": "Select Options", "items": { - "$ref": "#/$defs/SelectOption" + "$ref": "#/definitions/SelectOption" }, "minItems": 1, "type": "array", @@ -1152,12 +1152,12 @@ "namespace": { "title": "Namespace", "description": "This identifies the namespace within which this schema should live.", - "$ref": "#/$defs/Namespace" + "$ref": "#/definitions/Namespace" }, "identifier": { "title": "Identifier", "description": "This identifies the schema within its namespace.", - "$ref": "#/$defs/Identifier" + "$ref": "#/definitions/Identifier" }, "name": { "title": "Name", @@ -1166,29 +1166,29 @@ }, "version": { "title": "Version", - "$ref": "#/$defs/SemanticVersion" + "$ref": "#/definitions/SemanticVersion" }, "kind": { "title": "Kind", "description": "The kind of entity that will implement this schema. Entities are also referred to as instances of the schema", - "$ref": "#/$defs/SchemaKind" + "$ref": "#/definitions/SchemaKind" }, "ancestors": { - "$ref": "#/$defs/NamedAncestors" + "$ref": "#/definitions/NamedAncestors" }, "parents": { - "$ref": "#/$defs/Associations", + "$ref": "#/definitions/Associations", "description": "The type(s) of parent(s) this schema accepts." }, "children": { - "$ref": "#/$defs/Associations", + "$ref": "#/definitions/Associations", "description": "The type(s) of direct children this schema accepts." }, "orderings": { "title": "Orderings", "description": "A schema can provide default orderings, which an instance can override.", "items": { - "$ref": "#/$defs/Ordering" + "$ref": "#/definitions/Ordering" }, "type": "array" }, @@ -1197,11 +1197,11 @@ "description": "Describes the properties that will be available for this schema", "type": "array", "items": { - "$ref": "#/$defs/TopLevelProperty" + "$ref": "#/definitions/TopLevelProperty" } }, "render": { - "$ref": "#/$defs/SchemaRenderDefinition" + "$ref": "#/definitions/SchemaRenderDefinition" } } }, @@ -1221,7 +1221,7 @@ "description": "Options for controlling how to render instances of this schema in isolated contexts, outside of orderings.", "additionalProperties": false, "properties": { - "list_mode": { "$ref": "#/$defs/SchemaRenderListMode" } + "list_mode": { "$ref": "#/definitions/SchemaRenderListMode" } } }, "SchemaRenderListMode": { @@ -1270,8 +1270,8 @@ }, "TopLevelProperty": { "oneOf": [ - { "title": "Property", "$ref": "#/$defs/ScalarProperty" }, - { "title": "Group", "$ref": "#/$defs/PropertyGroup" } + { "title": "Property", "$ref": "#/definitions/ScalarProperty" }, + { "title": "Group", "$ref": "#/definitions/PropertyGroup" } ] }, "VersionRequirement": { @@ -1283,9 +1283,9 @@ "type": "array", "description": "An array of version requirements, for establishing constraints", "items": { - "$ref": "#/$defs/VersionRequirement" + "$ref": "#/definitions/VersionRequirement" } } }, - "$ref": "#/$defs/Schema" + "$ref": "#/definitions/Schema" } diff --git a/lib/schemas/metaschemas/seeding/1.0.0.json b/lib/schemas/metaschemas/seeding/1.0.0.json index e01eda82..1d6f8ddc 100644 --- a/lib/schemas/metaschemas/seeding/1.0.0.json +++ b/lib/schemas/metaschemas/seeding/1.0.0.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "WDP Entity Seed Schema", "description": "A meta-schema used by the NGLP Web Delivery Platform for importing entities in a specific hierarchy", - "$defs": { + "definitions": { "AssetData": { "description": "A Data URI that contains the actual asset encoded via Base64. Not recommended for very large assets.", "type": "string", @@ -19,9 +19,9 @@ "description": "A collection is an abstract grouping of items as well as other collections. A journal, a unit, or a series can be considered a collection.", "type": "object", "properties": { - "identifier": { "$ref": "#/$defs/Identifier" }, - "title": { "$ref": "#/$defs/CoreProperties/Title" }, - "subtitle": { "$ref": "#/$defs/CoreProperties/Subtitle" }, + "identifier": { "$ref": "#/definitions/Identifier" }, + "title": { "$ref": "#/definitions/CoreProperties/Title" }, + "subtitle": { "$ref": "#/definitions/CoreProperties/Subtitle" }, "schema": { "title": "Schema", "description": "The schema to use for this collection", @@ -34,13 +34,13 @@ ], "default": "default:collection" }, - "doi": { "$ref": "#/$defs/StringIdentifiers/DOI" }, - "issn": { "$ref": "#/$defs/StringIdentifiers/ISSN" }, - "hero_image": { "$ref": "#/$defs/CoreProperties/HeroImage" }, - "thumbnail": { "$ref": "#/$defs/CoreProperties/Thumbnail" }, + "doi": { "$ref": "#/definitions/StringIdentifiers/DOI" }, + "issn": { "$ref": "#/definitions/StringIdentifiers/ISSN" }, + "hero_image": { "$ref": "#/definitions/CoreProperties/HeroImage" }, + "thumbnail": { "$ref": "#/definitions/CoreProperties/Thumbnail" }, "properties": { "type": "object" }, - "collections": { "$ref": "#/$defs/Collections" }, - "pages": { "$ref": "#/$defs/Pages" } + "collections": { "$ref": "#/definitions/Collections" }, + "pages": { "$ref": "#/definitions/Pages" } }, "required": ["identifier", "title", "schema"], "if": { @@ -50,7 +50,7 @@ }, "then": { "properties": { - "properties": { "$ref": "#/$defs/CollectionProperties/nglp:journal" } + "properties": { "$ref": "#/definitions/CollectionProperties/nglp:journal" } } }, "else": { @@ -61,7 +61,7 @@ }, "then": { "properties": { - "properties": { "$ref": "#/$defs/CollectionProperties/nglp:series" } + "properties": { "$ref": "#/definitions/CollectionProperties/nglp:series" } } }, "else": { @@ -72,7 +72,7 @@ }, "then": { "properties": { - "properties": { "$ref": "#/$defs/CollectionProperties/nglp:unit" } + "properties": { "$ref": "#/definitions/CollectionProperties/nglp:unit" } } } } @@ -95,7 +95,7 @@ "description": "Whether or not this journal should be considered open access." }, "description": { - "$ref": "#/$defs/Content/FullText" + "$ref": "#/definitions/Content/FullText" }, "cc_license": { "title": "CC License", @@ -116,7 +116,7 @@ "type": "object", "properties": { "about": { - "$ref": "#/$defs/Content/Markdown" + "$ref": "#/definitions/Content/Markdown" } } }, @@ -124,7 +124,7 @@ "type": "object", "properties": { "about": { - "$ref": "#/$defs/Content/Markdown" + "$ref": "#/definitions/Content/Markdown" } } } @@ -134,7 +134,7 @@ "description": "A list of collections at the current hierarchical scope.", "type": "array", "items": { - "$ref": "#/$defs/Collection" + "$ref": "#/definitions/Collection" } }, "Community": { @@ -142,8 +142,8 @@ "description": "A community is a top-level organizational entity that acts as an entry point for accessing collections and their items within the WDP.", "type": "object", "properties": { - "identifier": { "$ref": "#/$defs/Identifier" }, - "title": { "$ref": "#/$defs/CoreProperties/Title"}, + "identifier": { "$ref": "#/definitions/Identifier" }, + "title": { "$ref": "#/definitions/CoreProperties/Title"}, "schema": { "title": "Schema", "description": "The schema to use for this community", @@ -154,13 +154,13 @@ "default": "default:community" }, "hero_image": { - "$ref": "#/$defs/CoreProperties/HeroImage" + "$ref": "#/definitions/CoreProperties/HeroImage" }, - "logo": { "$ref": "#/$defs/CoreProperties/Logo" }, - "thumbnail": { "$ref": "#/$defs/CoreProperties/Thumbnail" }, - "properties": { "$ref": "#/$defs/CommunityProperties/default:community" }, - "collections": { "$ref": "#/$defs/Collections" }, - "pages": { "$ref": "#/$defs/Pages" } + "logo": { "$ref": "#/definitions/CoreProperties/Logo" }, + "thumbnail": { "$ref": "#/definitions/CoreProperties/Thumbnail" }, + "properties": { "$ref": "#/definitions/CommunityProperties/default:community" }, + "collections": { "$ref": "#/definitions/Collections" }, + "pages": { "$ref": "#/definitions/Pages" } }, "required": ["identifier", "title", "schema"] }, @@ -177,15 +177,15 @@ "properties": { "journals": { "title": "Journals", - "$ref": "#/$defs/Identifiers" + "$ref": "#/definitions/Identifiers" }, "series": { "title": "Series", - "$ref": "#/$defs/Identifiers" + "$ref": "#/definitions/Identifiers" }, "units": { "title": "Units", - "$ref": "#/$defs/Identifiers" + "$ref": "#/definitions/Identifiers" } } } @@ -197,7 +197,7 @@ "description": "A list of communities to define at the top level of the WDP installation.", "type": "array", "items": { - "$ref": "#/$defs/Community" + "$ref": "#/definitions/Community" } }, "Content": { @@ -268,21 +268,21 @@ "title": "Hero Image", "description": "This image is displayed on the landing page for the entity.", "allOf": [ - { "$ref": "#/$defs/OptionalRawAsset" } + { "$ref": "#/definitions/OptionalRawAsset" } ] }, "Logo": { "title": "Logo", "description": "A community-specific image that acts as its logo.", "allOf": [ - { "$ref": "#/$defs/OptionalRawAsset" } + { "$ref": "#/definitions/OptionalRawAsset" } ] }, "Thumbnail": { "title": "Thumbnail", "description": "A representative image for the entity that is used when rendering in a list, search results, etc.", "allOf": [ - { "$ref": "#/$defs/OptionalRawAsset" } + { "$ref": "#/definitions/OptionalRawAsset" } ] } }, @@ -294,23 +294,23 @@ "IdentifierPath": { "type": "array", "items": { - "$ref": "#/$defs/Identifier" + "$ref": "#/definitions/Identifier" } }, "Identifiers": { "type": "array", "items": { - "$ref": "#/$defs/IdentifierPath" + "$ref": "#/definitions/IdentifierPath" } }, "Page": { "title": "Page", "description": "A markdown-based element that will appear as additional pages of arbitrary information on the associated entity.", "properties": { - "slug": { "$ref": "#/$defs/Slug" }, + "slug": { "$ref": "#/definitions/Slug" }, "title": { "type": "string" }, - "body": { "$ref": "#/$defs/Content/Markdown" }, - "hero_image": { "$ref": "#/$defs/CoreProperties/HeroImage" } + "body": { "$ref": "#/definitions/Content/Markdown" }, + "hero_image": { "$ref": "#/definitions/CoreProperties/HeroImage" } }, "required": ["slug", "title", "body"] }, @@ -318,7 +318,7 @@ "title": "Pages", "description": "A listing of pages for the associated entity. They will be deterministically ordered based on the order defined in this import.", "type": "array", - "items": { "$ref": "#/$defs/Page" } + "items": { "$ref": "#/definitions/Page" } }, "RawAsset": { "type": "object", @@ -333,7 +333,7 @@ "enum": ["url"], "default": "url" }, - "url": { "$ref": "#/$defs/AssetURL" } + "url": { "$ref": "#/definitions/AssetURL" } }, "required": ["format", "url"] }, @@ -347,7 +347,7 @@ "enum": ["data"], "default": "data" }, - "data": { "$ref": "#/$defs/AssetData" } + "data": { "$ref": "#/definitions/AssetData" } }, "required": ["format", "data"] } @@ -364,7 +364,7 @@ }, { "title": "Provided", - "$ref": "#/$defs/RawAsset" + "$ref": "#/definitions/RawAsset" } ] }, @@ -382,8 +382,8 @@ "title": "A WDP Seeding Definition", "type": "object", "properties": { - "version": { "$ref": "#/$defs/Seed/Version" }, - "communities": { "$ref": "#/$defs/Communities" } + "version": { "$ref": "#/definitions/Seed/Version" }, + "communities": { "$ref": "#/definitions/Communities" } }, "additionalProperties": false }, @@ -404,5 +404,5 @@ "DOI": { "type": "string" } } }, - "$ref": "#/$defs/Seed/Root" + "$ref": "#/definitions/Seed/Root" } diff --git a/lib/support/lib/caching/cache.rb b/lib/support/lib/caching/cache.rb index 77bfa5ff..e1548205 100644 --- a/lib/support/lib/caching/cache.rb +++ b/lib/support/lib/caching/cache.rb @@ -2,8 +2,8 @@ module Support module Caching - # dry-effects provides a cache effect that is very useful, but has no fall-through - # when not wrapped. + # A caching mechanism for GraphQL requests that is safe to use + # within the context of a request, whether it dips into threads or fibers. # # This marries cache with the Reader effect to specify whether or not the cache # is active or not. It allows for safe evaluation of anything with the ability @@ -11,16 +11,6 @@ module Caching # # @api private class Cache - include Dry::Effects::Handler.Cache(:vog_safe_cache) - include Dry::Effects::Handler.Reader(:vog_safe_cache_active) - include Dry::Effects.Cache(:vog_safe_cache, shared: true) - include Dry::Effects.Reader(:vog_safe_cache_active, default: false) - - alias vog_safe_cache_active? vog_safe_cache_active - - private :cache - private :with_cache - def vog_cache(...) if vog_safe_cache_active? cache(...) @@ -29,23 +19,79 @@ def vog_cache(...) end end + # Activates the VOG cache for the duration of the block + # within a Thread.current variable. + # @return [void] def with_vog_cache + # :nocov: return yield if vog_safe_cache_active? + # :nocov: - with_vog_safe_cache_active true do - with_cache do + with_vog_safe_cache_active! do + with_cache! do yield end end end + # @!attribute [r] vog_safe_cache_active + # @return [Boolean] + def vog_safe_cache_active + Thread.current.thread_variable_get(:vog_safe_cache_active) || false + end + + alias vog_safe_cache_active? vog_safe_cache_active + + alias vog_cache_active? vog_safe_cache_active + + # @!attribute [r] vog_safe_cache + # @return [Concurrent::Map, nil] + def vog_safe_cache + Thread.current.thread_variable_get(:vog_safe_cache) + end + + # @api private + # @param [Array] args the args to key by + # @return [Object] + def cache(*args, &) + vog_safe_cache.then do |c| + # :nocov: + return yield if c.nil? + # :nocov: + + c.compute_if_absent(args, &) + end + end + + # @return [void] + def with_cache! + original = vog_safe_cache + + Thread.current.thread_variable_set(:vog_safe_cache, Concurrent::Map.new) + + yield + ensure + Thread.current.thread_variable_set(:vog_safe_cache, original) + end + + # @return [void] + def with_vog_safe_cache_active! + original = vog_safe_cache_active + + Thread.current.thread_variable_set(:vog_safe_cache_active, true) + + yield + ensure + Thread.current.thread_variable_set(:vog_safe_cache_active, original) + end + class << self # @return [Support::Caching::Cache] def instance @instance ||= new end - delegate :vog_cache, :with_vog_cache, to: :instance + delegate :vog_cache_active?, :vog_cache, :with_vog_cache, to: :instance end end end diff --git a/lib/support/lib/graphql_api/association_helpers.rb b/lib/support/lib/graphql_api/association_helpers.rb index f8c406c2..af6fbf44 100644 --- a/lib/support/lib/graphql_api/association_helpers.rb +++ b/lib/support/lib/graphql_api/association_helpers.rb @@ -6,17 +6,45 @@ module GraphQLAPI module AssociationHelpers extend ActiveSupport::Concern + # @param [Symbol] association + # @param [Class(ApplicationRecord), nil] klass + # @param [ApplicationRecord, Object] record + # @return [Promise, Object] def association_loader_for(association, klass: object&.class, record: object) if !record.kind_of?(ActiveRecord::Base) # Handle AnonymousUser, other proxies - Promise.resolve(record.try(association)) + record.try(association) elsif record.association(association).loaded? - Promise.resolve(record.public_send(association)) + record.public_send(association) + elsif MeruConfig.experimental_dataloader? + # :nocov: + dataloader.with(GraphQL::Dataloader::ActiveRecordAssociationSource, association).load(record) + # :nocov: else Support::Loaders::AssociationLoader.for(klass, association).load(record) end end + def load_record_with(klass, id, **options) + if MeruConfig.experimental_dataloader? + # :nocov: + dataloader.with(GraphQL::Dataloader::ActiveRecordSource, klass, **options).load(id) + # :nocov: + else + Support::Loaders::RecordLoader.for(klass, **options).load(id) + end + end + + # If we are using graphql-batch / promises, await the promises. + # Otherwise, just return the values as-is. + def maybe_await(promises) + # :nocov: + return promises if MeruConfig.experimental_dataloader? + # :nocov: + + Promise.all(promises) + end + module ClassMethods # @param [Symbol] association_name # @param [Symbol] as diff --git a/lib/support/lib/graphql_api/direct_connection_and_edge_support.rb b/lib/support/lib/graphql_api/direct_connection_and_edge_support.rb index e2c89187..c73b8f6c 100644 --- a/lib/support/lib/graphql_api/direct_connection_and_edge_support.rb +++ b/lib/support/lib/graphql_api/direct_connection_and_edge_support.rb @@ -11,8 +11,13 @@ def use_direct_connection_and_edge!( connection_klass_name: "Types::#{base_name}ConnectionType", edge_klass_name: "Types::#{base_name}EdgeType" ) - @connection_type = connection_klass_name.constantize - @edge_type = edge_klass_name.constantize + define_singleton_method(:edge_type) do + @edge_type ||= edge_klass_name.constantize + end + + define_singleton_method(:connection_type) do + @connection_type ||= connection_klass_name.constantize + end end end end diff --git a/lib/support/lib/loaders/record_loader.rb b/lib/support/lib/loaders/record_loader.rb index 984a145f..80f61596 100644 --- a/lib/support/lib/loaders/record_loader.rb +++ b/lib/support/lib/loaders/record_loader.rb @@ -4,23 +4,23 @@ module Support module Loaders # A loader for a specific record. class RecordLoader < GraphQL::Batch::Loader - def initialize(model, column: model.primary_key, order: nil, where: nil) + def initialize(model, find_by: model.primary_key, order: nil, where: nil) super() @model = model - @column = column.to_s - @column_type = model.type_for_attribute(@column) + @find_by = find_by.to_s + @find_by_type = model.type_for_attribute(@find_by) @order = order if order.present? - @is_primary_key = column == model.primary_key + @is_primary_key = find_by == model.primary_key @where = where end def load(key) - super(@column_type.cast(key)) + super(@find_by_type.cast(key)) end def perform(keys) query(keys).each do |record| - key = record.public_send(@column) + key = record.public_send(@find_by) next if fulfilled? key @@ -37,12 +37,12 @@ def query(keys) scope = scope.where(@where) if @where unless @is_primary_key - scope = scope.distinct_on @column - scope = scope.order @column + scope = scope.distinct_on @find_by + scope = scope.order @find_by scope = scope.order @order end - scope.where(@column => keys) + scope.where(@find_by => keys) end end end diff --git a/lib/support/lib/requests/connection_counts.rb b/lib/support/lib/requests/connection_counts.rb index 98b2a635..2eceff43 100644 --- a/lib/support/lib/requests/connection_counts.rb +++ b/lib/support/lib/requests/connection_counts.rb @@ -8,8 +8,6 @@ module Requests # # @api private class ConnectionCounts < GraphQL::Schema::FieldExtension - include Dry::Effects::Handler.Resolve - extras %i[lookahead] # @return [void] @@ -22,12 +20,14 @@ def resolve(object:, arguments:, context:, **) wants_unfiltered_count = pagination.selects?(:total_unfiltered_count) - options = { path: context[:current_path], wants_total_count:, wants_unfiltered_count: } + current_path = context[:current_path] || ConnectionInfo::UNKNOWN_PATH + + options = { path: current_path, wants_total_count:, wants_unfiltered_count: } - connection_info = Support::Requests::ConnectionInfo.new(**options) + info = Support::Requests::ConnectionInfo.new(**options) - provide(connection_info:) do - yield object, arguments, connection_info + Support::Requests::CurrentConnection.set(info:, current_path:) do + yield object, arguments, info end end diff --git a/lib/support/lib/requests/connection_info.rb b/lib/support/lib/requests/connection_info.rb index 2fc830c6..4a01c18b 100644 --- a/lib/support/lib/requests/connection_info.rb +++ b/lib/support/lib/requests/connection_info.rb @@ -4,6 +4,8 @@ module Support module Requests # @api private class ConnectionInfo < Support::WritableStruct + UNKNOWN_PATH = %w[unknown].freeze + attribute :path, Types::Path attribute? :total_count, Types::Count @@ -14,6 +16,13 @@ class ConnectionInfo < Support::WritableStruct alias wants_total_count? wants_total_count alias wants_unfiltered_count? wants_unfiltered_count + + class << self + # @return [ConnectionInfo] + def unknown + new(path: UNKNOWN_PATH) + end + end end end end diff --git a/lib/support/lib/requests/current.rb b/lib/support/lib/requests/current.rb new file mode 100644 index 00000000..36205a97 --- /dev/null +++ b/lib/support/lib/requests/current.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Support + module Requests + # Current request attributes for GraphQL requests. + class Current < ActiveSupport::CurrentAttributes + attribute :current_user + attribute :graphql_kind, default: -> { "other" } + attribute :graphql_operation_name + attribute :graphql_steps, default: -> { [] } + attribute :request_id + attribute :state, default: -> { Support::Requests::State.new } + end + end +end diff --git a/lib/support/lib/requests/current_connection.rb b/lib/support/lib/requests/current_connection.rb new file mode 100644 index 00000000..c4a61f5b --- /dev/null +++ b/lib/support/lib/requests/current_connection.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Support + module Requests + # Current connection path for GraphQL requests. + class CurrentConnection < ActiveSupport::CurrentAttributes + attribute :current_path + attribute :info, default: -> { Requests::ConnectionInfo.unknown } + end + end +end diff --git a/lib/support/lib/requests/sets_connection_info.rb b/lib/support/lib/requests/sets_connection_info.rb index 7882e859..062665aa 100644 --- a/lib/support/lib/requests/sets_connection_info.rb +++ b/lib/support/lib/requests/sets_connection_info.rb @@ -2,27 +2,29 @@ module Support module Requests + # A concern to set and manage connection info within GraphQL requests. module SetsConnectionInfo extend ActiveSupport::Concern included do - include Dry::Effects.Resolve(:connection_info) - - delegate :wants_total_count?, :wants_unfiltered_count?, to: :safe_connection_info + delegate :wants_total_count?, :wants_unfiltered_count?, to: :connection_info end def increment_total_count!(value) - safe_connection_info.total_count += value + connection_info.total_count += value end def increment_unfiltered_count!(value) - safe_connection_info.unfiltered_count += value + connection_info.unfiltered_count += value end private - def safe_connection_info - connection_info { Requests::ConnectionInfo.new(path: %w[unknown]) } + # @!attribute [r] connection_info + # @see Support::Requests::CurrentConnection.info + # @return [Support::Requests::ConnectionInfo] + def connection_info + Support::Requests::CurrentConnection.info end end end diff --git a/lib/support/lib/requests/state.rb b/lib/support/lib/requests/state.rb index d11d113c..8094d90a 100644 --- a/lib/support/lib/requests/state.rb +++ b/lib/support/lib/requests/state.rb @@ -4,15 +4,26 @@ module Support module Requests # A state object that wraps GraphQL requests and provides certain caching # and other support in the context. + # + # @see Support::Requests::Current + # @see Support::Caching::Cache class State extend ActiveModel::Callbacks define_model_callbacks :request, :connection + around_request :provide_current_state! + around_request :provide_vog_cache! - def initialize - @lookups = Concurrent::Map.new + around_request :measure! + + # @return [Support::Requests::Timer, nil] + attr_reader :timer + + # @return [void] + def set_up_timer!(...) + @timer = Timer.new(...) end # @yieldreturn [void] @@ -25,6 +36,23 @@ def wrap private + # @return [void] + def measure! + # :nocov: + return yield unless timer.present? + # :nocov: + + timer.measure! do + yield + end + end + + # @return [void] + def provide_current_state!(&) + Support::Requests::Current.set(state: self, &) + end + + # @return [void] def provide_vog_cache!(&) Support::Caching.with_vog_cache(&) end diff --git a/lib/support/lib/requests/timer.rb b/lib/support/lib/requests/timer.rb new file mode 100644 index 00000000..ee845f83 --- /dev/null +++ b/lib/support/lib/requests/timer.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module Support + module Requests + class Timer < Support::FlexibleStruct + extend ActiveModel::Callbacks + + attribute :query, Support::Types::Coercible::String + attribute :operation_name, Support::Types::String.optional + attribute :variables, Support::Types::Hash.fallback { {} } + + define_model_callbacks :measure + + around_measure :record_duration! + + # @return [Integer] + attr_reader :duration + + # @return [String] + attr_reader :request_id + + # @return [::RequestQuery] + attr_reader :request_query + + delegate :id, to: :request_query, prefix: true + + # @return [::RequestTiming] + attr_reader :request_timing + + # @return [void] + def measure! + @request_id = Support::Requests::Current.request_id || SecureRandom.uuid + + @request_query ||= load_request_query! + + update_current_request! + + run_callbacks :measure do + yield + end + ensure + store_steps! + end + + private + + # @return [RequestQuery] + def load_request_query! + ::RequestQuery.where(query:).first_or_create! do |rq| + rq.operation_name = operation_name + end + end + + # @return [void] + def record_duration! + @duration = AbsoluteTime.realtime do + yield + end + + @request_query.request_timings.create(variables:, duration:, request_id:) + end + + # @return [void] + def store_steps! + base_tuple = { request_query_id:, request_id:, created_at: Time.current, updated_at: Time.current } + + tuples = Support::Requests::Current.graphql_steps.map do |step| + base_tuple.merge(step) + end + + # :nocov: + return if tuples.empty? + # :nocov: + + ::RequestStep.insert_all(tuples, returning: nil) + end + + # @return [void] + def update_current_request! + Support::Requests::Current.graphql_kind = request_query.kind + Support::Requests::Current.graphql_operation_name = request_query.operation_name + end + + class << self + def measure!(**kwargs, &) + new(**kwargs).measure!(&) + end + end + end + end +end diff --git a/lib/tasks/graphql.rake b/lib/tasks/graphql.rake new file mode 100644 index 00000000..f643aaa2 --- /dev/null +++ b/lib/tasks/graphql.rake @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +require "graphql/rake_task" + +GraphQL::RakeTask.new(schema_name: "APISchema") diff --git a/spec/jobs/testing/quick_job_spec.rb b/spec/jobs/testing/quick_job_spec.rb deleted file mode 100644 index 6d4cd4fe..00000000 --- a/spec/jobs/testing/quick_job_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Testing::QuickJob, type: :job do - it "times out very rapidly" do - expect do - expect(described_class.perform_now).to be_a_kind_of(ApplicationJob::JobTimeoutError) - end.to execute_safely - end -end diff --git a/spec/requests/graphql/query/contributor_lookup_spec.rb b/spec/requests/graphql/query/contributor_lookup_spec.rb index 3a91cbdf..c10018b0 100644 --- a/spec/requests/graphql/query/contributor_lookup_spec.rb +++ b/spec/requests/graphql/query/contributor_lookup_spec.rb @@ -9,7 +9,7 @@ } } - fragment AnyContributorDetailsFragment on AnyContributor { + fragment AnyContributorDetailsFragment on Contributor { ... on OrganizationContributor { slug diff --git a/spec/requests/graphql/query/contributor_spec.rb b/spec/requests/graphql/query/contributor_spec.rb index fe75d3c1..9369cf7a 100644 --- a/spec/requests/graphql/query/contributor_spec.rb +++ b/spec/requests/graphql/query/contributor_spec.rb @@ -13,7 +13,7 @@ } } - fragment AnyContributorDetailsFragment on AnyContributor { + fragment AnyContributorDetailsFragment on Contributor { ... on OrganizationContributor { slug diff --git a/spec/requests/graphql/query/schema_instance_spec.rb b/spec/requests/graphql/query/schema_instance_spec.rb index b829b280..82a8b352 100644 --- a/spec/requests/graphql/query/schema_instance_spec.rb +++ b/spec/requests/graphql/query/schema_instance_spec.rb @@ -50,7 +50,7 @@ } } - fragment PropInfoFragment on AnySchemaProperty { + fragment PropInfoFragment on SchemaProperty { __typename ... on ScalarProperty { diff --git a/spec/support/graphql/apply_schema_properties.graphql b/spec/support/graphql/apply_schema_properties.graphql index 47c6c3fb..f8ccf289 100644 --- a/spec/support/graphql/apply_schema_properties.graphql +++ b/spec/support/graphql/apply_schema_properties.graphql @@ -53,7 +53,7 @@ fragment GetSchemaInstanceFragment on SchemaInstance { } } -fragment GetPropertyInfoFragment on AnyScalarProperty { +fragment GetPropertyInfoFragment on ScalarProperty { __typename ... on ScalarProperty { @@ -178,23 +178,19 @@ fragment GetPropertyInfoFragment on AnyScalarProperty { } } -fragment GetAssetInfoFragment on AnyAsset { - ... on Asset { - id - kind - downloadUrl - } +fragment GetAssetInfoFragment on Asset { + id + kind + downloadUrl } -fragment GetContributorInfoFragment on AnyContributor { +fragment GetContributorInfoFragment on Contributor { __typename - ... on Contributor { - identifier - kind - name - email - } + identifier + kind + name + email } fragment GetControlledVocabularyInfoFragment on ControlledVocabulary { diff --git a/spec/support/shared_examples/a_graphql_entity_with_layouts.rb b/spec/support/shared_examples/a_graphql_entity_with_layouts.rb index 4ddf5af6..1b177a11 100644 --- a/spec/support/shared_examples/a_graphql_entity_with_layouts.rb +++ b/spec/support/shared_examples/a_graphql_entity_with_layouts.rb @@ -181,7 +181,7 @@ } - fragment AnyEntityFragment on AnyEntity { + fragment AnyEntityFragment on Entity { __typename ... on Entity { diff --git a/spec/vog/caching/cache_spec.rb b/spec/vog/caching/cache_spec.rb new file mode 100644 index 00000000..6f5aee7f --- /dev/null +++ b/spec/vog/caching/cache_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +RSpec.describe Support::Caching::Cache do + # @note We don't use let here because we want a simple + # method that doesn't work with RSpec's scoping, which + # fibers / threads may interfere with. + def instance + described_class.instance + end + + subject { instance } + + describe "#with_vog_cache" do + it "activates the vog cache within the block in a thread & fiber-safe manner", :aggregate_failures do + is_expected.not_to be_vog_cache_active + + instance.with_vog_cache do + is_expected.to be_vog_cache_active + + expect(instance.vog_safe_cache).to be_a(Concurrent::Map) + + expect do + fib1 = Fiber.new do + instance.vog_cache(:test) { "set in fiber1" } + + Fiber.yield + end + + fib1.resume + fib1.resume + end.to change { instance.vog_safe_cache.size }.by(1) + .and change { instance.vog_safe_cache.key?(%i[test]) } + + cached = instance.vog_cache(:test) { "diff value" } + + expect(cached).to eq("set in fiber1") + end + + expect(instance.vog_safe_cache).to be_nil + + is_expected.not_to be_vog_cache_active + end + end +end