diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00e78c03..6c2ba3a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,10 +57,10 @@ jobs: JWT_PUBLIC_KEY: ${{ secrets.JWT_PUBLIC_KEY }} steps: - uses: actions/checkout@v3 - - name: Set up Ruby 2.6 + - name: Set up Ruby 3.1.4 uses: ruby/setup-ruby@v1 with: - ruby-version: 2.6 + ruby-version: 3.1.4 - uses: actions/cache@v2 with: path: vendor/bundle diff --git a/.rubocop.yml b/.rubocop.yml index 87e04a1e..cc4e248f 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,7 +5,7 @@ require: - rubocop-rspec AllCops: - TargetRubyVersion: 2.6 + TargetRubyVersion: 3.1 # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop # to ignore them, so only the ones explicitly set in this file are enabled. DisabledByDefault: true @@ -89,7 +89,7 @@ Layout/EmptyLinesAroundModuleBody: # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. Style/HashSyntax: - Enabled: true + Enabled: false Layout/FirstArgumentIndentation: Enabled: true diff --git a/Dockerfile b/Dockerfile index 1a440271..9d6144be 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM phusion/passenger-full:2.0.0 +FROM phusion/passenger-full:2.5.1 LABEL maintainer="support@datacite.org" # Set correct environment variables @@ -15,8 +15,8 @@ RUN groupmod -g 1000 app # Use baseimage-docker's init process CMD ["/sbin/my_init"] -# Use Ruby 2.6.8 -RUN bash -lc 'rvm --default use ruby-2.6.8' +# Use Ruby 3.1.4 +RUN bash -lc 'rvm --default use ruby-3.1.4' # Set debconf to run non-interactively RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections diff --git a/Gemfile b/Gemfile index c6c93899..f0004c2d 100755 --- a/Gemfile +++ b/Gemfile @@ -14,7 +14,10 @@ gem "aws-sdk-sqs", "~> 1.23", ">= 1.23.1" gem "base32-crockford-checksum", "~> 0.2.3" gem "config", "~> 2.2", ">= 2.2.1" gem "dotenv", "~> 2.7", ">= 2.7.5" -gem "fast_jsonapi", "~> 1.5" +# IMPORTANT!!! +# We have monkey patched this gem -> config/initializers/serialization_core.rb +# Please check this before upgrading/downgrading versions +gem "jsonapi-serializer", "~> 2.2" gem "flipper", "~> 0.17.2" gem "flipper-active_support_cache_store" gem "flipper-api" @@ -24,7 +27,7 @@ gem "nilify_blanks", "~> 1.3" gem "oj", ">= 2.8.3" gem "oj_mimic_json", "~> 1.0", ">= 1.0.1" gem "orcid_client", "~> 0.11.0" -gem "postrank-uri", "~> 1.0", ">= 1.0.24" +gem "postrank-uri", "~> 1.1" gem "pwqgen.rb", "~> 0.1.0" gem "rake", "~> 12.0" gem "sentry-raven", "~> 2.13" @@ -66,7 +69,7 @@ gem "elasticsearch", "~> 7.1.0" gem "elasticsearch-model", "~> 7.0", require: "elasticsearch/model" gem "elasticsearch-rails", "~> 7.0" gem "faraday_middleware-aws-sigv4", "~> 0.2.4" -gem "google-protobuf", "3.10.0.rc.1" +gem "google-protobuf", "3.19.6" gem "graphql", "~> 1.9", ">= 1.9.16" gem "graphql-batch", "~> 0.4.1" gem "graphql-cache", "~> 0.6.0" @@ -102,7 +105,6 @@ group :development do gem "spring" gem "spring-commands-rspec" gem "spring-watcher-listen", "~> 2.0.0" - # gem "httplog", "~> 1.0" end group :test do @@ -117,7 +119,7 @@ group :test do gem "shoulda-matchers", "~> 4.1", ">= 4.1.2" gem "simplecov", "~> 0.17.1" gem "test-prof", "~> 0.10.2" - gem "vcr", "~> 3.0.3" + gem "vcr", "~> 6.2" gem "webmock", "~> 3.1" gem "with_env", "~> 1.1" end diff --git a/Gemfile.lock b/Gemfile.lock index f6a325b6..d6cd1ec0 100755 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -289,8 +289,6 @@ GEM faraday_middleware-aws-sigv4 (0.2.4) aws-sigv4 (~> 1.0) faraday (>= 0.9) - fast_jsonapi (1.5) - activesupport (>= 4.2) ferrum (0.11) addressable (~> 2.5) cliver (~> 0.3) @@ -318,7 +316,7 @@ GEM rchardet (~> 1.8) globalid (1.2.1) activesupport (>= 6.1) - google-protobuf (3.10.0.rc.1) + google-protobuf (3.19.6) graphql (1.12.16) graphql-batch (0.4.3) graphql (>= 1.3, < 2) @@ -352,6 +350,8 @@ GEM json-ld (~> 3.2) rdf (~> 3.2) jsonapi-renderer (0.2.2) + jsonapi-serializer (2.2.0) + activesupport (>= 4.2) jsonlint (0.3.0) oj (~> 3) optimist (~> 3) @@ -372,10 +372,9 @@ GEM launchy (2.5.0) addressable (~> 2.7) link_header (0.0.8) - listen (3.1.5) + listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - ruby_dep (~> 1.2) lograge (0.11.2) actionpack (>= 4) activesupport (>= 4) @@ -479,12 +478,12 @@ GEM parallel (1.22.1) parser (3.1.2.1) ast (~> 2.4.1) - postrank-uri (1.0.24) + postrank-uri (1.1) addressable (>= 2.4.0) nokogiri (>= 1.8.0) - public_suffix (>= 2.0.0, < 2.1) + public_suffix (>= 4.0.0, < 5) promise.rb (0.7.4) - public_suffix (2.0.5) + public_suffix (4.0.7) pwqgen.rb (0.1.0) docopt (~> 0.5) sysrandom @@ -615,7 +614,6 @@ GEM ruby-enum (0.9.0) i18n ruby-progressbar (1.11.0) - ruby_dep (1.5.0) ruby_dig (0.0.2) scanf (1.0.0) sentry-raven (2.13.0) @@ -675,7 +673,7 @@ GEM unicode_utils (1.4.0) validates_email_format_of (1.6.3) i18n - vcr (3.0.3) + vcr (6.2.0) warden (1.2.9) rack (>= 2.0.9) webmock (3.14.0) @@ -725,14 +723,13 @@ DEPENDENCIES email_spec (~> 2.2) factory_bot_rails (~> 4.8, >= 4.8.2) faraday_middleware-aws-sigv4 (~> 0.2.4) - fast_jsonapi (~> 1.5) flipper (~> 0.17.2) flipper-active_support_cache_store flipper-api flipper-redis flipper-ui git (~> 1.5) - google-protobuf (= 3.10.0.rc.1) + google-protobuf (= 3.19.6) graphql (~> 1.9, >= 1.9.16) graphql-batch (~> 0.4.1) graphql-cache (~> 0.6.0) @@ -740,6 +737,7 @@ DEPENDENCIES gravtastic (~> 3.2, >= 3.2.6) hashdiff (>= 1.0.0.beta1, < 2.0.0) hashie + jsonapi-serializer (~> 2.2) jwt (~> 2.2, >= 2.2.1) kaminari (~> 1.2) listen (>= 3.0.5, < 3.2) @@ -762,7 +760,7 @@ DEPENDENCIES omniauth-orcid (~> 2.0) omniauth-rails_csrf_protection (~> 1.0) orcid_client (~> 0.11.0) - postrank-uri (~> 1.0, >= 1.0.24) + postrank-uri (~> 1.1) pwqgen.rb (~> 0.1.0) rack-cors (~> 1.0) rack-jwt @@ -795,9 +793,9 @@ DEPENDENCIES tzinfo-data (~> 1.2019, >= 1.2019.3) uglifier (~> 2.7, >= 2.7.2) validates_email_format_of (~> 1.6, >= 1.6.3) - vcr (~> 3.0.3) + vcr (~> 6.2) webmock (~> 3.1) with_env (~> 1.1) BUNDLED WITH - 2.4.22 + 2.4.22 \ No newline at end of file diff --git a/app/controllers/claims_controller.rb b/app/controllers/claims_controller.rb index 3be05f82..0a916558 100644 --- a/app/controllers/claims_controller.rb +++ b/app/controllers/claims_controller.rb @@ -12,7 +12,7 @@ def show options[:include] = @include options[:is_collection] = false - render json: ClaimSerializer.new(@claim, options).serialized_json, status: :ok + render json: ClaimSerializer.new(@claim, options).serializable_hash.to_json, status: :ok end def index @@ -84,9 +84,9 @@ def index fields = fields_from_params(params) if fields - render json: ClaimSerializer.new(response.results, options.merge(fields: fields)).serialized_json, status: :ok + render json: ClaimSerializer.new(response.results, options.merge(fields: fields)).serializable_hash.to_json, status: :ok else - render json: ClaimSerializer.new(response.results, options).serialized_json, status: :ok + render json: ClaimSerializer.new(response.results, options).serializable_hash.to_json, status: :ok end rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e Raven.capture_exception(e) @@ -112,13 +112,12 @@ def create end if @claim.save - @claim.queue_claim_job options = {} options[:include] = @include options[:is_collection] = false - render json: ClaimSerializer.new(@claim, options).serialized_json, status: :accepted + render json: ClaimSerializer.new(@claim, options).serializable_hash.to_json, status: :accepted else logger.error @claim.errors.inspect render json: serialize_errors(@claim.errors), include: @include, status: :unprocessable_entity @@ -137,7 +136,7 @@ def destroy options = {} options[:include] = @include options[:is_collection] = false - render json: ClaimSerializer.new(@claim, options).serialized_json, status: :accepted + render json: ClaimSerializer.new(@claim, options).serializable_hash.to_json, status: :accepted else logger.error @claim.errors.inspect render json: serialize_errors(@claim.errors), include: @include, status: :unprocessable_entity diff --git a/app/controllers/concerns/error_serializable.rb b/app/controllers/concerns/error_serializable.rb index d958e811..2f28b703 100644 --- a/app/controllers/concerns/error_serializable.rb +++ b/app/controllers/concerns/error_serializable.rb @@ -4,20 +4,25 @@ module ErrorSerializable extend ActiveSupport::Concern included do - def serialize_errors(errors) + def serialize_errors(errors, options = {}) return nil if errors.nil? - arr = Array.wrap(errors).reduce([]) do |sum, err| - source = err.keys.first + errors_arr = [] - Array.wrap(err.values.first).each do |title| - sum << { source: source, title: title.is_a?(String) ? title.capitalize : title.to_s } + errors.each do |err| + unless errors_arr.any? { |e| e[:source] == err.attribute } + new_err = { source: err.attribute, title: capitalize_error_message(err) } + new_err[:uid] = options[:uid] if options[:uid].present? + errors_arr << new_err end - - sum end - { errors: arr }.to_json + { errors: errors_arr }.to_json end end + + private + def capitalize_error_message(error) + error.message.sub(/^./, &:upcase) + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 9d445fbf..948af989 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -30,7 +30,7 @@ def show options[:is_collection] = false options[:params] = { current_ability: current_ability } - render json: UserSerializer.new(@user, options).serialized_json, status: :ok + render json: UserSerializer.new(@user, options).serializable_hash.to_json, status: :ok end def index @@ -78,9 +78,9 @@ def index fields = fields_from_params(params) if fields - render json: UserSerializer.new(response.results, options.merge(fields: fields)).serialized_json, status: :ok + render json: UserSerializer.new(response.results, options.merge(fields: fields)).serializable_hash.to_json, status: :ok else - render json: UserSerializer.new(response.results, options).serialized_json, status: :ok + render json: UserSerializer.new(response.results, options).serializable_hash.to_json, status: :ok end rescue Elasticsearch::Transport::Transport::Errors::BadRequest => e Raven.capture_exception(e) @@ -98,7 +98,7 @@ def create if @user.save options = {} options[:is_collection] = false - render json: UserSerializer.new(@user, options).serialized_json, status: :created + render json: UserSerializer.new(@user, options).serializable_hash.to_json, status: :created else logger.error @user.errors.inspect render json: serialize_errors(@user.errors), status: :unprocessable_entity @@ -122,7 +122,7 @@ def update if @user.save options = {} options[:is_collection] = false - render json: UserSerializer.new(@user, options).serialized_json, status: status + render json: UserSerializer.new(@user, options).serializable_hash.to_json, status: status else logger.error @user.errors.inspect render json: serialize_errors(@user.errors), status: :unprocessable_entity diff --git a/app/serializers/claim_serializer.rb b/app/serializers/claim_serializer.rb index dae26edc..63a282b9 100644 --- a/app/serializers/claim_serializer.rb +++ b/app/serializers/claim_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ClaimSerializer - include FastJsonapi::ObjectSerializer + include JSONAPI::Serializer set_key_transform :camel_lower set_type :claims set_id :uuid diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index af60c844..cc6f962d 100755 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class UserSerializer - include FastJsonapi::ObjectSerializer + include JSONAPI::Serializer set_key_transform :camel_lower set_type :users set_id :uid diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index aed620c8..721814a1 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -20,10 +20,10 @@ # Format used for DOI validation # The prefix is 10.x where x is 4-5 digits. The suffix can be anything, but can"t be left off -DOI_FORMAT = %r(\A10\.\d{4,5}/.+).freeze +DOI_FORMAT = %r(\A10\.\d{4,5}/.+) # Format used for URL validation -URL_FORMAT = %r(\A(http|https|ftp):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?\z).freeze +URL_FORMAT = %r(\A(http|https|ftp):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?\z) # Form queue options QUEUE_OPTIONS = ["high", "default", "low"].freeze diff --git a/config/initializers/serialization_core.rb b/config/initializers/serialization_core.rb new file mode 100644 index 00000000..63660e9c --- /dev/null +++ b/config/initializers/serialization_core.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Monkey patch for jsonapi-serializer gem + +module FastJsonapi + module SerializationCore + class_methods do + def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {}) + return unless includes_list.present? + return [] unless relationships_to_serialize + + includes_list = parse_includes_list(includes_list) + + includes_list.each_with_object([]) do |include_item, included_records| + relationship_item = relationships_to_serialize[include_item.first] + + next unless relationship_item&.include_relationship?(record, params) + + associated_objects = relationship_item.fetch_associated_object(record, params) + + if associated_objects.is_a?(Elasticsearch::Model::HashWrapper) + associated_objects = OpenStruct.new(associated_objects) + end + + included_objects = Array(associated_objects) + + next if included_objects.empty? + + static_serializer = relationship_item.static_serializer + static_record_type = relationship_item.static_record_type + + included_objects.each do |inc_obj| + serializer = static_serializer || relationship_item.serializer_for(inc_obj, params) + record_type = static_record_type || serializer.record_type + + if include_item.last.any? + serializer_records = serializer.get_included_records(inc_obj, include_item.last, known_included_objects, fieldsets, params) + included_records.concat(serializer_records) unless serializer_records.empty? + end + + code = "#{record_type}_#{serializer.id_from_record(inc_obj, params)}" + next if known_included_objects.include?(code) + + known_included_objects << code + + included_records << serializer.record_hash(inc_obj, fieldsets[record_type], includes_list, params) + end + end + end + end + end +end diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 00000000..e7b2a67a --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,5 @@ +version: "3.8" + +services: + web: + build: . diff --git a/spec/requests/claims_spec.rb b/spec/requests/claims_spec.rb index c43ed79d..8b52bfc1 100644 --- a/spec/requests/claims_spec.rb +++ b/spec/requests/claims_spec.rb @@ -131,7 +131,7 @@ expect(last_response.status).to eq(422) response = JSON.parse(last_response.body) - expect(response).to eq("errors" => [{ "source" => "user", "title" => "Must exist" }]) + expect(response["errors"]).to include({ "source" => "user", "title" => "Must exist" }) end end @@ -196,7 +196,7 @@ expect(last_response.status).to eq(422) response = JSON.parse(last_response.body) - expect(response).to eq("errors" => [{ "status" => "422", "title" => "param is missing or the value is empty: claim" }]) + expect(response["errors"].first["title"]).to start_with("param is missing or the value is empty: claim") end end diff --git a/vendor/docker/webapp.conf b/vendor/docker/webapp.conf index fb67ac2a..81bc5a48 100644 --- a/vendor/docker/webapp.conf +++ b/vendor/docker/webapp.conf @@ -6,5 +6,7 @@ server { passenger_enabled on; passenger_user app; passenger_ruby /usr/bin/ruby; + passenger_preload_bundler on; + merge_slashes off; }