diff --git a/.hound.yml b/.hound.yml new file mode 100644 index 000000000..6a7cdca1e --- /dev/null +++ b/.hound.yml @@ -0,0 +1,4 @@ +ruby: + config_file: .rubocop.yml +rubocop: + version: 0.77.0 \ No newline at end of file diff --git a/Gemfile b/Gemfile index af332adda..e93041609 100644 --- a/Gemfile +++ b/Gemfile @@ -63,16 +63,21 @@ gem "graphql-errors", "~> 0.4.0" gem "graphql-batch", "~> 0.4.1" gem "batch-loader", "~> 1.4", ">= 1.4.1" gem "graphql-cache", "~> 0.6.0", git: "https://github.com/stackshareio/graphql-cache" -gem "apollo-federation", "~> 0.5.1" +gem 'apollo-federation', '~> 1.0' gem "google-protobuf", "3.10.0.rc.1" gem "sprockets", "~> 3.7", ">= 3.7.2" +gem 'uuid', '~> 2.3', '>= 2.3.9' +gem 'strong_migrations', '~> 0.6.0' +gem 'crawler_detect' +gem 'lhm', '~> 2.2' group :development, :test do gem "rspec-rails", "~> 3.8", ">= 3.8.2" gem "rspec-benchmark", "~> 0.4.0" - gem 'rubocop', '~> 0.77.0' + gem "rspec-graphql_matchers", "~> 1.1" + gem "rubocop", "~> 0.77.0" gem 'rubocop-performance', '~> 1.5', '>= 1.5.1' - gem 'rubocop-rails', '~> 2.4' + gem "rubocop-rails", "~> 2.4" gem "better_errors" gem "binding_of_caller" gem "byebug", platforms: [:mri, :mingw, :x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index 8bac9eca6..7ae6a9476 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -59,21 +59,21 @@ GEM addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) ansi (1.5.0) - apollo-federation (0.5.1) - google-protobuf - graphql + apollo-federation (1.0.1) + google-protobuf (~> 3.7) + graphql (~> 1.9.8) arel (9.0.0) ast (2.4.0) audited (4.9.0) activerecord (>= 4.2, < 6.1) aws-eventstream (1.0.3) - aws-partitions (1.263.0) - aws-sdk-core (3.88.0) + aws-partitions (1.269.0) + aws-sdk-core (3.89.1) aws-eventstream (~> 1.0, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.27.0) + aws-sdk-kms (1.28.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) aws-sdk-s3 (1.60.1) @@ -137,9 +137,9 @@ GEM bootsnap (1.4.5) msgpack (~> 1.0) builder (3.2.4) - byebug (11.0.1) + byebug (11.1.1) cancancan (2.3.0) - capybara (3.30.0) + capybara (3.31.0) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) @@ -151,7 +151,7 @@ GEM activesupport citeproc (1.0.10) namae (~> 1.0) - citeproc-ruby (1.1.10) + citeproc-ruby (1.1.12) citeproc (~> 1.0, >= 1.0.9) csl (~> 1.5) coderay (1.1.2) @@ -169,7 +169,8 @@ GEM sort_alphabetical (~> 1.0) crack (0.4.3) safe_yaml (~> 1.0.0) - crass (1.0.5) + crass (1.0.6) + crawler_detect (0.1.11) csl (1.5.1) namae (~> 1.0) csl-styles (1.0.1.10) @@ -177,7 +178,7 @@ GEM css_parser (1.7.1) addressable dalli (2.7.10) - database_cleaner (1.7.0) + database_cleaner (1.8.1) debug_inspector (0.0.3) diff-lcs (1.3) diffy (3.3.0) @@ -230,7 +231,7 @@ GEM faraday (>= 0.15) fast_jsonapi (1.5) activesupport (>= 4.2) - ffi (1.11.3) + ffi (1.12.1) flipper (0.17.2) flipper-active_support_cache_store (0.17.2) activesupport (>= 4.2, < 7) @@ -241,7 +242,7 @@ GEM globalid (0.4.2) activesupport (>= 4.2.0) google-protobuf (3.10.0.rc.1) - graphql (1.9.17) + graphql (1.9.19) graphql-batch (0.4.2) graphql (>= 1.3, < 2) promise.rb (~> 0.7.2) @@ -257,7 +258,7 @@ GEM htmlentities (4.3.4) http-cookie (1.0.3) domain_name (~> 0.5) - i18n (1.8.1) + i18n (1.8.2) concurrent-ruby (~> 1.0) i18n_data (0.8.0) iso8601 (0.9.1) @@ -280,19 +281,20 @@ GEM oj (~> 3) optimist (~> 3) jwt (2.2.1) - kaminari (1.1.1) + kaminari (1.2.0) activesupport (>= 4.1.0) - kaminari-actionview (= 1.1.1) - kaminari-activerecord (= 1.1.1) - kaminari-core (= 1.1.1) - kaminari-actionview (1.1.1) + kaminari-actionview (= 1.2.0) + kaminari-activerecord (= 1.2.0) + kaminari-core (= 1.2.0) + kaminari-actionview (1.2.0) actionview - kaminari-core (= 1.1.1) - kaminari-activerecord (1.1.1) + kaminari-core (= 1.2.0) + kaminari-activerecord (1.2.0) activerecord - kaminari-core (= 1.1.1) - kaminari-core (1.1.1) + kaminari-core (= 1.2.0) + kaminari-core (1.2.0) latex-decode (0.3.1) + lhm (2.2.0) link_header (0.0.8) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) @@ -309,6 +311,8 @@ GEM loofah (2.4.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) + macaddr (1.7.2) + systemu (~> 2.6.5) mail (2.7.1) mini_mime (>= 0.1.1) mailgun-ruby (1.2.0) @@ -330,7 +334,7 @@ GEM mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2019.1009) - mimemagic (0.3.3) + mimemagic (0.3.4) mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) @@ -346,7 +350,7 @@ GEM nio4r (2.5.2) nokogiri (1.10.7) mini_portile2 (~> 2.4.0) - oj (3.10.0) + oj (3.10.2) oj_mimic_json (1.0.1) optimist (3.0.0) pandoc-ruby (2.0.2) @@ -366,7 +370,7 @@ GEM pwqgen.rb (0.1.0) docopt (~> 0.5) sysrandom - rack (2.1.1) + rack (2.1.2) rack-accept (0.4.5) rack (>= 0.4) rack-cors (1.1.1) @@ -445,6 +449,8 @@ GEM rspec-expectations (3.9.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) + rspec-graphql_matchers (1.1) + graphql (>= 1.8, < 2.0) rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.9.0) @@ -466,7 +472,7 @@ GEM unicode-display_width (>= 1.4.0, < 1.7) rubocop-performance (1.5.2) rubocop (>= 0.71.0) - rubocop-rails (2.4.1) + rubocop-rails (2.4.2) rack (>= 1.1) rubocop (>= 0.72.0) ruby-enum (0.7.2) @@ -509,9 +515,12 @@ GEM regexp_parser (~> 1.3, >= 1.3.0) strip_attributes (1.9.0) activemodel (>= 3.0, < 7.0) + strong_migrations (0.6.1) + activerecord (>= 5) sxp (1.1.0) rdf (~> 3.1) sysrandom (1.0.5) + systemu (2.6.5) temple (0.8.2) test-prof (0.10.2) thor (0.20.3) @@ -527,10 +536,12 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.6) - unicode-display_width (1.6.0) + unicode-display_width (1.6.1) unicode_utils (1.4.0) + uuid (2.3.9) + macaddr (~> 1.0) vcr (3.0.3) - webmock (3.7.6) + webmock (3.8.0) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) @@ -546,7 +557,7 @@ PLATFORMS DEPENDENCIES aasm (~> 5.0, >= 5.0.1) active_model_serializers (~> 0.10.0) - apollo-federation (~> 0.5.1) + apollo-federation (~> 1.0) audited (~> 4.8) aws-sdk-s3 aws-sdk-sqs (~> 1.3) @@ -564,6 +575,7 @@ DEPENDENCIES commonmarker (~> 0.17.9) countries (~> 2.1, >= 2.1.2) country_select (~> 3.1) + crawler_detect dalli (~> 2.7, >= 2.7.6) database_cleaner diffy (~> 3.2, >= 3.2.1) @@ -593,6 +605,7 @@ DEPENDENCIES jsonlint (~> 0.3.0) jwt kaminari (~> 1.0, >= 1.0.1) + lhm (~> 2.2) listen (>= 3.0.5, < 3.2) lograge (~> 0.11.2) logstash-event (~> 1.2, >= 1.2.02) @@ -611,6 +624,7 @@ DEPENDENCIES rails (~> 5.2.0) rake (~> 12.0) rspec-benchmark (~> 0.4.0) + rspec-graphql_matchers (~> 1.1) rspec-rails (~> 3.8, >= 3.8.2) rubocop (~> 0.77.0) rubocop-performance (~> 1.5, >= 1.5.1) @@ -627,8 +641,10 @@ DEPENDENCIES sprockets (~> 3.7, >= 3.7.2) string_pattern strip_attributes (~> 1.8) + strong_migrations (~> 0.6.0) test-prof (~> 0.10.2) turnout (~> 2.5) + uuid (~> 2.3, >= 2.3.9) vcr (~> 3.0.3) webmock (~> 3.1) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 99fd3f4c1..992713e28 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -34,6 +34,14 @@ def set_jsonp_format end end + def detect_crawler + #### Crawlers shound't be making queires + if request.is_crawler? && params[:query].present? + render json: {}, status: :not_found + end + end + + def set_consumer_header if current_user response.headers['X-Credential-Username'] = current_user.uid diff --git a/app/controllers/client_prefixes_controller.rb b/app/controllers/client_prefixes_controller.rb index 3b87ea910..69a0fc9ec 100644 --- a/app/controllers/client_prefixes_controller.rb +++ b/app/controllers/client_prefixes_controller.rb @@ -89,7 +89,7 @@ def create render json: ClientPrefixSerializer.new(@client_prefix, options).serialized_json, status: :created else - logger.error @client_prefix.errors.inspect + Rails.logger.error @client_prefix.errors.inspect render json: serialize_errors(@client_prefix.errors), status: :unprocessable_entity end end diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb index d4bf357e4..6485078af 100644 --- a/app/controllers/clients_controller.rb +++ b/app/controllers/clients_controller.rb @@ -115,7 +115,7 @@ def create render json: ClientSerializer.new(@client, options).serialized_json, status: :created else - logger.error @client.errors.inspect + Rails.logger.error @client.errors.inspect render json: serialize_errors(@client.errors), status: :unprocessable_entity end end @@ -129,7 +129,7 @@ def update render json: ClientSerializer.new(@client, options).serialized_json, status: :ok else - logger.error @client.errors.inspect + Rails.logger.error @client.errors.inspect render json: serialize_errors(@client.errors), status: :unprocessable_entity end end @@ -140,13 +140,13 @@ def destroy if @client.dois.present? message = "Can't delete client that has DOIs." status = 400 - logger.warn message + Rails.logger.warn message render json: { errors: [{ status: status.to_s, title: message }] }.to_json, status: status elsif @client.update(is_active: nil, deleted_at: Time.zone.now) @client.send_delete_email unless Rails.env.test? head :no_content else - logger.error @client.errors.inspect + Rails.logger.error @client.errors.inspect render json: serialize_errors(@client.errors), status: :unprocessable_entity end end @@ -173,16 +173,17 @@ def set_include def set_client @client = Client.where(symbol: params[:id]).where(deleted_at: nil).first - fail ActiveRecord::RecordNotFound unless @client.present? + fail ActiveRecord::RecordNotFound if @client.blank? end private def safe_params - fail JSON::ParserError, "You need to provide a payload following the JSONAPI spec" unless params[:data].present? + fail JSON::ParserError, "You need to provide a payload following the JSONAPI spec" if params[:data].blank? + ActiveModelSerializers::Deserialization.jsonapi_parse!( - params, only: [:symbol, :name, "systemEmail", "contactEmail", :domains, :provider, :url, "repositoryType", { "repositoryType" => [] }, :description, :language, { language: [] }, "alternateName", :software, "targetId", "isActive", "passwordInput", "clientType", :re3data, :opendoar, :issn, { issn: [:issnl, :electronic, :print] }, :certificate, { certificate: [] }, "serviceContact", { "serviceContact": [:email, "givenName", "familyName"] }, "salesforceId"], - keys: { "systemEmail" => :system_email, "contactEmail" => :system_email, "salesforceId" => :salesforce_id, "targetId" => :target_id, "isActive" => :is_active, "passwordInput" => :password_input, "clientType" => :client_type, "alternateName" => :alternate_name, "repositoryType" => :repository_type, "serviceContact" => :service_contact } + params, only: [:symbol, :name, "systemEmail", "contactEmail", "globusUuid", :domains, :provider, :url, "repositoryType", { "repositoryType" => [] }, :description, :language, { language: [] }, "alternateName", :software, "targetId", "isActive", "passwordInput", "clientType", :re3data, :opendoar, :issn, { issn: [:issnl, :electronic, :print] }, :certificate, { certificate: [] }, "serviceContact", { "serviceContact": [:email, "givenName", "familyName"] }, "salesforceId"], + keys: { "systemEmail" => :system_email, "contactEmail" => :system_email, "globusUuid" => :globus_uuid, "salesforceId" => :salesforce_id, "targetId" => :target_id, "isActive" => :is_active, "passwordInput" => :password_input, "clientType" => :client_type, "alternateName" => :alternate_name, "repositoryType" => :repository_type, "serviceContact" => :service_contact } ) end end diff --git a/app/controllers/dois_controller.rb b/app/controllers/dois_controller.rb index 34a0cf5d3..c509e4158 100644 --- a/app/controllers/dois_controller.rb +++ b/app/controllers/dois_controller.rb @@ -22,6 +22,12 @@ def index when "-updated" then { updated: { order: 'desc' }} when "published" then { published: { order: 'asc' }} when "-published" then { published: { order: 'desc' }} + when "view-count" then { view_count: { order: 'asc' }} + when "-view-count" then { view_count: { order: 'desc' }} + when "download-count" then { download_count: { order: 'asc' }} + when "-download-count" then { download_count: { order: 'desc' }} + when "citation-count" then { citation_count: { order: 'asc' }} + when "-citation-count" then { citation_count: { order: 'desc' }} when "relevance" then { "_score": { "order": "desc" }} else { updated: { order: 'desc' }} end @@ -305,6 +311,7 @@ def show options[:is_collection] = false options[:params] = { current_ability: current_ability, + events: params[:events], detail: true, mix_in: params[:mix_in], affiliation: params[:affiliation], @@ -527,9 +534,18 @@ def status def set_include if params[:include].present? @include = params[:include].split(",").map { |i| i.downcase.underscore.to_sym } - @include = @include & [:client, :media] + + if params[:events].present? + @include = @include & [:client, :media, :views, :downloads] + else + @include = @include & [:client, :media] + end else - @include = [:client, :media] + if params[:events].present? + @include = [:client, :media, :views, :downloads] + else + @include = [:client, :media] + end end end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index feb5e40e9..861043a70 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -8,6 +8,7 @@ class EventsController < ApplicationController include BatchLoaderHelper prepend_before_action :authenticate_user!, except: [:index, :show] + before_action :detect_crawler before_action :load_event, only: [:show, :destroy] before_action :set_include, only: [:index, :show, :create, :update] authorize_resource only: [:destroy] diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 87fbc87be..440ea63f6 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -34,8 +34,8 @@ def ensure_hash(ambiguous_param) end def handle_error_in_development(e) - logger.error e.message - logger.error e.backtrace.join("\n") + Rails.logger.error e.message + Rails.logger.error e.backtrace.join("\n") render json: { error: { message: e.message, backtrace: e.backtrace }, data: {} }, status: 500 end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index b293864e0..4adc548ff 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -60,7 +60,7 @@ def create render json: MediaSerializer.new(@media, options).serialized_json, status: :created else - logger.error @media.errors.inspect + Rails.logger.error @media.errors.inspect render json: serialize_errors(@media.errors), status: :unprocessable_entity end end @@ -75,7 +75,7 @@ def update render json: MediaSerializer.new(@media, options).serialized_json, status: :ok else - logger.error @media.errors.inspect + Rails.logger.error @media.errors.inspect render json: serialize_errors(@media.errors), status: :unprocessable_entity end end @@ -86,7 +86,7 @@ def destroy if @media.destroy head :no_content else - logger.error @media.errors.inspect + Rails.logger.error @media.errors.inspect render json: serialize_errors(@media.errors), status: :unprocessable_entity end end diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb index c4b35c42c..47c7d5772 100644 --- a/app/controllers/metadata_controller.rb +++ b/app/controllers/metadata_controller.rb @@ -65,7 +65,7 @@ def create render json: MetadataSerializer.new(@metadata, options).serialized_json, status: :created else - logger.error @metadata.errors.inspect + Rails.logger.error @metadata.errors.inspect render json: serialize_errors(@metadata.errors), status: :unprocessable_entity end end @@ -77,7 +77,7 @@ def destroy if @metadata.destroy head :no_content else - logger.error @metadata.errors.inspect + Rails.logger.error @metadata.errors.inspect render json: serialize_errors(@metadata.errors), status: :unprocessable_entity end else diff --git a/app/controllers/old_events_controller.rb b/app/controllers/old_events_controller.rb index dba7d2b62..7749d7928 100644 --- a/app/controllers/old_events_controller.rb +++ b/app/controllers/old_events_controller.rb @@ -5,6 +5,7 @@ class OldEventsController < ApplicationController include Facetable prepend_before_action :authenticate_user!, except: [:index, :show] + before_action :detect_crawler before_action :load_event, only: [:show, :destroy] before_action :set_include, only: [:index, :show, :create, :update] authorize_resource only: [:destroy] diff --git a/app/controllers/provider_prefixes_controller.rb b/app/controllers/provider_prefixes_controller.rb index 9dbb8b3e0..c8f4d6026 100644 --- a/app/controllers/provider_prefixes_controller.rb +++ b/app/controllers/provider_prefixes_controller.rb @@ -123,7 +123,7 @@ def create render json: ProviderPrefixSerializer.new(@provider_prefix, options).serialized_json, status: :created else - logger.error @provider_prefix.errors.inspect + Rails.logger.error @provider_prefix.errors.inspect render json: serialize_errors(@provider_prefix.errors), status: :unprocessable_entity end end diff --git a/app/controllers/providers_controller.rb b/app/controllers/providers_controller.rb index eecf117b3..d139af87c 100644 --- a/app/controllers/providers_controller.rb +++ b/app/controllers/providers_controller.rb @@ -211,7 +211,7 @@ def create render json: ProviderSerializer.new(@provider, options).serialized_json, status: :ok else - logger.error @provider.errors.inspect + Rails.logger.error @provider.errors.inspect render json: serialize_errors(@provider.errors), status: :unprocessable_entity end end @@ -243,7 +243,7 @@ def update render json: ProviderSerializer.new(@provider, options).serialized_json, status: :ok else - logger.error @provider.errors.inspect + Rails.logger.error @provider.errors.inspect render json: serialize_errors(@provider.errors), status: :unprocessable_entity end end @@ -254,13 +254,13 @@ def destroy if active_client_count(provider_id: @provider.symbol).positive? message = "Can't delete provider that has active clients." status = 400 - logger.warn message + Rails.logger.warn message render json: { errors: [{ status: status.to_s, title: message }] }.to_json, status: status elsif @provider.update_attributes(is_active: nil, deleted_at: Time.zone.now) @provider.send_delete_email unless Rails.env.test? head :no_content else - logger.error @provider.errors.inspect + Rails.logger.error @provider.errors.inspect render json: serialize_errors(@provider.errors), status: :unprocessable_entity end end @@ -303,7 +303,7 @@ def safe_params ActiveModelSerializers::Deserialization.jsonapi_parse!( params, only: [ - :name, "displayName", :symbol, :description, :website, :joined, "organizationType", "focusArea", :consortium, "systemEmail", "groupEmail", "isActive", "passwordInput", :country, "billingInformation", { "billingInformation": ["postCode", :state, :city, :address, :department, :organization, :country]}, "rorId", "twitterHandle","memberType", "nonProfitStatus", "salesforceId", + :name, "displayName", :symbol, :description, :website, :joined, "globusUuid", "organizationType", "focusArea", :consortium, "systemEmail", "groupEmail", "isActive", "passwordInput", :country, "billingInformation", { "billingInformation": ["postCode", :state, :city, :address, :department, :organization, :country]}, "rorId", "twitterHandle","memberType", "nonProfitStatus", "salesforceId", "technicalContact", { "technicalContact": [:email, "givenName", "familyName"] }, "secondaryTechnicalContact", { "secondaryTechnicalContact": [:email, "givenName", "familyName"] }, "secondaryBillingContact", { "secondaryBillingContact": [:email, "givenName", "familyName"] }, @@ -325,7 +325,8 @@ def safe_params "groupEmail" => :group_email, "systemEmail" => :system_email, "nonProfitStatus" => :non_profit_status, - "salesforceId" => :salesforce_id + "salesforceId" => :salesforce_id, + "globusUuid" => :globus_uuid } ) end diff --git a/app/controllers/repositories_controller.rb b/app/controllers/repositories_controller.rb index c930e5348..0674a1c48 100644 --- a/app/controllers/repositories_controller.rb +++ b/app/controllers/repositories_controller.rb @@ -5,7 +5,7 @@ class RepositoriesController < ApplicationController before_action :set_repository, only: [:show, :update, :destroy] before_action :authenticate_user! before_action :set_include - load_and_authorize_resource :client, :parent => false, :except => [:index, :show, :totals, :random] + load_and_authorize_resource :client, parent: false, except: [:index, :show, :totals, :random] def index sort = case params[:sort] @@ -65,7 +65,7 @@ def index options[:links] = { self: request.original_url, - next: response.results.blank? ? nil : request.base_url + "/clients?" + { + next: response.results.blank? ? nil : request.base_url + "/repositories?" + { query: params[:query], "provider-id" => params[:provider_id], software: params[:software], @@ -141,7 +141,7 @@ def create render json: RepositorySerializer.new(@client, options).serialized_json, status: :created else - logger.error @client.errors.inspect + Rails.logger.error @client.errors.inspect render json: serialize_errors(@client.errors), status: :unprocessable_entity end end @@ -155,7 +155,7 @@ def update render json: RepositorySerializer.new(@client, options).serialized_json, status: :ok else - logger.error @client.errors.inspect + Rails.logger.error @client.errors.inspect render json: serialize_errors(@client.errors), status: :unprocessable_entity end end @@ -166,13 +166,13 @@ def destroy if @client.dois.present? message = "Can't delete repository that has DOIs." status = 400 - logger.warn message + Rails.logger.warn message render json: { errors: [{ status: status.to_s, title: message }] }.to_json, status: status elsif @client.update_attributes(is_active: nil, deleted_at: Time.zone.now) @client.send_delete_email unless Rails.env.test? head :no_content else - logger.error @client.errors.inspect + Rails.logger.error @client.errors.inspect render json: serialize_errors(@client.errors), status: :unprocessable_entity end end @@ -188,7 +188,7 @@ def totals state = current_user.present? && current_user.is_admin_or_staff? && params[:state].present? ? params[:state] : "registered,findable" response = Doi.query(nil, provider_id: params[:provider_id], state: state, page: page, totals_agg: true) registrant = clients_totals(response.response.aggregations.clients_totals.buckets) - + render json: registrant, status: :ok end @@ -205,16 +205,17 @@ def set_include def set_repository @client = Client.where(symbol: params[:id]).where(deleted_at: nil).first - fail ActiveRecord::RecordNotFound unless @client.present? + fail ActiveRecord::RecordNotFound if @client.blank? end private def safe_params - fail JSON::ParserError, "You need to provide a payload following the JSONAPI spec" unless params[:data].present? + fail JSON::ParserError, "You need to provide a payload following the JSONAPI spec" if params[:data].blank? + ActiveModelSerializers::Deserialization.jsonapi_parse!( - params, only: [:symbol, :name, "systemEmail", :domains, :provider, :url, "repositoryType", { "repositoryType" => [] }, :description, :language, { language: [] }, "alternateName", :software, "targetId", "isActive", "passwordInput", "clientType", :re3data, :opendoar, :issn, { issn: [:issnl, :electronic, :print] }, :certificate, { certificate: [] }, "serviceContact", { "serviceContact": [:email, "givenName", "familyName"] }, "salesforceId"], - keys: { "systemEmail" => :system_email, "salesforceId" => :salesforce_id, "targetId" => :target_id, "isActive" => :is_active, "passwordInput" => :password_input, "clientType" => :client_type, "alternateName" => :alternate_name, "repositoryType" => :repository_type, "serviceContact" => :service_contact } + params, only: [:symbol, :name, "systemEmail", :domains, :provider, :url, "globusUuid", "repositoryType", { "repositoryType" => [] }, :description, :language, { language: [] }, "alternateName", :software, "targetId", "isActive", "passwordInput", "clientType", :re3data, :opendoar, :issn, { issn: [:issnl, :electronic, :print] }, :certificate, { certificate: [] }, "serviceContact", { "serviceContact": [:email, "givenName", "familyName"] }, "salesforceId"], + keys: { "systemEmail" => :system_email, "salesforceId" => :salesforce_id, "globusUuid" => :globus_uuid, "targetId" => :target_id, "isActive" => :is_active, "passwordInput" => :password_input, "clientType" => :client_type, "alternateName" => :alternate_name, "repositoryType" => :repository_type, "serviceContact" => :service_contact } ) end end diff --git a/app/controllers/repository_prefixes_controller.rb b/app/controllers/repository_prefixes_controller.rb index af714a968..5b6a92e47 100644 --- a/app/controllers/repository_prefixes_controller.rb +++ b/app/controllers/repository_prefixes_controller.rb @@ -89,7 +89,7 @@ def create render json: RepositoryPrefixSerializer.new(@client_prefix, options).serialized_json, status: :created else - logger.error @client_prefix.errors.inspect + Rails.logger.error @client_prefix.errors.inspect render json: serialize_errors(@client_prefix.errors), status: :unprocessable_entity end end diff --git a/app/graphql/schema.graphql b/app/graphql/schema.graphql new file mode 100644 index 000000000..a7fbb6f06 --- /dev/null +++ b/app/graphql/schema.graphql @@ -0,0 +1,5040 @@ +""" +Information about addresses +""" +type Address { + """ + The country. + """ + addressCountry: String + + """ + The locality in which the street address is, and which is in the region. + """ + addressLocality: String + + """ + The region. + """ + addressRegion: String + + """ + The postal code. + """ + postalCode: String + + """ + The street address. + """ + streetAddress: String + + """ + The type. + """ + type: String +} + +type Audiovisual implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +Information about clients +""" +type Client { + """ + Client alternate name + """ + alternateName: String + + """ + Datasets managed by the client + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): ClientDatasetConnectionWithMeta! + + """ + Description of the client + """ + description: String + + """ + Unique identifier for each client + """ + id: ID! + + """ + Client name + """ + name: String! + + """ + Prefixes managed by the client + """ + prefixes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + year: String + ): PrefixConnectionWithMeta! + + """ + Publications managed by the client + """ + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): ClientPublicationConnectionWithMeta! + + """ + The re3data identifier for the client + """ + re3data: String + + """ + The name of the software that is used to run the repository + """ + software: String + + """ + Software managed by the client + """ + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): ClientSoftwareConnectionWithMeta! + + """ + Client system email + """ + systemEmail: String + + """ + The type of the item. + """ + type: String! + + """ + The homepage of the client + """ + url: String +} + +""" +The connection type for Client. +""" +type ClientConnectionWithMeta { + """ + A list of edges. + """ + edges: [ClientEdge] + + """ + A list of nodes. + """ + nodes: [Client] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for Dataset. +""" +type ClientDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [DatasetEdge] + + """ + A list of nodes. + """ + nodes: [Dataset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ClientEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Client +} + +""" +The connection type for Dataset. +""" +type ClientPublicationConnectionWithMeta { + """ + A list of edges. + """ + edges: [DatasetEdge] + + """ + A list of nodes. + """ + nodes: [Dataset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for Dataset. +""" +type ClientSoftwareConnectionWithMeta { + """ + A list of edges. + """ + edges: [DatasetEdge] + + """ + A list of nodes. + """ + nodes: [Dataset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +type Collection implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +Information about countries +""" +type Country { + """ + Country ISO 3166-1 code + """ + code: String + + """ + Country name + """ + name: String +} + +type CreativeWork implements DoiItem & MetricInterface { + """ + The count of DOI events that represents citations + """ + citationCount: Int + + """ + The citations distribution overtime + """ + citationHistogram: JSON + + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + The count of DOI dowloands according to the COUNTER code of Practice + """ + downloadCount: Int + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + The count of DOI events that represents references + """ + referenceCount: Int + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The count of DOI events that represents relations + """ + relationCount: Int + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String + + """ + The count of DOI views according to the COUNTER code of Practice + """ + viewCount: Int +} + +""" +The connection type for CreativeWork. +""" +type CreativeWorkConnectionWithMeta { + """ + A list of edges. + """ + edges: [CreativeWorkEdge] + + """ + A list of nodes. + """ + nodes: [CreativeWork] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type CreativeWorkEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: CreativeWork +} + +""" +A collection of datasets. +""" +type DataCatalog { + """ + An alias for the data catalog. + """ + alternateName: [String!] + + """ + Certificates of the data catalog. + """ + certificates: [DefinedTerm!] + + """ + Datasets hosted by the repository + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): DataCatalogDatasetConnectionWithMeta! + + """ + A description of the data catalog. + """ + description: String + + """ + The ID of the data catalog. + """ + id: ID! + + """ + re3data ID + """ + identifier: [Identifier!] + + """ + The language of the content of the data catalog. + """ + inLanguage: [String!] + + """ + Keywords or tags used to describe this data catalog. Multiple entries in a + keywords list are typically delimited by commas. + """ + keywords: String + + """ + The name of the data catalog. + """ + name: String + + """ + Publications hosted by the repository + """ + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): DataCatalogPublicationConnectionWithMeta! + + """ + Software + """ + softwareApplication: [SoftwareApplication!] + + """ + Software hosted by the repository + """ + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): DataCatalogSoftwareConnectionWithMeta! + + """ + Subject areas covered by the data catalog. + """ + subjects: [DefinedTerm!] + + """ + The type of the item. + """ + type: String! + + """ + URL of the data catalog. + """ + url: String +} + +""" +The connection type for DataCatalog. +""" +type DataCatalogConnectionWithMeta { + datasetConnectionCount: Int! + + """ + A list of edges. + """ + edges: [DataCatalogEdge] + + """ + A list of nodes. + """ + nodes: [DataCatalog] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int +} + +""" +The connection type for Dataset. +""" +type DataCatalogDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [DatasetEdge] + + """ + A list of nodes. + """ + nodes: [Dataset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DataCatalogEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: DataCatalog +} + +""" +The connection type for Dataset. +""" +type DataCatalogPublicationConnectionWithMeta { + """ + A list of edges. + """ + edges: [DatasetEdge] + + """ + A list of nodes. + """ + nodes: [Dataset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for Dataset. +""" +type DataCatalogSoftwareConnectionWithMeta { + """ + A list of edges. + """ + edges: [DatasetEdge] + + """ + A list of nodes. + """ + nodes: [Dataset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +type DataPaper implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +type Dataset implements DoiItem & MetricInterface { + """ + The count of DOI events that represents citations + """ + citationCount: Int + + """ + The citations distribution overtime + """ + citationHistogram: JSON + + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Referenced datasets + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DatasetDatasetConnectionWithMeta! + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + The count of DOI dowloands according to the COUNTER code of Practice + """ + downloadCount: Int + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + Referenced publications + """ + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): DatasetPublicationConnectionWithMeta! + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + The count of DOI events that represents references + """ + referenceCount: Int + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The count of DOI events that represents relations + """ + relationCount: Int + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Referenced software + """ + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DatasetSoftwareConnectionWithMeta! + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + Usage reports for this dataset + """ + usageReports( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): DatasetUsageReportConnectionWithMeta! + + """ + The version number of the resource + """ + version: String + + """ + The count of DOI views according to the COUNTER code of Practice + """ + viewCount: Int +} + +""" +The connection type for Dataset. +""" +type DatasetConnectionWithMeta { + datasetConnectionCount: Int! + + """ + A list of edges. + """ + edges: [DatasetEdge] + funderConnectionCount: Int! + + """ + A list of nodes. + """ + nodes: [Dataset] + organizationConnectionCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + personConnectionCount: Int! + publicationConnectionCount: Int! + softwareConnectionCount: Int! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type DatasetDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type DatasetEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Dataset +} + +""" +The connection type for EventData. +""" +type DatasetPublicationConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type DatasetSoftwareConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type DatasetUsageReportConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +Information about dates +""" +type Date { + """ + Any rights information for this resource + """ + date: String + + """ + The type of date + """ + dateType: String +} + +""" +A word, name, acronym, phrase, etc. with a formal definition. Often used in the +context of category or subject classification, glossaries or dictionaries, +product or creative work types, etc. +""" +type DefinedTerm { + """ + A description of the item. + """ + description: String + + """ + A DefinedTermSet that contains this term. + """ + inDefinedTermSet: String + + """ + The name of the item. + """ + name: String + + """ + A code that identifies this DefinedTerm within a DefinedTermSet. + """ + termCode: String +} + +""" +Information about descriptions +""" +type Description { + """ + Description + """ + description: String + + """ + Description type + """ + descriptionType: String + + """ + Language + """ + lang: ID +} + +""" +Information about DOIs +""" +interface DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +type Event implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +type EventData implements DoiItem & MetricInterface { + """ + The count of DOI events that represents citations + """ + citationCount: Int + + """ + The citations distribution overtime + """ + citationHistogram: JSON + + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + The count of DOI dowloands according to the COUNTER code of Practice + """ + downloadCount: Int + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + The count of DOI events that represents references + """ + referenceCount: Int + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The count of DOI events that represents relations + """ + relationCount: Int + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String + + """ + The count of DOI views according to the COUNTER code of Practice + """ + viewCount: Int +} + +""" +An edge in a connection. +""" +type EventDataEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: EventData + + """ + Relation type for this event. + """ + relationType: String + + """ + Source for this event + """ + source: String + + """ + The source ID of the event. + """ + sourceId: String + + """ + The target ID of the event. + """ + targetId: String + + """ + Total count for this event. + """ + total: Int +} + +""" +Summary information +""" +type Facet { + """ + Count + """ + count: Int + + """ + ID + """ + id: String + + """ + Title + """ + title: String +} + +""" +Information about funders +""" +type Funder { + """ + Physical address of the funder. + """ + address: Address + + """ + An alias for the funder. + """ + alternateName: [String!] + + """ + Funded datasets + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FunderDatasetConnectionWithMeta! + + """ + Crossref Funder ID + """ + id: ID! + + """ + The name of the funder. + """ + name: String! + + """ + Funded publications + """ + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): FunderPublicationConnectionWithMeta! + + """ + Funded software + """ + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): FunderSoftwareConnectionWithMeta! + + """ + The type of the item. + """ + type: String! +} + +""" +The connection type for Funder. +""" +type FunderConnectionWithMeta { + datasetConnectionCount: Int! + + """ + A list of edges. + """ + edges: [FunderEdge] + + """ + A list of nodes. + """ + nodes: [Funder] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + publicationConnectionCount: Int! + softwareConnectionCount: Int! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type FunderDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type FunderEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Funder +} + +""" +The connection type for EventData. +""" +type FunderPublicationConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type FunderSoftwareConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +Information about funding +""" +type Funding { + """ + Award number + """ + awardNumber: String + + """ + Award title + """ + awardTitle: String + + """ + Award URI + """ + awardUri: String + + """ + Funder identifier + """ + funderIdentifier: String + + """ + Funder identifier type + """ + funderIdentifierType: String + + """ + Funder name + """ + funderName: String +} + +""" +Information about identifiers +""" +type Identifier { + """ + The name of the identifier. + """ + name: String + + """ + The value of the identifier. + """ + value: String +} + +type Image implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +type InteractiveResource implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +Represents untyped JSON +""" +scalar JSON + +interface MetricInterface { + """ + The count of DOI events that represents citations + """ + citationCount: Int + + """ + The citations distribution overtime + """ + citationHistogram: JSON + + """ + The count of DOI dowloands according to the COUNTER code of Practice + """ + downloadCount: Int + + """ + The count of DOI events that represents references + """ + referenceCount: Int + + """ + The count of DOI events that represents relations + """ + relationCount: Int + + """ + The count of DOI views according to the COUNTER code of Practice + """ + viewCount: Int +} + +type Model implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +Information about organizations +""" +type Organization { + """ + Physical address of the organization. + """ + address: Address + + """ + An alias for the organization. + """ + alternateName: [String!] + + """ + Datasets from this organization + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationDatasetConnectionWithMeta! + + """ + ROR ID + """ + id: ID! + + """ + The identifier(s) for the organization. + """ + identifier: [Identifier!] + + """ + The name of the organization. + """ + name: String! + + """ + Publications from this organization + """ + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): OrganizationPublicationConnectionWithMeta! + + """ + Software from this organization + """ + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): OrganizationSoftwareConnectionWithMeta! + + """ + The type of the item. + """ + type: String! + + """ + URL of the organization. + """ + url: [String!] +} + +""" +The connection type for Organization. +""" +type OrganizationConnectionWithMeta { + """ + A list of edges. + """ + edges: [OrganizationEdge] + + """ + A list of nodes. + """ + nodes: [Organization] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + personConnectionCount: Int! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type OrganizationDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type OrganizationEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Organization +} + +""" +The connection type for EventData. +""" +type OrganizationPublicationConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type OrganizationSoftwareConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +type Other implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +Information about pagination in a connection. +""" +type PageInfo { + """ + When paginating forwards, the cursor to continue. + """ + endCursor: String + + """ + When paginating forwards, are there more items? + """ + hasNextPage: Boolean! + + """ + When paginating backwards, are there more items? + """ + hasPreviousPage: Boolean! + + """ + When paginating backwards, the cursor to continue. + """ + startCursor: String +} + +""" +A person. +""" +type Person { + """ + The count of DOI events that represents citations + """ + citationCount: Int + + """ + Authored creative works + """ + creativeWorks( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PersonCreativeWorkConnectionWithMeta + + """ + Authored datasets + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PersonDatasetConnectionWithMeta + + """ + The count of DOI dowloands according to the COUNTER code of Practice + """ + downloadCount: Int + + """ + Family name. In the U.S., the last name of an Person. + """ + familyName: String + + """ + Given name. In the U.S., the first name of a Person. + """ + givenName: String + + """ + The ORCID ID of the person. + """ + id: ID + + """ + The name of the person. + """ + name: String + + """ + Authored publications + """ + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PersonPublicationConnectionWithMeta + + """ + Authored software + """ + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PersonSoftwareConnectionWithMeta + + """ + The type of the item. + """ + type: String! + + """ + The count of DOI views according to the COUNTER code of Practice + """ + viewCount: Int +} + +""" +The connection type for Person. +""" +type PersonConnectionWithMeta { + datasetConnectionCount: Int! + + """ + A list of edges. + """ + edges: [PersonEdge] + + """ + A list of nodes. + """ + nodes: [Person] + organizationConnectionCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + publicationConnectionCount: Int! + softwareConnectionCount: Int! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type PersonCreativeWorkConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type PersonDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type PersonEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Person +} + +""" +The connection type for EventData. +""" +type PersonPublicationConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type PersonSoftwareConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +type PhysicalObject implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +Information about prefixes +""" +type Prefix { + """ + Unique identifier for each prefix + """ + id: ID! +} + +""" +The connection type for Prefix. +""" +type PrefixConnectionWithMeta { + """ + A list of edges. + """ + edges: [PrefixEdge] + + """ + A list of nodes. + """ + nodes: [Prefix] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + states: [Facet!]! + totalCount: Int! + years: [Facet!]! +} + +""" +An edge in a connection. +""" +type PrefixEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Prefix +} + +""" +Information about providers +""" +type Provider { + """ + Clients associated with the provider + """ + clients( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + software: String + year: String + ): ProviderClientConnectionWithMeta! + + """ + Country where the provider is located + """ + country: Country + + """ + Description of the provider + """ + description: String + + """ + Provider display name + """ + displayName: String + + """ + Field of science covered by provider + """ + focusArea: String + + """ + Provider contact email + """ + groupEmail: String + + """ + Unique identifier for each provider + """ + id: ID! + + """ + Date provider joined DataCite + """ + joined: String + + """ + URL for the provider logo + """ + logoUrl: String + + """ + Provider name + """ + name: String! + + """ + Type of organization + """ + organizationType: String + + """ + Prefixes managed by the provider + """ + prefixes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + state: String + year: String + ): PrefixConnectionWithMeta! + + """ + Geographic region where the provider is located + """ + region: String + + """ + Research Organization Registry (ROR) identifier + """ + rorId: String! + + """ + The type of the item. + """ + type: String! + + """ + Website of the provider + """ + website: String +} + +""" +The connection type for Client. +""" +type ProviderClientConnectionWithMeta { + """ + A list of edges. + """ + edges: [ClientEdge] + + """ + A list of nodes. + """ + nodes: [Client] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for Provider. +""" +type ProviderConnectionWithMeta { + """ + A list of edges. + """ + edges: [ProviderEdge] + + """ + A list of nodes. + """ + nodes: [Provider] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type ProviderEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: Provider +} + +type Publication implements DoiItem & MetricInterface { + """ + The count of DOI events that represents citations + """ + citationCount: Int + + """ + The citations distribution overtime + """ + citationHistogram: JSON + + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Referenced datasets + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PublicationDatasetConnectionWithMeta! + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + The count of DOI dowloands according to the COUNTER code of Practice + """ + downloadCount: Int + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + Referenced publications + """ + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): PublicationPublicationConnectionWithMeta! + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + The count of DOI events that represents references + """ + referenceCount: Int + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The count of DOI events that represents relations + """ + relationCount: Int + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Referenced software + """ + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): PublicationSoftwareConnectionWithMeta! + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String + + """ + The count of DOI views according to the COUNTER code of Practice + """ + viewCount: Int +} + +""" +The connection type for Dataset. +""" +type PublicationConnectionWithMeta { + datasetConnectionCount: Int! + + """ + A list of edges. + """ + edges: [DatasetEdge] + funderConnectionCount: Int! + + """ + A list of nodes. + """ + nodes: [Dataset] + organizationConnectionCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + personConnectionCount: Int! + publicationConnectionCount: Int! + softwareConnectionCount: Int! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type PublicationDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type PublicationPublicationConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type PublicationSoftwareConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +type Query { + _service: _Service! + audiovisual(id: ID!): Audiovisual! + audiovisuals(first: Int = 25, query: String): [Audiovisual!]! + client(id: ID!): Client! + clients( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + software: String + year: String + ): ClientConnectionWithMeta! + collection(id: ID!): Collection! + collections(first: Int = 25, query: String): [Collection!]! + creativeWork(id: ID!): CreativeWork! + creativeWorks( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + clientId: String + first: Int = 25 + ids: String + + """ + Returns the last _n_ elements from the list. + """ + last: Int + providerId: String + query: String + ): CreativeWorkConnectionWithMeta! + dataCatalog(id: ID!): DataCatalog! + dataCatalogs( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): DataCatalogConnectionWithMeta! + dataPaper(id: ID!): DataPaper! + dataPapers(first: Int = 25, query: String): [DataPaper!]! + dataset(id: ID!): Dataset! + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + clientId: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + providerId: String + query: String + ): DatasetConnectionWithMeta! + event(id: ID!): Event! + events(first: Int = 25, query: String): [Event!]! + funder(id: ID!): Funder! + funders( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): FunderConnectionWithMeta! + image(id: ID!): Image! + images(first: Int = 25, query: String): [Image!]! + interactiveResource(id: ID!): InteractiveResource! + interactiveResources(first: Int = 25, query: String): [InteractiveResource!]! + model(id: ID!): Model! + models(first: Int = 25, query: String): [Model!]! + organization(id: ID!): Organization! + organizations( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): OrganizationConnectionWithMeta! + other(id: ID!): Other! + others(first: Int = 25, query: String): [Other!]! + people( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): PersonConnectionWithMeta! + person(id: ID!): Person + physicalObject(id: ID!): PhysicalObject! + physicalObjects(first: Int = 25, query: String): [PhysicalObject!]! + prefix(id: ID!): Prefix! + prefixes(first: Int = 25, query: String): [Prefix!]! + provider(id: ID!): Provider! + providers( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): ProviderConnectionWithMeta! + publication(id: ID!): Publication! + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + clientId: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + providerId: String + query: String + ): PublicationConnectionWithMeta! + service(id: ID!): Service! + services( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + clientId: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + providerId: String + query: String + ): ServiceConnectionWithMeta! + softwareSourceCode(id: ID!): Software! + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + clientId: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + providerId: String + query: String + ): SoftwareConnectionWithMeta! + sound(id: ID!): Sound! + sounds(first: Int = 25, query: String): [Sound!]! + usageReport(id: ID!): UsageReport! + usageReports( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UsageReportConnectionWithMeta! + workflow(id: ID!): Workflow! + workflows(first: Int = 25, query: String): [Workflow!]! +} + +""" +Information about related identifiers +""" +type RelatedIdentifier { + """ + Related identifier + """ + relatedIdentifier: String + + """ + Related identifier type + """ + relatedIdentifierType: String + + """ + Related metadata scheme + """ + relatedMetadataScheme: String + + """ + Relation type + """ + relationType: String + + """ + Resource type general + """ + resourceTypeGeneral: String + + """ + Scheme type + """ + schemeType: String + + """ + Scheme URI + """ + schemeUri: String +} + +""" +Information about reporting periods +""" +type ReportingPeriod { + """ + Begin reporting period + """ + beginDate: String + + """ + End reporting period + """ + endDate: String +} + +""" +Information about types +""" +type ResourceType { + """ + BibTex + """ + bibtex: String + + """ + Citeproc + """ + citeproc: String + + """ + Resource type + """ + resourceType: String + + """ + Resource type general + """ + resourceTypeGeneral: String + + """ + RIS + """ + ris: String + + """ + Schema.org + """ + schemaOrg: String +} + +""" +Information about rights +""" +type Rights { + """ + Language + """ + lang: String + + """ + Any rights information for this resource + """ + rights: String + + """ + The URI of the license + """ + rightsUri: String +} + +type Service implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +The connection type for Dataset. +""" +type ServiceConnectionWithMeta { + """ + A list of edges. + """ + edges: [DatasetEdge] + + """ + A list of nodes. + """ + nodes: [Dataset] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +type Software implements DoiItem & MetricInterface { + """ + The count of DOI events that represents citations + """ + citationCount: Int + + """ + The citations distribution overtime + """ + citationHistogram: JSON + + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Referenced datasets + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SoftwareDatasetConnectionWithMeta! + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + The count of DOI dowloands according to the COUNTER code of Practice + """ + downloadCount: Int + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + Referenced publications + """ + publications( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + query: String + ): SoftwarePublicationConnectionWithMeta! + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + The count of DOI events that represents references + """ + referenceCount: Int + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The count of DOI events that represents relations + """ + relationCount: Int + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Referenced software + """ + softwareSourceCodes( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): SoftwareSoftwareConnectionWithMeta! + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String + + """ + The count of DOI views according to the COUNTER code of Practice + """ + viewCount: Int +} + +""" +A software application. +""" +type SoftwareApplication { + """ + A description of the item. + """ + description: String + + """ + The name of the item. + """ + name: String + + """ + Version of the software instance. + """ + softwareVersion: String + + """ + URL of the item. + """ + url: String +} + +""" +The connection type for Dataset. +""" +type SoftwareConnectionWithMeta { + datasetConnectionCount: Int! + + """ + A list of edges. + """ + edges: [DatasetEdge] + funderConnectionCount: Int! + + """ + A list of nodes. + """ + nodes: [Dataset] + organizationConnectionCount: Int! + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + personConnectionCount: Int! + publicationConnectionCount: Int! + softwareConnectionCount: Int! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type SoftwareDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type SoftwarePublicationConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type SoftwareSoftwareConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +type Sound implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +Subject information +""" +type Subject { + """ + Language + """ + lang: ID + + """ + The URI of the subject identifier scheme + """ + schemeUri: String + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subject: String + + """ + The name of the subject scheme or classification code or authority if one is used + """ + subjectScheme: String + + """ + The URI of the subject term + """ + valueUri: String +} + +""" +Information about titles +""" +type Title { + """ + Language + """ + lang: String + + """ + Title + """ + title: String + + """ + Title type + """ + titleType: String +} + +""" +Information about usage reports +""" +type UsageReport { + """ + Client who created the report + """ + clientId: String + + """ + Datasets included in usage report + """ + datasets( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + first: Int = 25 + + """ + Returns the last _n_ elements from the list. + """ + last: Int + ): UsageReportDatasetConnectionWithMeta! + + """ + Date information was created + """ + dateCreated: String! + + """ + Usage report ID + """ + id: ID! + + """ + Time period covered by the report + """ + reportingPeriod: ReportingPeriod! +} + +""" +The connection type for UsageReport. +""" +type UsageReportConnectionWithMeta { + """ + A list of edges. + """ + edges: [UsageReportEdge] + + """ + A list of nodes. + """ + nodes: [UsageReport] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +The connection type for EventData. +""" +type UsageReportDatasetConnectionWithMeta { + """ + A list of edges. + """ + edges: [EventDataEdge] + + """ + A list of nodes. + """ + nodes: [EventData] + + """ + Information to aid in pagination. + """ + pageInfo: PageInfo! + totalCount: Int! +} + +""" +An edge in a connection. +""" +type UsageReportEdge { + """ + A cursor for use in pagination. + """ + cursor: String! + + """ + The item at the end of the edge. + """ + node: UsageReport +} + +type Workflow implements DoiItem { + """ + The client account managing this resource + """ + client: Client + + """ + The main researchers involved in producing the data, or the authors of the publication, in priority order + """ + creators(first: Int = 20): [Person!] + + """ + Different dates relevant to the work + """ + dates: [Date!] + + """ + All additional information that does not fit in any of the other categories + """ + descriptions(first: Int = 5): [Description!] + + """ + Technical format of the resource + """ + formats: [String!] + + """ + Metadata as formatted citation + """ + formattedCitation(locale: String = "en-US", style: String = "apa"): String + + """ + Information about financial support (funding) for the resource being registered + """ + fundingReferences: [Funding!] + + """ + The persistent identifier for the resource + """ + id: ID! + + """ + An identifier or identifiers applied to the resource being registered + """ + identifiers: [Identifier!] + + """ + The primary language of the resource + """ + language: String + + """ + The provider account managing this resource + """ + provider: Provider + + """ + The year when the data was or will be made publicly available + """ + publicationYear: Int + + """ + The name of the entity that holds, archives, publishes prints, distributes, releases, issues, or produces the resource + """ + publisher: String + + """ + Identifiers of related resources. These must be globally unique identifiers + """ + relatedIdentifiers: [RelatedIdentifier!] + + """ + The general type of a resource + """ + resourceTypeGeneral: String + + """ + Any rights information for this resource + """ + rights: [Rights!] + + """ + Size (e.g. bytes, pages, inches, etc.) or duration (extent), e.g. hours, minutes, days, etc., of a resource + """ + sizes: [String!] + + """ + Subject, keyword, classification code, or key phrase describing the resource + """ + subjects: [Subject!] + + """ + A name or title by which a resource is known + """ + titles(first: Int = 5): [Title!] + + """ + The type of the item. + """ + type: String! + + """ + The resource type + """ + types: ResourceType + + """ + The URL registered for the resource + """ + url: String + + """ + The version number of the resource + """ + version: String +} + +""" +The sdl representing the federated service capabilities. Includes federation +directives, removes federation types, and includes rest of full schema after +schema directives have been applied +""" +type _Service { + sdl: String +} \ No newline at end of file diff --git a/app/graphql/types/client_dataset_connection_with_meta_type.rb b/app/graphql/types/client_dataset_connection_with_meta_type.rb index 0e5a6f326..91053e9e3 100644 --- a/app/graphql/types/client_dataset_connection_with_meta_type.rb +++ b/app/graphql/types/client_dataset_connection_with_meta_type.rb @@ -3,7 +3,7 @@ class ClientDatasetConnectionWithMetaType < BaseConnection edge_type(DatasetEdgeType) field_class GraphQL::Cache::Field - + field :total_count, Integer, null: false, cache: true def total_count diff --git a/app/graphql/types/client_type.rb b/app/graphql/types/client_type.rb index ce96ac689..7d125db9a 100644 --- a/app/graphql/types/client_type.rb +++ b/app/graphql/types/client_type.rb @@ -4,7 +4,9 @@ class ClientType < BaseObject description "Information about clients" field :id, ID, null: false, hash_key: "uid", description: "Unique identifier for each client" + field :type, String, null: false, description: "The type of the item." field :name, String, null: false, description: "Client name" + field :alternate_name, String, null: true, description: "Client alternate name" field :re3data, String, null: true, description: "The re3data identifier for the client" field :description, String, null: true, description: "Description of the client" field :url, String, null: true, description: "The homepage of the client" @@ -31,6 +33,10 @@ class ClientType < BaseObject argument :first, Int, required: false, default_value: 25 end + def type + "Client" + end + def prefixes(**args) collection = ClientPrefix.joins(:client, :prefix).where('datacentre.symbol = ?', object.uid) collection = collection.query(args[:query]) if args[:query].present? diff --git a/app/graphql/types/data_catalog_connection_with_meta_type.rb b/app/graphql/types/data_catalog_connection_with_meta_type.rb index f780859d4..7c02dd45a 100644 --- a/app/graphql/types/data_catalog_connection_with_meta_type.rb +++ b/app/graphql/types/data_catalog_connection_with_meta_type.rb @@ -3,7 +3,7 @@ class DataCatalogConnectionWithMetaType < BaseConnection edge_type(DataCatalogEdgeType) field_class GraphQL::Cache::Field - + field :total_count, Integer, null: true, cache: true field :dataset_connection_count, Integer, null: false, cache: true diff --git a/app/graphql/types/dataset_type.rb b/app/graphql/types/dataset_type.rb index 766276efb..c97a4638b 100644 --- a/app/graphql/types/dataset_type.rb +++ b/app/graphql/types/dataset_type.rb @@ -29,7 +29,7 @@ def datasets(**args) ids = Event.query(nil, doi: doi_from_url(object.identifier), citation_type: "Dataset-Dataset").results.to_a.map do |e| object.identifier == e.subj_id ? doi_from_url(e.obj_id) : doi_from_url(e.subj_id) end - + ElasticsearchLoader.for(Doi).load_many(ids) end @@ -40,7 +40,7 @@ def publications(**args) ElasticsearchLoader.for(Doi).load_many(ids) end - def softwsoftware_source_codesares(**args) + def software_source_codes(**args) ids = Event.query(nil, doi: doi_from_url(object.identifier), citation_type: "Dataset-SoftwareSourceCode").results.to_a.map do |e| object.identifier == e.subj_id ? doi_from_url(e.obj_id) : doi_from_url(e.subj_id) end diff --git a/app/graphql/types/funder_connection_with_meta_type.rb b/app/graphql/types/funder_connection_with_meta_type.rb index 45c170890..4d1ab00f7 100644 --- a/app/graphql/types/funder_connection_with_meta_type.rb +++ b/app/graphql/types/funder_connection_with_meta_type.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true class FunderConnectionWithMetaType < BaseConnection - edge_type(FunderEdgeType) field_class GraphQL::Cache::Field - + field :total_count, Integer, null: false, cache: true field :publication_connection_count, Integer, null: false, cache: true field :dataset_connection_count, Integer, null: false, cache: true @@ -15,7 +14,7 @@ def total_count Funder.query(args[:query], limit: 0).dig(:meta, "total").to_i end - + def publication_connection_count Event.query(nil, citation_type: "Funder-ScholarlyArticle").results.total end diff --git a/app/graphql/types/funder_type.rb b/app/graphql/types/funder_type.rb index b1652cae1..cadf02d78 100644 --- a/app/graphql/types/funder_type.rb +++ b/app/graphql/types/funder_type.rb @@ -2,7 +2,7 @@ class FunderType < BaseObject description "Information about funders" - + field :id, ID, null: false, description: "Crossref Funder ID" field :type, String, null: false, description: "The type of the item." field :name, String, null: false, description: "The name of the funder." @@ -34,7 +34,7 @@ def datasets(**args) ids = Event.query(nil, obj_id: object[:id], citation_type: "Dataset-Funder").results.to_a.map do |e| doi_from_url(e.subj_id) end - + ElasticsearchLoader.for(Doi).load_many(ids) end diff --git a/app/graphql/types/provider_client_connection_with_meta_type.rb b/app/graphql/types/provider_client_connection_with_meta_type.rb new file mode 100644 index 000000000..fd8ebb1db --- /dev/null +++ b/app/graphql/types/provider_client_connection_with_meta_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ProviderClientConnectionWithMetaType < BaseConnection + edge_type(ClientEdgeType) + field_class GraphQL::Cache::Field + + field :total_count, Integer, null: false, cache: true + + def total_count + args = object.arguments + + Client.query(args[:query], provider_id: object.parent.uid, year: args[:year], software: args[:software], page: { number: 1, size: 0 }).results.total + end +end diff --git a/app/graphql/types/provider_type.rb b/app/graphql/types/provider_type.rb index 9fdcac724..eba303fb7 100644 --- a/app/graphql/types/provider_type.rb +++ b/app/graphql/types/provider_type.rb @@ -4,7 +4,9 @@ class ProviderType < BaseObject description "Information about providers" field :id, ID, null: false, hash_key: "uid", description: "Unique identifier for each provider" + field :type, String, null: false, description: "The type of the item." field :name, String, null: false, description: "Provider name" + field :displayName, String, null: true, description: "Provider display name" field :ror_id, String, null: false, description: "Research Organization Registry (ROR) identifier" field :description, String, null: true, description: "Description of the provider" field :website, String, null: true, description: "Website of the provider" @@ -22,13 +24,17 @@ class ProviderType < BaseObject argument :first, Int, required: false, default_value: 25 end - field :clients, ClientConnectionWithMetaType, null: false, description: "Clients associated with the provider", connection: true do + field :clients, ProviderClientConnectionWithMetaType, null: false, description: "Clients associated with the provider", connection: true do argument :query, String, required: false argument :year, String, required: false argument :software, String, required: false argument :first, Int, required: false, default_value: 25 end + def type + "Provider" + end + def country return {} unless object.country_code.present? { diff --git a/app/graphql/types/publication_connection_with_meta_type.rb b/app/graphql/types/publication_connection_with_meta_type.rb index 891efa41d..639411104 100644 --- a/app/graphql/types/publication_connection_with_meta_type.rb +++ b/app/graphql/types/publication_connection_with_meta_type.rb @@ -14,7 +14,7 @@ class PublicationConnectionWithMetaType < BaseConnection def total_count args = object.arguments - + Doi.query(args[:query], client_id: args[:client_id], provider_id: args[:provider_id], resource_type_id: "Text", state: "findable", page: { number: 1, size: 0 }).results.total end diff --git a/app/graphql/types/publication_type.rb b/app/graphql/types/publication_type.rb index 746fb81b9..03266d96b 100644 --- a/app/graphql/types/publication_type.rb +++ b/app/graphql/types/publication_type.rb @@ -19,7 +19,7 @@ def datasets(**args) ids = Event.query(nil, doi: doi_from_url(object.identifier), citation_type: "Dataset-ScholarlyArticle").results.to_a.map do |e| object.identifier == e.subj_id ? doi_from_url(e.obj_id) : doi_from_url(e.subj_id) end - + ElasticsearchLoader.for(Doi).load_many(ids) end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 26f17227b..f679a526f 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -342,8 +342,10 @@ def physical_object(id:) set_doi(id) end - field :services, [ServiceType], null: false do + field :services, ServiceConnectionWithMetaType, null: false, connection: true, max_page_size: 1000 do argument :query, String, required: false + argument :client_id, String, required: false + argument :provider_id, String, required: false argument :first, Int, required: false, default_value: 25 end diff --git a/app/graphql/types/service_connection_with_meta_type.rb b/app/graphql/types/service_connection_with_meta_type.rb new file mode 100644 index 000000000..f50587af9 --- /dev/null +++ b/app/graphql/types/service_connection_with_meta_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ServiceConnectionWithMetaType < BaseConnection + edge_type(DatasetEdgeType) + field_class GraphQL::Cache::Field + + field :total_count, Integer, null: false, cache: true + + def total_count + args = object.arguments + + Doi.query(args[:query], client_id: args[:client_id], provider_id: args[:provider_id], resource_type_id: "Service", state: "findable", page: { number: 1, size: 0 }).results.total + end +end diff --git a/app/jobs/affiliation_job.rb b/app/jobs/affiliation_job.rb index b3a0aedc8..fcf5afa2d 100644 --- a/app/jobs/affiliation_job.rb +++ b/app/jobs/affiliation_job.rb @@ -17,7 +17,7 @@ def perform(doi_id) doi.__elasticsearch__.index_document else - Rails.logger.error "[Affiliation] Error updaing DOI " + doi_id + ": not found" + Rails.logger.error "[Affiliation] Error updating DOI " + doi_id + ": not found" end end end diff --git a/app/jobs/target_doi_by_id_job.rb b/app/jobs/target_doi_by_id_job.rb new file mode 100644 index 000000000..f908064f1 --- /dev/null +++ b/app/jobs/target_doi_by_id_job.rb @@ -0,0 +1,19 @@ +class TargetDoiByIdJob < ActiveJob::Base + queue_as :lupo_background + + rescue_from ActiveJob::DeserializationError, Elasticsearch::Transport::Transport::Errors::BadRequest do |error| + Rails.logger.error error.message + end + + def perform(id, options={}) + item = Event.where(uuid: id).first + return false if item.blank? + + item.set_source_and_target_doi + if item.save + Rails.logger.info "Target doi for #{item.uuid} updated." + else + Rails.logger.error item.errors.inspect + " for #{item.uuid}" + end + end +end diff --git a/app/jobs/target_doi_job.rb b/app/jobs/target_doi_job.rb new file mode 100644 index 000000000..f0b7cc566 --- /dev/null +++ b/app/jobs/target_doi_job.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class TargetDoiJob < ActiveJob::Base + queue_as :lupo_background + + def perform(ids, options={}) + ids.each { |id| TargetDoiByIdJob.perform_later(id, options) } + end +end diff --git a/app/models/activity.rb b/app/models/activity.rb index 9089fc775..4976a868c 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -220,21 +220,21 @@ def self.import_by_id(options={}) # log errors errors += response['items'].map { |k, v| k.values.first['error'] }.compact.length response['items'].select { |k, v| k.values.first['error'].present? }.each do |err| - logger.error "[Elasticsearch] " + err.inspect + Rails.logger.error "[Elasticsearch] " + err.inspect end count += activities.length end if errors > 1 - logger.error "[Elasticsearch] #{errors} errors importing #{count} activities with IDs #{id} - #{(id + 499)}." + Rails.logger.error "[Elasticsearch] #{errors} errors importing #{count} activities with IDs #{id} - #{(id + 499)}." elsif count > 0 - logger.info "[Elasticsearch] Imported #{count} activities with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Imported #{count} activities with IDs #{id} - #{(id + 499)}." end count rescue Elasticsearch::Transport::Transport::Errors::RequestEntityTooLarge, Faraday::ConnectionFailed, ActiveRecord::LockWaitTimeout => error - logger.error "[Elasticsearch] Error #{error.message} importing activities with IDs #{id} - #{(id + 499)}." + Rails.logger.error "[Elasticsearch] Error #{error.message} importing activities with IDs #{id} - #{(id + 499)}." count = 0 @@ -243,7 +243,7 @@ def self.import_by_id(options={}) count += 1 end - logger.info "[Elasticsearch] Imported #{count} activities with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Imported #{count} activities with IDs #{id} - #{(id + 499)}." count end @@ -315,11 +315,11 @@ def self.convert_affiliation_by_id(options={}) end end - logger.info "[Elasticsearch] Converted affiliations for #{count} activities with IDs #{id} - #{(id + 499)}." if count > 0 + Rails.logger.info "[Elasticsearch] Converted affiliations for #{count} activities with IDs #{id} - #{(id + 499)}." if count > 0 count rescue Elasticsearch::Transport::Transport::Errors::RequestEntityTooLarge, Faraday::ConnectionFailed, ActiveRecord::LockWaitTimeout => error - logger.info "[Elasticsearch] Error #{error.message} converting affiliations for DOIs with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Error #{error.message} converting affiliations for DOIs with IDs #{id} - #{(id + 499)}." end def uid diff --git a/app/models/client.rb b/app/models/client.rb index ecf0b43ee..89b415a20 100644 --- a/app/models/client.rb +++ b/app/models/client.rb @@ -48,6 +48,7 @@ class Client < ActiveRecord::Base validate :check_issn, if: :issn? validate :check_certificate, if: :certificate? validate :check_repository_type, if: :repository_type? + validate :uuid_format, if: :globus_uuid? strip_attributes belongs_to :provider, foreign_key: :allocator, touch: true @@ -82,12 +83,14 @@ class Client < ActiveRecord::Base } do mapping dynamic: 'false' do indexes :id, type: :keyword + indexes :uid, type: :keyword, normalizer: "keyword_lowercase" indexes :symbol, type: :keyword indexes :provider_id, type: :keyword indexes :consortium_id, type: :keyword indexes :re3data_id, type: :keyword indexes :opendoar_id, type: :integer indexes :salesforce_id, type: :keyword + indexes :globus_uuid, type: :keyword indexes :issn, type: :object, properties: { issnl: { type: :keyword }, electronic: { type: :keyword }, @@ -123,6 +126,7 @@ class Client < ActiveRecord::Base id: { type: :keyword }, uid: { type: :keyword }, symbol: { type: :keyword }, + globus_uuid: { type: :keyword }, client_ids: { type: :keyword }, prefix_ids: { type: :keyword }, name: { type: :text, fields: { keyword: { type: "keyword" }, raw: { type: "text", "analyzer": "string_lowercase", "fielddata": true }} }, @@ -211,6 +215,7 @@ def as_indexed_json(options={}) "re3data_id" => re3data_id, "opendoar_id" => opendoar_id, "salesforce_id" => salesforce_id, + "globus_uuid" => globus_uuid, "issn" => issn, "prefix_ids" => prefix_ids, "name" => name, @@ -245,7 +250,7 @@ def self.query_fields def self.query_aggregations { years: { date_histogram: { field: 'created', interval: 'year', min_doc_count: 1 } }, - cumulative_years: { terms: { field: 'cumulative_years', min_doc_count: 1, order: { _count: "asc" } } }, + cumulative_years: { terms: { field: 'cumulative_years', size: 15, min_doc_count: 1, order: { _count: "asc" } } }, providers: { terms: { field: 'provider_id', size: 15, min_doc_count: 1 } }, software: { terms: { field: 'software.keyword', size: 15, min_doc_count: 1 } }, client_types: { terms: { field: 'client_type', size: 15, min_doc_count: 1 } }, @@ -424,6 +429,10 @@ def check_repository_type end end + def uuid_format + errors.add(:globus_uuid, "#{globus_uuid} is not a valid UUID") unless UUID.validate(globus_uuid) + end + def freeze_symbol errors.add(:symbol, "cannot be changed") if symbol_changed? end diff --git a/app/models/concerns/authenticable.rb b/app/models/concerns/authenticable.rb index 58fee0820..fc734676b 100644 --- a/app/models/concerns/authenticable.rb +++ b/app/models/concerns/authenticable.rb @@ -31,10 +31,40 @@ def encode_alb_token(payload) nil end - # decode JWT token using SHA-256 hash algorithm + # use only for testing, as we don't have private key for JWT encoded by Globus + def encode_globus_token(payload) + return nil if payload.blank? || !Rails.env.test? + + # replace newline characters with actual newlines + private_key = OpenSSL::PKey.read(File.read(Rails.root.join("spec", "fixtures", "certs", "ec512-private.pem").to_s)) + JWT.encode(payload, private_key, 'RS512') + rescue OpenSSL::PKey::ECError => e + Rails.logger.error e.inspect + " for " + payload.inspect + + nil + end + + # decode JWT token. Check whether it is a DataCite or Globus JWT via the JWT header + # DataCite uses RS256, Globus uses RS512 def decode_token(token) - public_key = OpenSSL::PKey::RSA.new(ENV['JWT_PUBLIC_KEY'].to_s.gsub('\n', "\n")) - payload = (JWT.decode token, public_key, true, { :algorithm => 'RS256' }).first + # check that JWT has header, payload and secret, separated by dot + token_parts = token.to_s.split(".") + raise JWT::DecodeError if token_parts.length != 3 + + # decode token + header = JSON.parse(Base64.urlsafe_decode64(token_parts.first)) + case header["alg"] + when "RS256" + # DataCite JWT + public_key = OpenSSL::PKey::RSA.new(ENV['JWT_PUBLIC_KEY'].to_s.gsub('\n', "\n")) + payload = (JWT.decode token, public_key, true, { :algorithm => 'RS256' }).first + when "RS512" + # Globus JWT + public_key = OpenSSL::PKey::RSA.new(cached_globus_public_key.fetch("n", nil).to_s.gsub('\n', "\n")) + payload = (JWT.decode token, public_key, true, { :algorithm => 'RS512' }).first + else + raise JWT::DecodeError, "Algorithm #{header["alg"]} is not supported." + end # check whether token has expired fail JWT::ExpiredSignature, "The token has expired." unless Time.now.to_i < payload["exp"].to_i @@ -191,6 +221,18 @@ def encode_alb_token(payload) nil end + # encode token using RSA and SHA-512 + # use this only for testing as private key is publicly available from ruby-jwt gem + def encode_globus_token(payload) + return nil if payload.blank? || !Rails.env.test? + private_key = OpenSSL::PKey.read(File.read(Rails.root.join("spec", "fixtures", "certs", "ec512-private.pem").to_s)) + JWT.encode(payload, private_key, 'RS512') + rescue OpenSSL::PKey::ECError => e + Rails.logger.error e.inspect + " for " + payload.inspect + + nil + end + # basic auth def encode_auth_param(username: nil, password: nil) return nil unless username.present? && password.present? diff --git a/app/models/concerns/cacheable.rb b/app/models/concerns/cacheable.rb index d48f9cb1e..c872c7b3d 100644 --- a/app/models/concerns/cacheable.rb +++ b/app/models/concerns/cacheable.rb @@ -67,6 +67,14 @@ def cached_alb_public_key(kid) response.body.fetch("data", nil) end end + + def cached_globus_public_key + Rails.cache.fetch("globus_public_key", expires_in: 1.month) do + url = "https://auth.globus.org/jwk.json" + response = Maremma.get(url) + response.body.dig("data", "keys", 0) + end + end end module ClassMethods diff --git a/app/models/concerns/crosscitable.rb b/app/models/concerns/crosscitable.rb index d594b9cad..8e126610f 100644 --- a/app/models/concerns/crosscitable.rb +++ b/app/models/concerns/crosscitable.rb @@ -51,8 +51,8 @@ def parse_xml(input, options={}) rescue NoMethodError, ArgumentError => exception Raven.capture_exception(exception) - logger.error "Error " + exception.message + " for doi " + @doi + "." - logger.error exception + Rails.logger.error "Error " + exception.message + " for doi " + @doi + "." + Rails.logger.error exception {} end @@ -117,8 +117,8 @@ def clean_xml(string) doc = Nokogiri::XML(string) { |config| config.strict.noblanks } doc.to_xml rescue ArgumentError, Encoding::CompatibilityError => exception - logger.error "Error " + exception.message + "." - logger.error exception + Rails.logger.error "Error " + exception.message + "." + Rails.logger.error exception nil end diff --git a/app/models/concerns/helpable.rb b/app/models/concerns/helpable.rb index dac6109c7..199c32cad 100644 --- a/app/models/concerns/helpable.rb +++ b/app/models/concerns/helpable.rb @@ -14,12 +14,12 @@ module Helpable def register_url unless url.present? - logger.error "[Handle] Error updating DOI " + doi + ": url missing." + Rails.logger.error "[Handle] Error updating DOI " + doi + ": url missing." return OpenStruct.new(body: { "errors" => [{ "title" => "URL missing." }] }) end unless client_id.present? - logger.error "[Handle] Error updating DOI " + doi + ": client ID missing." + Rails.logger.error "[Handle] Error updating DOI " + doi + ": client ID missing." return OpenStruct.new(body: { "errors" => [{ "title" => "Client ID missing." }] }) end @@ -56,11 +56,11 @@ def register_url if [200, 201].include?(response.status) # update minted column after first successful registration in handle system self.update_attributes(minted: Time.zone.now, updated: Time.zone.now) if minted.blank? - logger.info "[Handle] URL for DOI " + doi + " updated to " + url + "." unless Rails.env.test? + Rails.logger.info "[Handle] URL for DOI " + doi + " updated to " + url + "." unless Rails.env.test? self.__elasticsearch__.index_document else - logger.error "[Handle] Error updating URL for DOI " + doi + ": " + response.body.inspect unless Rails.env.test? + Rails.logger.error "[Handle] Error updating URL for DOI " + doi + ": " + response.body.inspect unless Rails.env.test? end response @@ -82,7 +82,7 @@ def get_url response = Maremma.get(url, ssl_self_signed: true, timeout: 10) if response.status != 200 - logger.error "[Handle] Error fetching URL for DOI " + doi + ": " + response.body.inspect unless Rails.env.test? + Rails.logger.error "[Handle] Error fetching URL for DOI " + doi + ": " + response.body.inspect unless Rails.env.test? end response @@ -153,13 +153,13 @@ def get_dois(options={}) else text = "Error " + response.body["errors"].inspect - logger.error "[Handle] " + text + Rails.logger.error "[Handle] " + text User.send_notification_to_slack(text, title: "Error #{response.status.to_s}", level: "danger") unless Rails.env.test? end end end - logger.info "#{total} DOIs found." + Rails.logger.info "#{total} DOIs found." dois end @@ -174,7 +174,7 @@ def get_doi(options={}) if response.status != 200 text = "Error " + response.body["errors"].inspect - logger.error "[Handle] " + text + Rails.logger.error "[Handle] " + text User.send_notification_to_slack(text, title: "Error #{response.status.to_s}", level: "danger") unless Rails.env.test? end @@ -193,7 +193,7 @@ def delete_doi(options={}) else text = "Error " + response.body["errors"].inspect - logger.error "[Handle] " + text + Rails.logger.error "[Handle] " + text User.send_notification_to_slack(text, title: "Error #{response.status.to_s}", level: "danger") unless Rails.env.test? response end diff --git a/app/models/concerns/indexable.rb b/app/models/concerns/indexable.rb index 56d20e58f..56b5f44dc 100644 --- a/app/models/concerns/indexable.rb +++ b/app/models/concerns/indexable.rb @@ -4,12 +4,28 @@ module Indexable require 'aws-sdk-sqs' included do - after_commit on: [:create, :update] do + after_commit on: [:create] do # use index_document instead of update_document to also update virtual attributes IndexJob.perform_later(self) if self.class.name == "Doi" update_column(:indexed, Time.zone.now) - send_import_message(self.to_jsonapi) if aasm_state == "findable" unless (Rails.env.test? || %w(crossref medra kisti jalc op).include?(client.symbol.downcase.split(".").first)) + send_import_message(self.to_jsonapi) if aasm_state == "findable" && !Rails.env.test? && !%w(crossref medra kisti jalc op).include?(client.symbol.downcase.split(".").first) + elsif self.class.name == "Event" + # reindex dois associated with Event + @source_doi = Doi.where(doi: source_doi).first if source_doi + IndexJob.perform_later(@source_doi) if @source_doi + + @target_doi = Doi.where(doi: target_doi).first if target_doi + IndexJob.perform_later(@target_doi) if @target_doi + end + end + + after_commit on: [:update] do + # use index_document instead of update_document to also update virtual attributes + IndexJob.perform_later(self) + if self.class.name == "Doi" + update_column(:indexed, Time.zone.now) + send_import_message(self.to_jsonapi) if aasm_state == "findable" && !Rails.env.test? && !%w(crossref medra kisti jalc op).include?(client.symbol.downcase.split(".").first) end end diff --git a/app/models/doi.rb b/app/models/doi.rb index 60a85f8d9..4b46557f5 100644 --- a/app/models/doi.rb +++ b/app/models/doi.rb @@ -72,9 +72,15 @@ class Doi < ActiveRecord::Base belongs_to :client, foreign_key: :datacentre has_many :media, -> { order "created DESC" }, foreign_key: :dataset, dependent: :destroy has_many :metadata, -> { order "created DESC" }, foreign_key: :dataset, dependent: :destroy - has_many :views, -> { where relation_type_id: "unique-dataset-investigations-regular" }, class_name: "Event", primary_key: :doi, foreign_key: :doi_id, dependent: :destroy - has_many :downloads, -> { where relation_type_id: "unique-dataset-requests-regular" }, class_name: "Event", primary_key: :doi, foreign_key: :doi_id, dependent: :destroy - + has_many :views, -> { where target_relation_type_id: "views" }, class_name: "Event", primary_key: :doi, foreign_key: :target_doi, dependent: :destroy + has_many :downloads, -> { where target_relation_type_id: "downloads" }, class_name: "Event", primary_key: :doi, foreign_key: :target_doi, dependent: :destroy + has_many :references, -> { where source_relation_type_id: "references" }, class_name: "Event", primary_key: :doi, foreign_key: :source_doi, dependent: :destroy + has_many :citations, -> { where target_relation_type_id: "citations" }, class_name: "Event", primary_key: :doi, foreign_key: :target_doi, dependent: :destroy + has_many :parts, -> { where source_relation_type_id: "parts" }, class_name: "Event", primary_key: :doi, foreign_key: :source_doi, dependent: :destroy + has_many :part_of, -> { where target_relation_type_id: "part_of" }, class_name: "Event", primary_key: :doi, foreign_key: :target_doi, dependent: :destroy + has_many :versions, -> { where source_relation_type_id: "versions" }, class_name: "Event", primary_key: :doi, foreign_key: :source_doi, dependent: :destroy + has_many :version_of, -> { where target_relation_type_id: "version_of" }, class_name: "Event", primary_key: :doi, foreign_key: :target_doi, dependent: :destroy + delegate :provider, to: :client, allow_nil: true delegate :consortium_id, to: :provider, allow_nil: true @@ -129,7 +135,7 @@ class Doi < ActiveRecord::Base } do mapping dynamic: 'false' do indexes :id, type: :keyword - indexes :uid, type: :keyword + indexes :uid, type: :keyword, normalizer: "keyword_lowercase" indexes :doi, type: :keyword indexes :identifier, type: :keyword indexes :url, type: :text, fields: { keyword: { type: "keyword" }} @@ -150,12 +156,12 @@ class Doi < ActiveRecord::Base schemeUri: { type: :keyword } }}, } - indexes :contributors, type: :object, properties: { + indexes :contributors, type: :object, properties: { nameType: { type: :keyword }, nameIdentifiers: { type: :object, properties: { nameIdentifier: { type: :keyword }, nameIdentifierScheme: { type: :keyword }, - schemeUri: { type: :keyword } + schemeUri: { type: :keyword }, }}, name: { type: :text }, givenName: { type: :text }, @@ -198,16 +204,16 @@ class Doi < ActiveRecord::Base } indexes :identifiers, type: :object, properties: { identifierType: { type: :keyword }, - identifier: { type: :keyword } + identifier: { type: :keyword, normalizer: "keyword_lowercase" }, } indexes :related_identifiers, type: :object, properties: { relatedIdentifierType: { type: :keyword }, - relatedIdentifier: { type: :keyword }, + relatedIdentifier: { type: :keyword, normalizer: "keyword_lowercase" }, relationType: { type: :keyword }, relatedMetadataScheme: { type: :keyword }, schemeUri: { type: :keyword }, schemeType: { type: :keyword }, - resourceTypeGeneral: { type: :keyword } + resourceTypeGeneral: { type: :keyword }, } indexes :types, type: :object, properties: { resourceTypeGeneral: { type: :keyword }, @@ -215,11 +221,11 @@ class Doi < ActiveRecord::Base schemaOrg: { type: :keyword }, bibtex: { type: :keyword }, citeproc: { type: :keyword }, - ris: { type: :keyword } + ris: { type: :keyword }, } indexes :funding_references, type: :object, properties: { funderName: { type: :keyword }, - funderIdentifier: { type: :keyword }, + funderIdentifier: { type: :keyword, normalizer: "keyword_lowercase" }, funderIdentifierType: { type: :keyword }, awardNumber: { type: :keyword }, awardUri: { type: :keyword }, @@ -248,7 +254,7 @@ class Doi < ActiveRecord::Base } indexes :container, type: :object, properties: { type: { type: :keyword }, - identifier: { type: :keyword }, + identifier: { type: :keyword, normalizer: "keyword_lowercase" }, identifierType: { type: :keyword }, title: { type: :keyword }, volume: { type: :keyword }, @@ -296,6 +302,7 @@ class Doi < ActiveRecord::Base # include parent objects indexes :client, type: :object, properties: { id: { type: :keyword }, + uid: { type: :keyword, normalizer: "keyword_lowercase" }, symbol: { type: :keyword }, provider_id: { type: :keyword }, re3data_id: { type: :keyword }, @@ -324,7 +331,7 @@ class Doi < ActiveRecord::Base } indexes :provider, type: :object, properties: { id: { type: :keyword }, - uid: { type: :keyword }, + uid: { type: :keyword, normalizer: "keyword_lowercase" }, symbol: { type: :keyword }, client_ids: { type: :keyword }, prefix_ids: { type: :keyword }, @@ -363,48 +370,68 @@ class Doi < ActiveRecord::Base technical_contact: { type: :object, properties: { email: { type: :text }, given_name: { type: :text}, - family_name: { type: :text } + family_name: { type: :text }, } }, secondary_technical_contact: { type: :object, properties: { email: { type: :text }, given_name: { type: :text}, - family_name: { type: :text } + family_name: { type: :text }, } }, billing_contact: { type: :object, properties: { email: { type: :text }, given_name: { type: :text}, - family_name: { type: :text } + family_name: { type: :text }, } }, secondary_billing_contact: { type: :object, properties: { email: { type: :text }, - given_name: { type: :text}, - family_name: { type: :text } + given_name: { type: :text }, + family_name: { type: :text }, } }, service_contact: { type: :object, properties: { email: { type: :text }, - given_name: { type: :text}, - family_name: { type: :text } + given_name: { type: :text }, + family_name: { type: :text }, } }, secondary_service_contact: { type: :object, properties: { email: { type: :text }, - given_name: { type: :text}, - family_name: { type: :text } + given_name: { type: :text }, + family_name: { type: :text }, } }, voting_contact: { type: :object, properties: { email: { type: :text }, - given_name: { type: :text}, - family_name: { type: :text } + given_name: { type: :text }, + family_name: { type: :text }, } }, created: { type: :date }, updated: { type: :date }, deleted_at: { type: :date }, cumulative_years: { type: :integer, index: "false" }, consortium: { type: :object }, - consortium_organizations: { type: :object } + consortium_organizations: { type: :object }, } indexes :resource_type, type: :object - indexes :views, type: :object - indexes :downloads, type: :object + indexes :view_count, type: :integer + indexes :download_count, type: :integer + indexes :reference_count, type: :integer + indexes :citation_count, type: :integer + indexes :part_count, type: :integer + indexes :part_of_count, type: :integer + indexes :version_count, type: :integer + indexes :version_of_count, type: :integer + indexes :views_over_time, type: :object + indexes :downloads_over_time, type: :object + indexes :reference_ids, type: :keyword + indexes :citation_ids, type: :keyword + indexes :part_ids, type: :keyword + indexes :part_of_ids, type: :keyword + indexes :version_ids, type: :keyword + indexes :version_of_ids, type: :keyword + indexes :references, type: :object + indexes :citations, type: :object + indexes :parts, type: :object + indexes :part_of, type: :object + indexes :versions, type: :object + indexes :version_of, type: :object end end @@ -426,6 +453,22 @@ def as_indexed_json(options={}) "consortium_id" => consortium_id, "resource_type_id" => resource_type_id, "media_ids" => media_ids, + "view_count" => view_count, + "views_over_time" => views_over_time, + "download_count" => download_count, + "downloads_over_time" => downloads_over_time, + "reference_ids" => reference_ids, + "reference_count" => reference_count, + "citation_ids" => citation_ids, + "citation_count" => citation_count, + "part_ids" => part_ids, + "part_count" => part_count, + "part_of_ids" => part_of_ids, + "part_of_count" => part_of_count, + "version_ids" => version_ids, + "version_count" => version_count, + "version_of_ids" => version_of_ids, + "version_of_count" => version_of_count, "prefix" => prefix, "suffix" => suffix, "types" => types, @@ -461,15 +504,19 @@ def as_indexed_json(options={}) "provider" => provider.try(:as_indexed_json), "resource_type" => resource_type.try(:as_indexed_json), "media" => media.map { |m| m.try(:as_indexed_json) }, - "views" => views.map { |m| m.try(:as_indexed_json) }, - "downloads" => downloads.map { |m| m.try(:as_indexed_json) } + "references" => references.map { |m| m.try(:as_indexed_json) }, + "citations" => citations.map { |m| m.try(:as_indexed_json) }, + "parts" => parts.map { |m| m.try(:as_indexed_json) }, + "part_of" => part_of.map { |m| m.try(:as_indexed_json) }, + "versions" => versions.map { |m| m.try(:as_indexed_json) }, + "version_of" => version_of.map { |m| m.try(:as_indexed_json) }, } end def self.query_aggregations { - resource_types: { terms: { field: 'types.resourceTypeGeneral', size: 15, min_doc_count: 1 } }, - states: { terms: { field: 'aasm_state', size: 15, min_doc_count: 1 } }, + resource_types: { terms: { field: 'types.resourceTypeGeneral', size: 20, min_doc_count: 1 } }, + states: { terms: { field: 'aasm_state', size: 3, min_doc_count: 1 } }, years: { date_histogram: { field: 'publication_year', interval: 'year', min_doc_count: 1 } }, created: { date_histogram: { field: 'created', interval: 'year', min_doc_count: 1 } }, registered: { date_histogram: { field: 'registered', interval: 'year', min_doc_count: 1 } }, @@ -513,7 +560,7 @@ def self.sub_aggregations end def self.query_fields - ['doi^50', 'id^50', 'titles.title^3', 'creator_names^3', 'creators.name^3', 'creators.id^3', 'publisher^3', 'descriptions.description^3', 'types.resourceTypeGeneral^3', 'subjects.subject^3', 'identifiers.identifier^3', 'related_identifiers.relatedIdentifier^3', '_all'] + ["uid^50", "related_identifiers.relatedIdentifier^3", "funding_references.relatedIdentifier^3", "container.identifier^3", 'titles.title^3', 'creator_names^3', 'creators.name^3', 'creators.id^3', 'publisher^3', 'descriptions.description^3', 'types.resourceTypeGeneral^3', 'subjects.subject^3', 'client.uid^3', 'provider.uid^3', '_all'] end # return results for one or more ids @@ -579,6 +626,16 @@ def self.query(query, options={}) options[:page][:number] ||= 1 options[:page][:size] ||= 25 + if options[:totals_agg] == "provider" + aggregations = provider_aggregations + elsif options[:totals_agg] == "client" + aggregations = client_aggregations + elsif options[:totals_agg] == "prefix" + aggregations = prefix_aggregations + else + aggregations = get_aggregations_hash(options) + end + # Cursor nav use the search after, this should always be an array of values that match the sort. if options.dig(:page, :cursor) from = 0 @@ -741,13 +798,13 @@ def self.query(query, options={}) def self.import_one(doi_id: nil) doi = Doi.where(doi: doi_id).first unless doi.present? - logger.error "[MySQL] DOI " + doi_id + " not found." + Rails.logger.error "[MySQL] DOI " + doi_id + " not found." return nil end string = doi.current_metadata.present? ? doi.clean_xml(doi.current_metadata.xml) : nil unless string.present? - logger.error "[MySQL] No metadata for DOI " + doi.doi + " found: " + doi.current_metadata.inspect + Rails.logger.error "[MySQL] No metadata for DOI " + doi.doi + " found: " + doi.current_metadata.inspect return nil end @@ -758,10 +815,10 @@ def self.import_one(doi_id: nil) # update_attributes will trigger validations and Elasticsearch indexing doi.update_attributes(attrs) - logger.info "[MySQL] Imported metadata for DOI " + doi.doi + "." + Rails.logger.info "[MySQL] Imported metadata for DOI " + doi.doi + "." doi rescue TypeError, NoMethodError, RuntimeError, ActiveRecord::StatementInvalid, ActiveRecord::LockWaitTimeout => error - logger.error "[MySQL] Error importing metadata for " + doi.doi + ": " + error.message + Rails.logger.error "[MySQL] Error importing metadata for " + doi.doi + ": " + error.message Raven.capture_exception(error) doi end @@ -773,7 +830,7 @@ def self.import_by_ids(options={}) # get every id between from_id and end_id (from_id..until_id).step(500).each do |id| DoiImportByIdJob.perform_later(options.merge(id: id)) - logger.info "Queued importing for DOIs with IDs starting with #{id}." unless Rails.env.test? + Rails.logger.info "Queued importing for DOIs with IDs starting with #{id}." unless Rails.env.test? end (from_id..until_id).to_a.length @@ -802,21 +859,21 @@ def self.import_by_id(options={}) # log errors errors += response['items'].map { |k, v| k.values.first['error'] }.compact.length response['items'].select { |k, v| k.values.first['error'].present? }.each do |err| - logger.error "[Elasticsearch] " + err.inspect + Rails.logger.error "[Elasticsearch] " + err.inspect end count += dois.length end if errors > 1 - logger.error "[Elasticsearch] #{errors} errors importing #{count} DOIs with IDs #{id} - #{(id + 499)}." + Rails.logger.error "[Elasticsearch] #{errors} errors importing #{count} DOIs with IDs #{id} - #{(id + 499)}." elsif count > 0 - logger.info "[Elasticsearch] Imported #{count} DOIs with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Imported #{count} DOIs with IDs #{id} - #{(id + 499)}." end count rescue Elasticsearch::Transport::Transport::Errors::RequestEntityTooLarge, Faraday::ConnectionFailed, ActiveRecord::LockWaitTimeout => error - logger.info "[Elasticsearch] Error #{error.message} importing DOIs with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Error #{error.message} importing DOIs with IDs #{id} - #{(id + 499)}." count = 0 @@ -825,7 +882,7 @@ def self.import_by_id(options={}) count += 1 end - logger.info "[Elasticsearch] Imported #{count} DOIs with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Imported #{count} DOIs with IDs #{id} - #{(id + 499)}." count end @@ -844,9 +901,73 @@ def media_ids media.pluck(:id).map { |m| Base32::URL.encode(m, split: 4, length: 16) }.compact end + def view_count + views.pluck(:total).inject(:+).to_i + end + + def views_over_time + views.pluck(:occurred_at, :total).map { |v| { year_month: v[0].present? ? v[0].utc.iso8601[0..6] : nil, total: v[1] } } + end + + def download_count + downloads.pluck(:total).inject(:+).to_i + end + + def downloads_over_time + downloads.pluck(:occurred_at, :total).map { |v| { year_month: v[0].present? ? v[0].utc.iso8601[0..6] : nil, total: v[1] } } + end + + def reference_ids + references.pluck(:uuid) + end + + def reference_count + references.count + end + + def citation_ids + citations.pluck(:uuid) + end + + def citation_count + citations.count + end + + def part_ids + parts.pluck(:uuid) + end + + def part_count + parts.count + end + + def part_of_ids + part_of.pluck(:uuid) + end + + def part_of_count + part_of.count + end + + def version_ids + versions.pluck(:uuid) + end + + def version_count + versions.count + end + + def version_of_ids + version_of.pluck(:uuid) + end + + def version_of_count + version_of.count + end + def xml_encoded Base64.strict_encode64(xml) if xml.present? - rescue ArgumentError => exception + rescue ArgumentError nil end @@ -870,7 +991,7 @@ def self.convert_affiliations(options={}) # get every id between from_id and end_id (from_id..until_id).step(500).each do |id| DoiConvertAffiliationByIdJob.perform_later(options.merge(id: id)) - logger.info "Queued converting affiliations for DOIs with IDs starting with #{id}." unless Rails.env.test? + Rails.logger.info "Queued converting affiliations for DOIs with IDs starting with #{id}." unless Rails.env.test? end (from_id..until_id).to_a.length @@ -886,7 +1007,7 @@ def self.convert_affiliation_by_id(options={}) should_update = false creators = Array.wrap(doi.creators).map do |c| if !(c.is_a?(Hash)) - logger.error "[MySQL] creators for DOI #{doi.doi} should be a hash." + Rails.logger.error "[MySQL] creators for DOI #{doi.doi} should be a hash." elsif c["affiliation"].nil? c["affiliation"] = [] should_update = true @@ -916,7 +1037,7 @@ def self.convert_affiliation_by_id(options={}) end contributors = Array.wrap(doi.contributors).map do |c| if !(c.is_a?(Hash)) - logger.error "[MySQL] creators for DOI #{doi.doi} should be a hash." + Rails.logger.error "[MySQL] creators for DOI #{doi.doi} should be a hash." elsif c["affiliation"].nil? c["affiliation"] = [] should_update = true @@ -954,16 +1075,16 @@ def self.convert_affiliation_by_id(options={}) end unless (Array.wrap(doi.creators).all? { |c| c.is_a?(Hash) && c["affiliation"].is_a?(Array) && c["affiliation"].all? { |a| a.is_a?(Hash) } } && Array.wrap(doi.contributors).all? { |c| c.is_a?(Hash) && c["affiliation"].is_a?(Array) && c["affiliation"].all? { |a| a.is_a?(Hash) } }) - logger.error "[MySQL] Error converting affiliations for doi #{doi.doi}: creators #{doi.creators.inspect} contributors #{doi.contributors.inspect}." + Rails.logger.error "[MySQL] Error converting affiliations for doi #{doi.doi}: creators #{doi.creators.inspect} contributors #{doi.contributors.inspect}." fail TypeError, "Affiliation for doi #{doi.doi} is of wrong type" if Rails.env.test? end end - logger.info "[MySQL] Converted affiliations for #{count} DOIs with IDs #{id} - #{(id + 499)}." if count > 0 + Rails.logger.info "[MySQL] Converted affiliations for #{count} DOIs with IDs #{id} - #{(id + 499)}." if count > 0 count rescue TypeError, ActiveRecord::ActiveRecordError, ActiveRecord::LockWaitTimeout => error - logger.error "[MySQL] Error converting affiliations for DOIs with IDs #{id} - #{(id + 499)}." + Rails.logger.error "[MySQL] Error converting affiliations for DOIs with IDs #{id} - #{(id + 499)}." count end @@ -974,7 +1095,7 @@ def self.convert_containers(options={}) # get every id between from_id and end_id (from_id..until_id).step(500).each do |id| DoiConvertContainerByIdJob.perform_later(options.merge(id: id)) - logger.info "Queued converting containers for DOIs with IDs starting with #{id}." unless Rails.env.test? + Rails.logger.info "Queued converting containers for DOIs with IDs starting with #{id}." unless Rails.env.test? end (from_id..until_id).to_a.length @@ -993,7 +1114,7 @@ def self.convert_container_by_id(options={}) should_update = true container = {} elsif !(doi.container.is_a?(Hash)) - logger.error "[MySQL] container for DOI #{doi.doi} should be a hash." + Rails.logger.error "[MySQL] container for DOI #{doi.doi} should be a hash." elsif [doi.container["title"], doi.container["volume"], doi.container["issue"], doi.container["identifier"]].any? { |c| c.is_a?(Hash) } should_update = true container = { @@ -1013,11 +1134,11 @@ def self.convert_container_by_id(options={}) end end - logger.info "[MySQL] Converted containers for #{count} DOIs with IDs #{id} - #{(id + 499)}." if count > 0 + Rails.logger.info "[MySQL] Converted containers for #{count} DOIs with IDs #{id} - #{(id + 499)}." if count > 0 count rescue TypeError, ActiveRecord::ActiveRecordError, ActiveRecord::LockWaitTimeout => error - logger.error "[MySQL] Error converting containers for DOIs with IDs #{id} - #{(id + 499)}." + Rails.logger.error "[MySQL] Error converting containers for DOIs with IDs #{id} - #{(id + 499)}." count end @@ -1244,7 +1365,7 @@ def check_container # to be used after DOIs were transferred to another DOI RA def self.delete_dois_by_prefix(prefix, options={}) if prefix.blank? - logger.error "[Error] No prefix provided." + Rails.logger.error "[Error] No prefix provided." return nil end @@ -1252,7 +1373,7 @@ def self.delete_dois_by_prefix(prefix, options={}) size = (options[:size] || 1000).to_i response = Doi.query(nil, prefix: prefix, page: { size: 1, cursor: [] }) - logger.info "#{response.results.total} DOIs found for prefix #{prefix}." + Rails.logger.info "#{response.results.total} DOIs found for prefix #{prefix}." if prefix && response.results.total > 0 # walk through results using cursor @@ -1262,7 +1383,7 @@ def self.delete_dois_by_prefix(prefix, options={}) response = Doi.query(nil, prefix: prefix, page: { size: size, cursor: cursor }) break unless response.results.results.length > 0 - logger.info "Deleting #{response.results.results.length} DOIs starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "Deleting #{response.results.results.length} DOIs starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] response.results.results.each do |d| @@ -1278,7 +1399,7 @@ def self.delete_dois_by_prefix(prefix, options={}) # provider europ registers their DOIs in the handle system themselves and are ignored def self.set_handle response = Doi.query("-registered:* +url:* -aasm_state:draft -provider_id:europ -agency:Crossref", page: { size: 1, cursor: [] }) - logger.info "#{response.results.total} DOIs found that are not registered in the Handle system." + Rails.logger.info "#{response.results.total} DOIs found that are not registered in the Handle system." if response.results.total > 0 # walk through results using cursor @@ -1288,7 +1409,7 @@ def self.set_handle response = Doi.query("-registered:* +url:* -aasm_state:draft -provider_id:europ -agency:Crossref", page: { size: 1000, cursor: cursor }) break unless response.results.results.length > 0 - logger.info "[Handle] Register #{response.results.results.length} DOIs in the handle system starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "[Handle] Register #{response.results.results.length} DOIs in the handle system starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] response.results.results.each do |d| @@ -1300,7 +1421,7 @@ def self.set_handle def self.set_url response = Doi.query("-url:* (+provider_id:ethz OR -aasm_status:draft)", page: { size: 1, cursor: [] }) - logger.info "#{response.results.total} DOIs with no URL found in the database." + Rails.logger.info "#{response.results.total} DOIs with no URL found in the database." if response.results.total > 0 # walk through results using cursor @@ -1310,7 +1431,7 @@ def self.set_url response = Doi.query("-url:* (+provider_id:ethz OR -aasm_status:draft)", page: { size: 1000, cursor: cursor }) break unless response.results.results.length.positive? - logger.info "[Handle] Update URL for #{response.results.results.length} DOIs starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "[Handle] Update URL for #{response.results.results.length} DOIs starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] response.results.results.each do |d| @@ -1322,7 +1443,7 @@ def self.set_url def self.set_minted response = Doi.query("provider_id:ethz AND +aasm_state:draft +url:*", page: { size: 1, cursor: [] }) - logger.info "#{response.results.total} draft DOIs from provider ETHZ found in the database." + Rails.logger.info "#{response.results.total} draft DOIs from provider ETHZ found in the database." if response.results.total > 0 # walk through results using cursor @@ -1332,7 +1453,7 @@ def self.set_minted response = Doi.query("provider_id:ethz AND +aasm_state:draft +url:*", page: { size: 1000, cursor: cursor }) break unless response.results.results.length.positive? - logger.info "[MySQL] Set minted for #{response.results.results.length} DOIs starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "[MySQL] Set minted for #{response.results.results.length} DOIs starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] response.results.results.each do |d| @@ -1344,12 +1465,12 @@ def self.set_minted def self.transfer(options={}) if options[:client_id].blank? - logger.error "[Transfer] No client provided." + Rails.logger.error "[Transfer] No client provided." return nil end if options[:target_id].blank? - logger.error "[Transfer] No target client provided." + Rails.logger.error "[Transfer] No target client provided." return nil end @@ -1357,7 +1478,7 @@ def self.transfer(options={}) size = (options[:size] || 1000).to_i response = Doi.query(nil, client_id: options[:client_id], page: { size: 1, cursor: [] }) - logger.info "[Transfer] #{response.results.total} DOIs found for client #{options[:client_id]}." + Rails.logger.info "[Transfer] #{response.results.total} DOIs found for client #{options[:client_id]}." if options[:client_id] && options[:target_id] && response.results.total > 0 # walk through results using cursor @@ -1367,7 +1488,7 @@ def self.transfer(options={}) response = Doi.query(nil, client_id: options[:client_id], page: { size: size, cursor: cursor }) break unless response.results.results.length.positive? - logger.info "[Transfer] Transferring #{response.results.results.length} DOIs starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "[Transfer] Transferring #{response.results.results.length} DOIs starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] response.results.results.each do |d| @@ -1391,7 +1512,7 @@ def set_defaults end def self.migrate_landing_page(options={}) - logger.info "Starting migration" + Rails.logger.info "Starting migration" # Handle camel casing first. Doi.where.not('last_landing_page_status_result' => nil).find_each do |doi| @@ -1441,10 +1562,10 @@ def self.migrate_landing_page(options={}) doi.update_columns("landing_page": landing_page) - logger.info "Updated " + doi.doi + Rails.logger.info "Updated " + doi.doi rescue TypeError, NoMethodError => error - logger.error "Error updating landing page " + doi.doi + ": " + error.message + Rails.logger.error "Error updating landing page " + doi.doi + ": " + error.message end end end diff --git a/app/models/event.rb b/app/models/event.rb index 385ef7418..fd18e945a 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -19,8 +19,9 @@ class Event < ActiveRecord::Base include Elasticsearch::Model before_validation :set_defaults + before_create :set_source_and_target_doi - validates :uuid, format: { with: /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/i } + validate :uuid_format # include state machine include AASM @@ -80,7 +81,6 @@ class Event < ActiveRecord::Base "is-referenced-by" ] - RELATIONS_RELATION_TYPES = [ "compiles", "is-compiled-by", "documents", "is-documented-by", @@ -122,7 +122,7 @@ class Event < ActiveRecord::Base proxyIdentifiers: { type: :keyword }, datePublished: { type: :date, format: "date_optional_time||yyyy-MM-dd||yyyy-MM||yyyy", ignore_malformed: true }, registrantId: { type: :keyword }, - cache_key: { type: :keyword } + cache_key: { type: :keyword }, } indexes :obj, type: :object, properties: { type: { type: :keyword }, @@ -131,8 +131,12 @@ class Event < ActiveRecord::Base proxyIdentifiers: { type: :keyword }, datePublished: { type: :date, format: "date_optional_time||yyyy-MM-dd||yyyy-MM||yyyy", ignore_malformed: true }, registrantId: { type: :keyword }, - cache_key: { type: :keyword } + cache_key: { type: :keyword }, } + indexes :source_doi, type: :keyword + indexes :target_doi, type: :keyword + indexes :source_relation_type_id, type: :keyword + indexes :target_relation_type_id, type: :keyword indexes :source_id, type: :keyword indexes :source_token, type: :keyword indexes :message_action, type: :keyword @@ -163,6 +167,10 @@ def as_indexed_json(options = {}) "obj_id" => obj_id, "subj" => subj.merge(cache_key: subj_cache_key), "obj" => obj.merge(cache_key: obj_cache_key), + "source_doi" => source_doi, + "target_doi" => target_doi, + "source_relation_type_id" => source_relation_type_id, + "target_relation_type_id" => target_relation_type_id, "doi" => doi, "orcid" => orcid, "issn" => issn, @@ -302,7 +310,7 @@ def self.import_by_ids(options = {}) # get every id between from_id and until_id (from_id..until_id).step(500).each do |id| EventImportByIdJob.perform_later(id: id) - logger.info "Queued importing for events with IDs starting with #{id}." unless Rails.env.test? + Rails.logger.info "Queued importing for events with IDs starting with #{id}." unless Rails.env.test? end end @@ -323,19 +331,19 @@ def self.import_by_id(options = {}) # log errors errors += response["items"].map { |k, v| k.values.first["error"] }.compact.length response["items"].select { |k, v| k.values.first["error"].present? }.each do |err| - logger.error "[Elasticsearch] " + err.inspect + Rails.logger.error "[Elasticsearch] " + err.inspect end count += events.length end if errors > 1 - logger.error "[Elasticsearch] #{errors} errors importing #{count} events with IDs #{id} - #{(id + 499)}." + Rails.logger.error "[Elasticsearch] #{errors} errors importing #{count} events with IDs #{id} - #{(id + 499)}." elsif count > 0 - logger.info "[Elasticsearch] Imported #{count} events with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Imported #{count} events with IDs #{id} - #{(id + 499)}." end rescue Elasticsearch::Transport::Transport::Errors::RequestEntityTooLarge, Faraday::ConnectionFailed, ActiveRecord::LockWaitTimeout => error - logger.info "[Elasticsearch] Error #{error.message} importing events with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Error #{error.message} importing events with IDs #{id} - #{(id + 499)}." count = 0 @@ -344,7 +352,7 @@ def self.import_by_id(options = {}) count += 1 end - logger.info "[Elasticsearch] Imported #{count} events with IDs #{id} - #{(id + 499)}." + Rails.logger.info "[Elasticsearch] Imported #{count} events with IDs #{id} - #{(id + 499)}." end def self.update_crossref(options = {}) @@ -352,15 +360,15 @@ def self.update_crossref(options = {}) cursor = (options[:cursor] || []) response = Event.query(nil, source_id: "crossref", page: { size: 1, cursor: [] }) - logger.info "[Update] #{response.results.total} events for source crossref." + Rails.logger.info "[Update] #{response.results.total} events for source crossref." # walk through results using cursor if response.results.total > 0 while response.results.results.length > 0 do response = Event.query(nil, source_id: "crossref", page: { size: size, cursor: cursor }) - break unless response.results.results.length > 0 + break unless response.results.results.length.positive? - logger.info "[Update] Updating #{response.results.results.length} crossref events starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "[Update] Updating #{response.results.results.length} crossref events starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] dois = response.results.results.map(&:subj_id).uniq @@ -371,6 +379,31 @@ def self.update_crossref(options = {}) response.results.total end + def self.update_target_doi(options = {}) + size = (options[:size] || 1000).to_i + cursor = (options[:cursor] || []) + + response = Event.query(nil, target_doi: nil, page: { size: 1, cursor: [] }) + Rails.logger.info "[Update] #{response.results.total} events with no target_doi." + + # walk through results using cursor + if response.results.total > 0 + while response.results.results.length > 0 do + response = Event.query(nil, target_doi: nil, page: { size: size, cursor: cursor }) + break unless response.results.results.length.positive? + + Rails.logger.info "[Update] Updating #{response.results.results.length} events with no target_doi starting with _id #{response.results.to_a.first[:_id]}." + cursor = response.results.to_a.last[:sort] + + ids = response.results.results.map(&:uuid).uniq + + TargetDoiJob.perform_later(ids, options) + end + end + + response.results.total + end + def self.update_datacite_crossref(options = {}) update_datacite_ra(options.merge(ra: "crossref")) end @@ -398,7 +431,7 @@ def self.update_datacite_ra(options = {}) source_id = "datacite-#{ra}" response = Event.query(nil, source_id: source_id, page: { size: 1, cursor: cursor }) - logger.info "[Update] #{response.results.total} events for source #{source_id}." + Rails.logger.info "[Update] #{response.results.total} events for source #{source_id}." # walk through results using cursor if response.results.total > 0 @@ -406,7 +439,7 @@ def self.update_datacite_ra(options = {}) response = Event.query(nil, source_id: source_id, page: { size: size, cursor: cursor }) break unless response.results.results.length > 0 - logger.info "[Update] Updating #{response.results.results.length} #{source_id} events starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "[Update] Updating #{response.results.results.length} #{source_id} events starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] dois = response.results.results.map(&:obj_id).uniq @@ -428,7 +461,7 @@ def self.update_registrant(options = {}) query = options[:query] || "registrant_id:*crossref.citations" response = Event.query(query, source_id: source_id, citation_type: citation_type, page: { size: 1, cursor: cursor }) - logger.info "[Update] #{response.results.total} events for sources #{source_id}." + Rails.logger.info "[Update] #{response.results.total} events for sources #{source_id}." # walk through results using cursor if response.results.total > 0 @@ -436,7 +469,7 @@ def self.update_registrant(options = {}) response = Event.query(query, source_id: source_id, citation_type: citation_type, page: { size: size, cursor: cursor }) break unless response.results.results.length > 0 - logger.info "[Update] Updating #{response.results.results.length} #{source_id} events starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "[Update] Updating #{response.results.results.length} #{source_id} events starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] ids = response.results.results.map(&:uuid).uniq @@ -453,7 +486,7 @@ def self.update_datacite_orcid_auto_update(options = {}) cursor = (options[:cursor] || []).to_i response = Event.query(nil, source_id: "datacite-orcid-auto-update", page: { size: 1, cursor: cursor }) - logger.info "[Update] #{response.results.total} events for source datacite-orcid-auto-update." + Rails.logger.info "[Update] #{response.results.total} events for source datacite-orcid-auto-update." # walk through results using cursor if response.results.total > 0 @@ -461,7 +494,7 @@ def self.update_datacite_orcid_auto_update(options = {}) response = Event.query(nil, source_id: "datacite-orcid-auto-update", page: { size: size, cursor: cursor }) break unless response.results.results.length > 0 - logger.info "[Update] Updating #{response.results.results.length} datacite-orcid-auto-update events starting with _id #{response.results.to_a.first[:_id]}." + Rails.logger.info "[Update] Updating #{response.results.results.length} datacite-orcid-auto-update events starting with _id #{response.results.to_a.first[:_id]}." cursor = response.results.to_a.last[:sort] ids = response.results.results.map(&:obj_id).uniq @@ -527,6 +560,10 @@ def issn nil end + def uuid_format + errors.add(:uuid, "#{uuid} is not a valid UUID") unless UUID.validate(uuid) + end + def registrant_id [subj["registrant_id"], obj["registrant_id"], subj["provider_id"], obj["provider_id"]].compact end @@ -588,6 +625,47 @@ def date_published(doi) item[:publication_year].to_s if item.present? end + def set_source_and_target_doi + case relation_type_id + when *ACTIVE_RELATION_TYPES + self.source_doi = doi_from_url(subj_id) + self.target_doi = doi_from_url(obj_id) + self.source_relation_type_id = "references" + self.target_relation_type_id = "citations" + when *PASSIVE_RELATION_TYPES + self.source_doi = doi_from_url(obj_id) + self.target_doi = doi_from_url(subj_id) + self.source_relation_type_id = "references" + self.target_relation_type_id = "citations" + when "unique-dataset-investigations-regular" + self.target_doi = doi_from_url(obj_id) + self.target_relation_type_id = "views" + when "unique-dataset-requests-regular" + self.target_doi = doi_from_url(obj_id) + self.target_relation_type_id = "downloads" + when "has-version" + self.source_doi = doi_from_url(subj_id) + self.target_doi = doi_from_url(obj_id) + self.source_relation_type_id = "versions" + self.target_relation_type_id = "version_of" + when "is-version-of" + self.source_doi = doi_from_url(obj_id) + self.target_doi = doi_from_url(subj_id) + self.source_relation_type_id = "versions" + self.target_relation_type_id = "version_of" + when "has-part" + self.source_doi = doi_from_url(subj_id) + self.target_doi = doi_from_url(obj_id) + self.source_relation_type_id = "parts" + self.target_relation_type_id = "part_of" + when "is-part-of" + self.source_doi = doi_from_url(obj_id) + self.target_doi = doi_from_url(subj_id) + self.source_relation_type_id = "parts" + self.target_relation_type_id = "part_of" + end + end + def set_defaults self.uuid = SecureRandom.uuid if uuid.blank? self.subj_id = normalize_doi(subj_id) || subj_id @@ -597,9 +675,6 @@ def set_defaults self.subj = subj.to_h.merge("id" => self.subj_id) self.obj = obj.to_h.merge("id" => self.obj_id) - # set doi_id for views and downloads - self.doi_id = doi_from_url(obj_id) if source_id == "datacite-usage" - self.total = 1 if total.blank? self.relation_type_id = "references" if relation_type_id.blank? self.occurred_at = Time.zone.now.utc if occurred_at.blank? diff --git a/app/models/provider.rb b/app/models/provider.rb index 979cd36a7..5698eda1c 100644 --- a/app/models/provider.rb +++ b/app/models/provider.rb @@ -45,6 +45,7 @@ class Provider < ActiveRecord::Base validates_inclusion_of :focus_area, :in => %w(naturalSciences engineeringAndTechnology medicalAndHealthSciences agriculturalSciences socialSciences humanities general), message: "focus area %s is not included in the list", if: :focus_area? validate :freeze_symbol, :on => :update validate :can_be_in_consortium + validate :uuid_format, if: :globus_uuid? validates_format_of :ror_id, :with => /\Ahttps:\/\/ror\.org\/0\w{6}\d{2}\z/, if: :ror_id?, message: "ROR ID should be a url" validates_format_of :twitter_handle, :with => /\A@[a-zA-Z0-9_]{1,15}\z/, if: :twitter_handle? @@ -84,13 +85,17 @@ class Provider < ActiveRecord::Base analyzer: { string_lowercase: { tokenizer: 'keyword', filter: %w(lowercase ascii_folding) } }, + normalizer: { + keyword_lowercase: { type: "custom", filter: %w(lowercase) } + }, filter: { ascii_folding: { type: 'asciifolding', preserve_original: true } } } } do mapping dynamic: 'false' do indexes :id, type: :keyword - indexes :uid, type: :keyword + indexes :uid, type: :keyword, normalizer: "keyword_lowercase" indexes :symbol, type: :keyword + indexes :globus_uuid, type: :keyword indexes :client_ids, type: :keyword indexes :prefix_ids, type: :keyword indexes :name, type: :text, fields: { keyword: { type: "keyword" }, raw: { type: "text", "analyzer": "string_lowercase", "fielddata": true }} @@ -202,6 +207,7 @@ def as_indexed_json(options={}) "twitter_handle" => twitter_handle, "ror_id" => ror_id, "salesforce_id" => salesforce_id, + "globus_uuid" => globus_uuid, "billing_information" => { "address" => billing_address, "organization" => billing_organization, @@ -233,7 +239,7 @@ def self.query_fields def self.query_aggregations { years: { date_histogram: { field: 'created', interval: 'year', min_doc_count: 1 } }, - cumulative_years: { terms: { field: 'cumulative_years', min_doc_count: 1, order: { _count: "asc" } } }, + cumulative_years: { terms: { field: 'cumulative_years', size: 15, min_doc_count: 1, order: { _count: "asc" } } }, regions: { terms: { field: 'region', size: 10, min_doc_count: 1 } }, member_types: { terms: { field: 'member_type', size: 10, min_doc_count: 1 } }, organization_types: { terms: { field: 'organization_type', size: 10, min_doc_count: 1 } }, @@ -528,6 +534,10 @@ def can_be_in_consortium end end + def uuid_format + errors.add(:globus_uuid, "#{globus_uuid} is not a valid UUID") unless UUID.validate(globus_uuid) + end + def freeze_symbol errors.add(:symbol, "cannot be changed") if self.symbol_changed? end diff --git a/app/serializers/client_serializer.rb b/app/serializers/client_serializer.rb index d75ccc305..3233ad067 100644 --- a/app/serializers/client_serializer.rb +++ b/app/serializers/client_serializer.rb @@ -4,7 +4,7 @@ class ClientSerializer set_type :clients set_id :uid - attributes :name, :symbol, :year, :contact_email, :alternate_name, :description, :language, :client_type, :domains, :re3data, :opendoar, :issn, :url, :salesforce_id, :created, :updated + attributes :name, :symbol, :year, :contact_email, :globus_uuid, :alternate_name, :description, :language, :client_type, :domains, :re3data, :opendoar, :issn, :url, :salesforce_id, :created, :updated belongs_to :provider, record_type: :providers belongs_to :consortium, record_type: :providers, serializer: ProviderSerializer, if: Proc.new { |client| client.consortium_id } @@ -26,6 +26,10 @@ class ClientSerializer object.salesforce_id end + attribute :globus_uuid, if: Proc.new { |object, params| params[:current_ability] && params[:current_ability].can?(:read_billing_information, object) == true } do |object| + object.globus_uuid + end + attribute :re3data do |object| "https://doi.org/#{object.re3data_id}" if object.re3data_id.present? end diff --git a/app/serializers/doi_serializer.rb b/app/serializers/doi_serializer.rb index d9444f9b7..3ae37017c 100644 --- a/app/serializers/doi_serializer.rb +++ b/app/serializers/doi_serializer.rb @@ -7,13 +7,13 @@ class DoiSerializer set_id :uid # don't cache dois, as works are cached using the doi model - attributes :doi, :prefix, :suffix, :identifiers, :creators, :titles, :publisher, :container, :publication_year, :subjects, :contributors, :dates, :language, :types, :related_identifiers, :sizes, :formats, :version, :rights_list, :descriptions, :geo_locations, :funding_references, :xml, :url, :content_url, :metadata_version, :schema_version, :source, :is_active, :state, :reason, :landing_page, :created, :registered, :published, :updated, :citations, :views, :downloads + attributes :doi, :prefix, :suffix, :identifiers, :creators, :titles, :publisher, :container, :publication_year, :subjects, :contributors, :dates, :language, :types, :related_identifiers, :sizes, :formats, :version, :rights_list, :descriptions, :geo_locations, :funding_references, :xml, :url, :content_url, :metadata_version, :schema_version, :source, :is_active, :state, :reason, :landing_page, :view_count, :download_count, :reference_count, :citation_count, :part_count, :part_of_count, :version_count, :version_of_count, :views_over_time, :downloads_over_time, :created, :registered, :published, :updated, :citations attributes :prefix, :suffix, if: Proc.new { |object, params| params && params[:detail] } belongs_to :client, record_type: :clients has_many :media, record_type: :media, id_method_name: :uid, if: Proc.new { |object, params| params && params[:detail] && !params[:is_collection]} - has_many :views, record_type: :event, id_method_name: :uuid, if: Proc.new { |object, params| params && params[:events]} - has_many :downloads, record_type: :event, id_method_name: :uuid, if: Proc.new { |object, params| params && params[:events]} + # has_many :views, record_type: :events, if: Proc.new { |object, params| params && params[:events]} + # has_many :downloads, record_type: :events, if: Proc.new { |object, params| params && params[:events]} attribute :xml, if: Proc.new { |object, params| params && params[:detail] } do |object| begin diff --git a/app/serializers/download_serializer.rb b/app/serializers/download_serializer.rb new file mode 100644 index 000000000..96265354a --- /dev/null +++ b/app/serializers/download_serializer.rb @@ -0,0 +1,17 @@ +class DownloadSerializer + include FastJsonapi::ObjectSerializer + # include BatchLoaderHelper + + set_key_transform :camel_lower + set_type :events + set_id :uuid + + attributes :subj_id, :obj_id, :source_id, :relation_type_id, :total, :message_action, :source_token, :license, :occurred_at, :timestamp + + + # has_many :dois, record_type: :dois, serializer: DoiSerializer, id_method_name: :doi do |object| + # load_doi(object) + # end + + attribute :timestamp, &:updated_at +end diff --git a/app/serializers/provider_serializer.rb b/app/serializers/provider_serializer.rb index 4a7edb614..2b8f0f200 100644 --- a/app/serializers/provider_serializer.rb +++ b/app/serializers/provider_serializer.rb @@ -5,7 +5,7 @@ class ProviderSerializer set_id :uid # cache_options enabled: true, cache_length: 24.hours ### we cannot filter if we cache - attributes :name, :display_name, :symbol, :website, :system_email, :group_email, :description, :region, :country, :logo_url, :member_type, :organization_type, :focus_area, :non_profit_status, :is_active, :has_password, :joined, :twitter_handle, :billing_information, :ror_id, :salesforce_id, :technical_contact, :secondary_technical_contact, :billing_contact, :secondary_billing_contact, :service_contact, :secondary_service_contact, :voting_contact, :created, :updated + attributes :name, :display_name, :symbol, :website, :system_email, :group_email, :globus_uuid, :description, :region, :country, :logo_url, :member_type, :organization_type, :focus_area, :non_profit_status, :is_active, :has_password, :joined, :twitter_handle, :billing_information, :ror_id, :salesforce_id, :technical_contact, :secondary_technical_contact, :billing_contact, :secondary_billing_contact, :service_contact, :secondary_service_contact, :voting_contact, :created, :updated has_many :clients, record_type: :clients has_many :prefixes, record_type: :prefixes @@ -32,6 +32,10 @@ class ProviderSerializer object.twitter_handle end + attribute :globus_uuid, if: Proc.new { |object, params| params[:current_ability] && params[:current_ability].can?(:read_billing_information, object) == true } do |object| + object.globus_uuid + end + # Convert all contacts json models back to json style camelCase attribute :technical_contact do |object| object.technical_contact.present? ? object.technical_contact.transform_keys!{ |key| key.to_s.camelcase(:lower) } : {} diff --git a/app/serializers/repository_serializer.rb b/app/serializers/repository_serializer.rb index e33c53c50..a113d7cc2 100644 --- a/app/serializers/repository_serializer.rb +++ b/app/serializers/repository_serializer.rb @@ -4,7 +4,7 @@ class RepositorySerializer set_type :repositories set_id :uid - attributes :name, :symbol, :re3data, :opendoar, :year, :system_email, :alternate_name, :description, :client_type, :repository_type, :language, :certificate, :domains, :issn, :url, :salesforce_id, :created, :updated + attributes :name, :symbol, :re3data, :opendoar, :year, :system_email, :globus_uuid, :alternate_name, :description, :client_type, :repository_type, :language, :certificate, :domains, :issn, :url, :salesforce_id, :created, :updated belongs_to :provider, record_type: :providers has_many :prefixes, record_type: :prefixes @@ -29,6 +29,10 @@ class RepositorySerializer object.service_contact.present? ? object.service_contact.transform_keys!{ |key| key.to_s.camelcase(:lower) } : {} end + attribute :globus_uuid, if: Proc.new { |object, params| params[:current_ability] && params[:current_ability].can?(:read_billing_information, object) == true } do |object| + object.globus_uuid + end + attribute :salesforce_id, if: Proc.new { |object, params| params[:current_ability] && params[:current_ability].can?(:read_salesforce_id, object) == true } do |object| object.salesforce_id end diff --git a/app/serializers/view_serializer.rb b/app/serializers/view_serializer.rb new file mode 100644 index 000000000..bac88eba0 --- /dev/null +++ b/app/serializers/view_serializer.rb @@ -0,0 +1,17 @@ +class ViewSerializer + include FastJsonapi::ObjectSerializer + # include BatchLoaderHelper + + set_key_transform :camel_lower + set_type :events + set_id :uuid + + attributes :subj_id, :obj_id, :source_id, :relation_type_id, :total, :message_action, :source_token, :license, :occurred_at, :timestamp + + + # has_many :dois, record_type: :dois, serializer: DoiSerializer, id_method_name: :doi do |object| + # load_doi(object) + # end + + attribute :timestamp, &:updated_at +end diff --git a/config/application.rb b/config/application.rb index 9df4f3da7..1a9a783ff 100644 --- a/config/application.rb +++ b/config/application.rb @@ -65,6 +65,8 @@ class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 5.2 + config.middleware.use Rack::CrawlerDetect + # include graphql config.paths.add Rails.root.join('app', 'graphql', 'types').to_s, eager_load: true config.paths.add Rails.root.join('app', 'graphql', 'mutations').to_s, eager_load: true @@ -81,6 +83,34 @@ class Application < Rails::Application # secret_key_base is not used by Rails API, as there are no sessions config.secret_key_base = "blipblapblup" + # config.lograge.enabled = true + # config.lograge.formatter = Lograge::Formatters::Logstash.new + # config.lograge.logger = ::LogStashLogger.new( + # type: :stdout, + # ) + # config.lograge.log_level = ENV["LOG_LEVEL"].to_sym + + # config.active_job.logger = config.lograge.logger + + # config.lograge.ignore_actions = ["HeartbeatController#index", "IndexController#index"] + # config.lograge.ignore_custom = lambda do |event| + # event.payload.inspect.length > 100000 + # end + # config.lograge.base_controller_class = "ActionController::API" + + # config.lograge.custom_options = lambda do |event| + # exceptions = %w(controller action format id) + # { + # params: event.payload[:params].except(*exceptions), + # uid: event.payload[:uid], + # } + # end + # config.logger = config.lograge.logger + + # Disable loggers that log to file + config.active_record.logger = nil + config.active_job.logger = nil + # configure caching config.cache_store = :dalli_store, nil, { :namespace => ENV['APPLICATION'] } diff --git a/config/initializers/_lograge.rb b/config/initializers/_lograge.rb index eb6693412..4065e2caa 100644 --- a/config/initializers/_lograge.rb +++ b/config/initializers/_lograge.rb @@ -6,9 +6,9 @@ config.lograge.enabled = true config.lograge.formatter = Lograge::Formatters::Logstash.new config.lograge.logger = LogStashLogger.new(type: :stdout) + config.logger = config.lograge.logger config.lograge.log_level = ENV["LOG_LEVEL"].to_sym - - config.active_job.logger = config.lograge.logger + config.logger = config.lograge.logger config.lograge.ignore_actions = ["HeartbeatController#index", "IndexController#index"] config.lograge.ignore_custom = lambda do |event| @@ -24,3 +24,5 @@ } end end + +Rails.cache.silence! diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 343a9eef6..71f67acd1 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -7,4 +7,5 @@ # ignore 502, 503 and 504 from Elasticsearch config.excluded_exceptions += ['Elasticsearch::Transport::Transport::Errors::BadGateway', 'Elasticsearch::Transport::Transport::Errors::ServiceUnavailable', 'Elasticsearch::Transport::Transport::Errors::GatewayTimeout'] + config.logger = Rails.application.config.lograge.logger end diff --git a/config/routes.rb b/config/routes.rb index f48c9436c..7b7a0925d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -44,7 +44,6 @@ get '/organizations/text/csv', :to => 'organizations#index', defaults: { format: :csv } get '/repositories/text/csv', :to => 'repositories#index', defaults: { format: :csv } - # manage DOIs post 'dois/validate', :to => 'dois#validate' post 'dois/undo', :to => 'dois#undo' @@ -56,6 +55,7 @@ get 'dois/get-dois', :to => 'dois#get_dois' get 'providers/totals', :to => 'providers#totals' get 'clients/totals', :to => 'clients#totals' + get 'repositories/totals', :to => 'repositories#totals' get 'prefixes/totals', :to => 'prefixes#totals' # Reporting @@ -63,7 +63,6 @@ get 'export/repositories', :to => 'export#repositories', defaults: { format: :csv } get 'export/contacts', :to => 'export#contacts', defaults: { format: :csv } - resources :heartbeat, only: [:index] resources :index, only: [:index] diff --git a/db/migrate/20200121101841_add_foreign_key_to_events.rb b/db/migrate/20200121101841_add_foreign_key_to_events.rb deleted file mode 100644 index 343230fb3..000000000 --- a/db/migrate/20200121101841_add_foreign_key_to_events.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -class AddForeignKeyToEvents < ActiveRecord::Migration[5.2] - def change - add_column :events, :doi_id, :text - add_index :events, [:doi_id, :relation_type_id], name: "index_events_on_doi_id", length: { doi_id: 100, relation_type_id: 191 } - end -end diff --git a/db/migrate/20200122153731_add_globus_uuid.rb b/db/migrate/20200122153731_add_globus_uuid.rb new file mode 100644 index 000000000..87bc581d1 --- /dev/null +++ b/db/migrate/20200122153731_add_globus_uuid.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddGlobusUuid < ActiveRecord::Migration[5.2] + def change + add_column :datacentre, :globus_uuid, :string, limit: 191 + add_column :allocator, :globus_uuid, :string, limit: 191 + + add_index :datacentre, [:globus_uuid], name: "index_datacentre_on_globus_uuid", length: { globus_uuid: 191 } + add_index :allocator, [:globus_uuid], name: "index_allocator_on_globus_uuid", length: { globus_uuid: 191 } + end +end diff --git a/db/migrate/20200131180609_add_events_properties.rb b/db/migrate/20200131180609_add_events_properties.rb new file mode 100644 index 000000000..53aaaa382 --- /dev/null +++ b/db/migrate/20200131180609_add_events_properties.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'lhm' + +class AddEventsProperties < ActiveRecord::Migration[5.2] + def up + Lhm.change_table :events do |m| + m.add_column :source_doi, "TEXT" + m.add_column :target_doi, "TEXT" + m.add_column :source_relation_type_id, "VARCHAR(191)" + m.add_column :target_relation_type_id, "VARCHAR(191)" + m.add_index ["source_doi(100)", "source_relation_type_id(191)"], :index_events_on_source_doi + m.add_index ["target_doi(100)", "target_relation_type_id(191)"], :index_events_on_target_doi + end + end + + def down + Lhm.change_table :events do |m| + m.remove_column :source_doi + m.remove_column :target_doi + m.remove_column :source_relation_type_id + m.remove_column :target_relation_type_id + m.remove_index ["source_doi(100)", "source_relation_type_id(191)"], :index_events_on_source_doi + m.remove_index ["target_doi(100)", "target_relation_type_id(191)"], :index_events_on_target_doi + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 7f3dcd3df..b0b06523a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_01_21_101841) do +ActiveRecord::Schema.define(version: 2020_01_31_180609) do create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin", force: :cascade do |t| t.string "name", limit: 191, null: false @@ -33,7 +33,7 @@ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end - create_table "allocator", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "allocator", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.string "system_email", null: false t.datetime "created" t.integer "doi_quota_allowed", null: false @@ -71,11 +71,13 @@ t.string "consortium_id" t.string "salesforce_id", limit: 191 t.string "non_profit_status", limit: 191 + t.string "globus_uuid", limit: 191 + t.index ["globus_uuid"], name: "index_allocator_on_globus_uuid" t.index ["organization_type"], name: "index_allocator_organization_type" t.index ["symbol"], name: "symbol", unique: true end - create_table "allocator_prefixes", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "allocator_prefixes", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.bigint "allocator", null: false t.bigint "prefixes", null: false t.datetime "created_at" @@ -107,7 +109,18 @@ t.index ["user_id", "user_type"], name: "user_index" end - create_table "datacentre", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "contacts", options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t| + t.bigint "allocator" + t.string "email" + t.string "given_name" + t.string "family_name" + t.string "role" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["allocator"], name: "fk_rails_5c598567a8" + end + + create_table "datacentre", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.text "comments", limit: 4294967295 t.string "system_email", null: false t.datetime "created" @@ -137,13 +150,15 @@ t.integer "opendoar_id" t.string "salesforce_id", limit: 191 t.json "service_contact" + t.string "globus_uuid", limit: 191 t.index ["allocator"], name: "FK6695D60546EBD781" + t.index ["globus_uuid"], name: "index_datacentre_on_globus_uuid" t.index ["re3data_id"], name: "index_datacentre_on_re3data_id" t.index ["symbol"], name: "symbol", unique: true t.index ["url"], name: "index_datacentre_on_url", length: 100 end - create_table "datacentre_prefixes", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "datacentre_prefixes", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.bigint "datacentre", null: false t.bigint "prefixes", null: false t.datetime "created_at" @@ -155,7 +170,7 @@ t.index ["prefixes"], name: "FK13A1B3BAAF86A1C7" end - create_table "dataset", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "dataset", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.datetime "created" t.string "doi", null: false t.binary "is_active", limit: 1, null: false @@ -210,7 +225,7 @@ t.index ["url"], name: "index_dataset_on_url", length: 100 end - create_table "events", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin", force: :cascade do |t| + create_table "events", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t| t.text "uuid", null: false t.text "subj_id", null: false t.text "obj_id" @@ -231,15 +246,21 @@ t.integer "total", default: 1 t.string "license", limit: 191 t.text "doi_id" + t.text "source_doi" + t.text "target_doi" + t.string "source_relation_type_id", limit: 191 + t.string "target_relation_type_id", limit: 191 t.index ["created_at", "indexed_at", "updated_at"], name: "index_events_on_created_indexed_updated" - t.index ["doi_id", "relation_type_id"], name: "index_events_on_doi_id", length: { doi_id: 100 } - t.index ["source_id", "created_at"], name: "index_events_on_source_id_created_at" - t.index ["subj_id", "obj_id", "source_id", "relation_type_id"], name: "index_events_on_multiple_columns", unique: true, length: { subj_id: 191, obj_id: 191 } + t.index ["created_at"], name: "index_events_on_source_id_created_at" + t.index ["relation_type_id"], name: "index_events_on_doi_id" + t.index ["source_doi", "source_relation_type_id"], name: "index_events_on_source_doi", length: { source_doi: 100 } + t.index ["subj_id", "obj_id", "relation_type_id"], name: "index_events_on_multiple_columns", unique: true, length: { subj_id: 191, obj_id: 191 } + t.index ["target_doi", "target_relation_type_id"], name: "index_events_on_target_doi", length: { target_doi: 100 } t.index ["updated_at"], name: "index_events_on_updated_at" t.index ["uuid"], name: "index_events_on_uuid", unique: true, length: 36 end - create_table "media", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "media", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.datetime "created" t.string "media_type", limit: 80 t.datetime "updated" @@ -251,7 +272,7 @@ t.index ["url"], name: "index_media_on_url", length: 100 end - create_table "metadata", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "metadata", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.datetime "created" t.integer "metadata_version" t.integer "version" @@ -263,7 +284,7 @@ t.index ["dataset"], name: "FKE52D7B2F4D3D6B1B" end - create_table "prefix", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "prefix", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.datetime "created" t.string "prefix", limit: 80, null: false t.integer "version" @@ -302,6 +323,7 @@ add_foreign_key "allocator_prefixes", "allocator", column: "allocator", name: "FKE7FBD67446EBD781" add_foreign_key "allocator_prefixes", "prefix", column: "prefixes", name: "FKE7FBD674AF86A1C7" + add_foreign_key "contacts", "allocator", column: "allocator" add_foreign_key "datacentre", "allocator", column: "allocator", name: "FK6695D60546EBD781" add_foreign_key "datacentre_prefixes", "datacentre", column: "datacentre", name: "FK13A1B3BA47B5F5FF" add_foreign_key "datacentre_prefixes", "prefix", column: "prefixes", name: "FK13A1B3BAAF86A1C7" diff --git a/lib/tasks/event.rake b/lib/tasks/event.rake index 727bf6d74..9a761968e 100644 --- a/lib/tasks/event.rake +++ b/lib/tasks/event.rake @@ -56,16 +56,23 @@ namespace :event do desc 'update registrant metadata' task :update_registrant => :environment do - cursor = ENV['CURSOR'].to_s.split(",") || [Event.minimum(:id),Event.minimum(:id)] + cursor = ENV['CURSOR'].to_s.split(",") || [Event.minimum(:id), Event.minimum(:id)] Event.update_registrant(cursor: cursor, size: ENV['SIZE']) end + + desc 'update target doi' + task :update_target_doi => :environment do + cursor = ENV['CURSOR'].to_s.split(",") || [Event.minimum(:id), Event.minimum(:id)] + + Event.update_target_doi(cursor: cursor, size: ENV['SIZE']) + end end namespace :crossref do desc 'Import crossref dois for all events' task :import_doi => :environment do - cursor = ENV['CURSOR'].to_s.split(",") || [Event.minimum(:id),Event.minimum(:id)] + cursor = ENV['CURSOR'].to_s.split(",") || [Event.minimum(:id), Event.minimum(:id)] Event.update_crossref(cursor: cursor) end diff --git a/lib/tasks/graphql.rake b/lib/tasks/graphql.rake new file mode 100644 index 000000000..a4df3cfff --- /dev/null +++ b/lib/tasks/graphql.rake @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +namespace :graphql do + task :dump_schema => :environment do + # Get a string containing the definition in GraphQL IDL: + schema_defn = LupoSchema.to_definition + # Choose a place to write the schema dump: + schema_path = "app/graphql/schema.graphql" + # Write the schema dump to that file: + File.write(Rails.root.join(schema_path), schema_defn) + puts "Updated #{schema_path}" + end +end diff --git a/spec/concerns/authenticable_spec.rb b/spec/concerns/authenticable_spec.rb index d13aca379..b5fe758d7 100644 --- a/spec/concerns/authenticable_spec.rb +++ b/spec/concerns/authenticable_spec.rb @@ -4,7 +4,7 @@ let(:token) { User.generate_token } subject { User.new(token) } - describe 'decode_token' do + describe "decode_token DataCite" do it "has name" do payload = subject.decode_token(token) expect(payload["name"]).to eq("Josiah Carberry") @@ -28,6 +28,30 @@ end end + # describe "decode_token Globus", vcr: true do + # it "has name" do + # payload = subject.decode_token(token) + # expect(payload["name"]).to eq("Josiah Carberry") + # end + + # it "empty token" do + # payload = subject.decode_token("") + # expect(payload).to eq(errors: "The token could not be decoded.") + # end + + # it "invalid token" do + # payload = subject.decode_token("abc") + # expect(payload).to eq(errors: "The token could not be decoded.") + # end + + # it "expired token" do + # token = User.generate_token(exp: 0) + # subject = User.new(token) + # payload = subject.decode_token(token) + # expect(payload).to eq(errors: "The token has expired.") + # end + # end + describe 'decode_alb_token' do let(:token) { User.generate_alb_token } @@ -197,6 +221,18 @@ expect(token).to be_nil end end + + describe 'encode_globus_token' do + it "with name" do + token = subject.encode_globus_token("name" => "Josiah Carberry") + expect(token).to start_with("eyJhbG") + end + + it "empty string" do + token = subject.encode_globus_token("") + expect(token).to be_nil + end + end end describe Provider, type: :model do diff --git a/spec/factories/default.rb b/spec/factories/default.rb index 788789b72..fb3f92944 100644 --- a/spec/factories/default.rb +++ b/spec/factories/default.rb @@ -44,6 +44,7 @@ "given_name": "Martin", "family_name": "Fenner" }} + globus_uuid { "bc7d0274-3472-4a79-b631-e4c7baccc667" } sequence(:symbol) { |n| provider.symbol + ".TEST#{n}" } name { "My data center" } role_name { "ROLE_DATACENTRE" } @@ -240,6 +241,7 @@ factory :provider do system_email { "josiah@example.org" } sequence(:symbol, 'A') { |n| "TEST#{n}" } + globus_uuid { "53d8d984-450d-4b1d-970b-67faff28db1c" } name { "My provider" } display_name { "My provider" } website { Faker::Internet.url } @@ -333,6 +335,41 @@ relation_type_id { "references" } end + factory :event_for_datacite_parts do + source_id { "datacite_related" } + source_token { "datacite_related_123" } + subj_id { "http://doi.org/10.5061/DRYAD.47SD5" } + subj { { "datePublished" => "2006-06-13T16:14:19Z" } } + sequence(:obj_id) { |n| "http://doi.org/10.5061/DRYAD.47SD5/#{n}" } + relation_type_id { "has-part" } + end + + factory :event_for_datacite_part_of do + source_id { "datacite_related" } + source_token { "datacite_related_123" } + subj_id { "http://doi.org/10.5061/DRYAD.47SD5/1" } + subj { { "datePublished" => "2006-06-13T16:14:19Z" } } + obj_id { "http://doi.org/10.5061/DRYAD.47SD5" } + relation_type_id { "is-part-of" } + end + + factory :event_for_datacite_crossref do + source_id { "datacite_crossref" } + source_token { "datacite_crossref_123" } + sequence(:subj_id) { |n| "https://doi.org/10.5061/DRYAD.47SD5e/#{n}" } + subj { { "datePublished" => "2006-06-13T16:14:19Z" } } + obj_id { "https://doi.org/10.1371/journal.pbio.2001414" } + relation_type_id { "is-referenced-by" } + end + + factory :event_for_crossref do + source_id { "crossref" } + source_token { "crossref_123" } + subj_id { "https://doi.org/10.1371/journal.pbio.2001414" } + sequence(:obj_id) { |n| "https://doi.org/10.5061/DRYAD.47SD5e/#{n}" } + relation_type_id { "references" } + end + factory :event_for_datacite_usage do source_id { "datacite-usage" } source_token { "5348967fhdjksr3wyui325" } @@ -356,5 +393,14 @@ relation_type_id { "unique-dataset-investigations-regular" } occurred_at { "2015-06-13T16:14:19Z" } end + + factory :event_for_datacite_orcid_auto_update do + source_id { "datacite-orcid-auto-update" } + source_token { "5348967fhdjksr3wyui325" } + sequence(:obj_id) { |n| "https://orcid.org/0000-0003-1419-211#{n}}" } + sequence(:subj_id) { |n| "http://doi.org/10.5061/DRYAD.47SD5e/#{n}" } + relation_type_id { "is-authored-by" } + occurred_at { "2015-06-13T16:14:19Z" } + end end end diff --git a/spec/fixtures/certs/ec512-private.pem b/spec/fixtures/certs/ec512-private.pem new file mode 100644 index 000000000..6b99b7f0d --- /dev/null +++ b/spec/fixtures/certs/ec512-private.pem @@ -0,0 +1,10 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIB0/+ffxEj7j62xvGaB5pvzk888e412ESO/EK/K0QlS9dSF8+Rj1rG +zqpRB8fvDnoe8xdmkW/W5GKzojMyv7YQYumgBwYFK4EEACOhgYkDgYYABAEw74Yw +aTbPY6TtWmxx6LJDzCX2nKWCPnKdZcEH9Ncu8g5RjRBRq2yacja3OoS6nA2YeDng +reBJxZr376P6Ns6XcQFWDA6K/MCTrEBCsPxXZNxd8KR9vMGWhgNtWRrcKzwJfQkr +suyehZkbbYyFnAWyARKHZuV7VUXmeEmRS/f93MPqVA== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/spec/fixtures/certs/ec512-public.pem b/spec/fixtures/certs/ec512-public.pem new file mode 100644 index 000000000..3e5b266a4 --- /dev/null +++ b/spec/fixtures/certs/ec512-public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQBMO+GMGk2z2Ok7VpsceiyQ8wl9pyl +gj5ynWXBB/TXLvIOUY0QUatsmnI2tzqEupwNmHg54K3gScWa9++j+jbOl3EBVgwO +ivzAk6xAQrD8V2TcXfCkfbzBloYDbVka3Cs8CX0JK7LsnoWZG22MhZwFsgESh2bl +e1VF5nhJkUv3/dzD6lQ= +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/spec/fixtures/certs/ec512-wrong-private.pem b/spec/fixtures/certs/ec512-wrong-private.pem new file mode 100644 index 000000000..476fd3754 --- /dev/null +++ b/spec/fixtures/certs/ec512-wrong-private.pem @@ -0,0 +1,10 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIHbAgEBBEG/KbA2oCbiCT6L3V8XSz2WKBy0XhGvIFbl/ZkXIXnkYt+1B7wViSVo +KCHuMFsi6xU/5nE1EuDG2UsQJmKeAMkIOKAHBgUrgQQAI6GBiQOBhgAEAG0TFWe5 +cZ5DZIyfuysrCoQySTNxd+aT8sPIxsx7mW6YBTsuO6rEgxyegd2Auy4xtikxpzKv +soMXR02999Aaus2jAAt/wxrhhr41BDP4MV0b6Zngb72hna0pcGqit5OyU8AbOJUZ ++rdyowRGsOY+aPbOyVhdNcsEdxYC8GdIyCQLBC1H +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/spec/fixtures/certs/ec512-wrong-public.pem b/spec/fixtures/certs/ec512-wrong-public.pem new file mode 100644 index 000000000..8ebeb0114 --- /dev/null +++ b/spec/fixtures/certs/ec512-wrong-public.pem @@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAbRMVZ7lxnkNkjJ+7KysKhDJJM3F3 +5pPyw8jGzHuZbpgFOy47qsSDHJ6B3YC7LjG2KTGnMq+ygxdHTb330Bq6zaMAC3/D +GuGGvjUEM/gxXRvpmeBvvaGdrSlwaqK3k7JTwBs4lRn6t3KjBEaw5j5o9s7JWF01 +ywR3FgLwZ0jIJAsELUc= +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/spec/graphql/printout_spec.rb b/spec/graphql/printout_spec.rb new file mode 100644 index 000000000..d830cfa7e --- /dev/null +++ b/spec/graphql/printout_spec.rb @@ -0,0 +1,9 @@ +require "rails_helper" + +describe LupoSchema do + it "printout is up-to-date" do + current_defn = LupoSchema.to_definition + printout_defn = File.read(Rails.root.join("app/graphql/schema.graphql")) + expect(current_defn).to eq(printout_defn) + end +end diff --git a/spec/graphql/types/client_type_spec.rb b/spec/graphql/types/client_type_spec.rb new file mode 100644 index 000000000..679b8b7bd --- /dev/null +++ b/spec/graphql/types/client_type_spec.rb @@ -0,0 +1,14 @@ +require "rails_helper" + +describe ClientType do + describe "fields" do + subject { described_class } + + it { is_expected.to have_field(:id).of_type(!types.ID) } + it { is_expected.to have_field(:type).of_type("String!") } + it { is_expected.to have_field(:name).of_type("String!") } + it { is_expected.to have_field(:alternateName).of_type("String") } + it { is_expected.to have_field(:description).of_type("String") } + it { is_expected.to have_field(:datasets).of_type("ClientDatasetConnectionWithMeta!") } + end +end diff --git a/spec/graphql/types/data_catalog_type_spec.rb b/spec/graphql/types/data_catalog_type_spec.rb new file mode 100644 index 000000000..26f57fc7d --- /dev/null +++ b/spec/graphql/types/data_catalog_type_spec.rb @@ -0,0 +1,16 @@ +# require "rails_helper" + +# describe DataCatalogType do +# describe "fields" do +# subject { described_class } + +# it { is_expected.to have_field(:id).of_type(!types.ID) } +# it { is_expected.to have_field(:type).of_type("String!") } +# # it { is_expected.to have_field(:name).of_type("[Person!]") } +# # it { is_expected.to have_field(:alternateName).of_type("[Title!]") } +# # it { is_expected.to have_field(:description).of_type("Int") } +# # it { is_expected.to have_field(:certicates).of_type("String") } +# # it { is_expected.to have_field(:subjects).of_type("String") } +# # it { is_expected.to have_field(:datasets).of_type("String") } +# end +# end diff --git a/spec/graphql/types/dataset_type_spec.rb b/spec/graphql/types/dataset_type_spec.rb new file mode 100644 index 000000000..0116cd62d --- /dev/null +++ b/spec/graphql/types/dataset_type_spec.rb @@ -0,0 +1,13 @@ +require "rails_helper" + +describe DatasetType do + describe "fields" do + subject { described_class } + + it { is_expected.to have_field(:id).of_type(!types.ID) } + it { is_expected.to have_field(:type).of_type("String!") } + it { is_expected.to have_field(:datasets).of_type("DatasetDatasetConnectionWithMeta!") } + it { is_expected.to have_field(:publications).of_type("DatasetPublicationConnectionWithMeta!") } + it { is_expected.to have_field(:softwareSourceCodes).of_type("DatasetSoftwareConnectionWithMeta!") } + end +end diff --git a/spec/graphql/types/doi_item_spec.rb b/spec/graphql/types/doi_item_spec.rb new file mode 100644 index 000000000..5f5893dee --- /dev/null +++ b/spec/graphql/types/doi_item_spec.rb @@ -0,0 +1,14 @@ +require "rails_helper" + +describe DoiItem do + describe "fields" do + subject { described_class } + + it { is_expected.to have_field(:id).of_type(!types.ID) } + it { is_expected.to have_field(:type).of_type("String!") } + it { is_expected.to have_field(:creators).of_type("[Person!]") } + it { is_expected.to have_field(:titles).of_type("[Title!]") } + it { is_expected.to have_field(:publicationYear).of_type("Int") } + it { is_expected.to have_field(:publisher).of_type("String") } + end +end diff --git a/spec/graphql/types/funder_type_spec.rb b/spec/graphql/types/funder_type_spec.rb new file mode 100644 index 000000000..3d57ac5e8 --- /dev/null +++ b/spec/graphql/types/funder_type_spec.rb @@ -0,0 +1,13 @@ +require "rails_helper" + +describe FunderType do + describe "fields" do + subject { described_class } + + it { is_expected.to have_field(:id).of_type(!types.ID) } + it { is_expected.to have_field(:type).of_type("String!") } + it { is_expected.to have_field(:name).of_type("String!") } + it { is_expected.to have_field(:alternateName).of_type("[String!]") } + it { is_expected.to have_field(:datasets).of_type("FunderDatasetConnectionWithMeta!") } + end +end diff --git a/spec/graphql/types/provider_type_spec.rb b/spec/graphql/types/provider_type_spec.rb new file mode 100644 index 000000000..dcc7c5fe1 --- /dev/null +++ b/spec/graphql/types/provider_type_spec.rb @@ -0,0 +1,14 @@ +require "rails_helper" + +describe ProviderType do + describe "fields" do + subject { described_class } + + it { is_expected.to have_field(:id).of_type(!types.ID) } + it { is_expected.to have_field(:type).of_type("String!") } + it { is_expected.to have_field(:name).of_type("String!") } + it { is_expected.to have_field(:displayName).of_type("String") } + it { is_expected.to have_field(:description).of_type("String") } + it { is_expected.to have_field(:clients).of_type("ProviderClientConnectionWithMeta!") } + end +end diff --git a/spec/graphql/types/publication_type_spec.rb b/spec/graphql/types/publication_type_spec.rb new file mode 100644 index 000000000..ce1d75b13 --- /dev/null +++ b/spec/graphql/types/publication_type_spec.rb @@ -0,0 +1,13 @@ +require "rails_helper" + +describe PublicationType do + describe "fields" do + subject { described_class } + + it { is_expected.to have_field(:id).of_type(!types.ID) } + it { is_expected.to have_field(:type).of_type("String!") } + it { is_expected.to have_field(:datasets).of_type("PublicationDatasetConnectionWithMeta!") } + it { is_expected.to have_field(:publications).of_type("PublicationPublicationConnectionWithMeta!") } + it { is_expected.to have_field(:softwareSourceCodes).of_type("PublicationSoftwareConnectionWithMeta!") } + end +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb new file mode 100644 index 000000000..9bf5ce276 --- /dev/null +++ b/spec/graphql/types/query_type_spec.rb @@ -0,0 +1,42 @@ +require "rails_helper" + +describe QueryType do + describe "fields" do + subject { described_class } + + it { is_expected.to have_field(:dataset).of_type("Dataset!") } + it { is_expected.to have_field(:datasets).of_type("DatasetConnectionWithMeta!") } + it { is_expected.to have_field(:publication).of_type("Publication!") } + it { is_expected.to have_field(:publications).of_type("PublicationConnectionWithMeta!") } + it { is_expected.to have_field(:service).of_type("Service!") } + it { is_expected.to have_field(:services).of_type("ServiceConnectionWithMeta!") } + end + + describe "query datasets", elasticsearch: true do + let!(:datasets) { create_list(:doi, 3, aasm_state: "findable") } + + before do + Doi.import + sleep 1 + end + + let(:query) do + %(query { + datasets { + totalCount + nodes { + id + } + } + }) + end + + it "returns all datasets" do + response = LupoSchema.execute(query).as_json + + expect(response.dig("data", "datasets", "totalCount")).to eq(3) + expect(response.dig("data", "datasets", "nodes").length).to eq(3) + expect(response.dig("data", "datasets", "nodes", 0, "id")).to eq(datasets.first.identifier) + end + end +end diff --git a/spec/graphql/types/software_type_spec.rb b/spec/graphql/types/software_type_spec.rb new file mode 100644 index 000000000..400204f8e --- /dev/null +++ b/spec/graphql/types/software_type_spec.rb @@ -0,0 +1,13 @@ +require "rails_helper" + +describe SoftwareType do + describe "fields" do + subject { described_class } + + it { is_expected.to have_field(:id).of_type(!types.ID) } + it { is_expected.to have_field(:type).of_type("String!") } + it { is_expected.to have_field(:datasets).of_type("SoftwareDatasetConnectionWithMeta!") } + it { is_expected.to have_field(:publications).of_type("SoftwarePublicationConnectionWithMeta!") } + it { is_expected.to have_field(:softwareSourceCodes).of_type("SoftwareSoftwareConnectionWithMeta!") } + end +end diff --git a/spec/models/client_spec.rb b/spec/models/client_spec.rb index 6241a9ee6..289d84ff9 100644 --- a/spec/models/client_spec.rb +++ b/spec/models/client_spec.rb @@ -156,6 +156,28 @@ end end + describe "globus_uuid" do + let(:client) { build(:client, provider: provider) } + + it "should support version 1 UUID" do + client.globus_uuid = "6d133cee-3d3f-11ea-b77f-2e728ce88125" + expect(client.save).to be true + expect(client.errors.details).to be_empty + end + + it "should support version 4 UUID" do + client.globus_uuid = "9908a164-1e4f-4c17-ae1b-cc318839d6c8" + expect(client.save).to be true + expect(client.errors.details).to be_empty + end + + it "should reject string that is not a UUID" do + client.globus_uuid = "abc" + expect(client.save).to be false + expect(client.errors.details).to eq(:globus_uuid=>[{:error=>"abc is not a valid UUID"}]) + end + end + describe "cumulative_years" do before(:each) do allow(Time).to receive(:now).and_return(Time.mktime(2015, 4, 8)) diff --git a/spec/models/doi_spec.rb b/spec/models/doi_spec.rb index f5e39d499..e87439b77 100644 --- a/spec/models/doi_spec.rb +++ b/spec/models/doi_spec.rb @@ -540,6 +540,174 @@ end end + describe "views", elasticsearch: true do + let(:client) { create(:client) } + let(:doi) { create(:doi, client: client, aasm_state: "findable") } + let!(:views) { create_list(:event_for_datacite_usage, 3, obj_id: "https://doi.org/#{doi.doi}", relation_type_id: "unique-dataset-investigations-regular", total: 25) } + + before do + Doi.import + sleep 1 + end + + it "has views" do + expect(doi.views.count).to eq(3) + expect(doi.view_count).to eq(75) + expect(doi.views_over_time.first).to eq(:total=>25, :year_month=>"2015-06") + + view = doi.views.first + expect(view.target_doi).to eq(doi.uid) + expect(view.total).to eq(25) + end + end + + describe "downloads", elasticsearch: true do + let(:client) { create(:client) } + let(:doi) { create(:doi, client: client, aasm_state: "findable") } + let!(:downloads) { create_list(:event_for_datacite_usage, 3, obj_id: "https://doi.org/#{doi.doi}", relation_type_id: "unique-dataset-requests-regular", total: 10) } + + before do + Doi.import + sleep 1 + end + + it "has downloads" do + expect(doi.downloads.count).to eq(3) + expect(doi.download_count).to eq(30) + expect(doi.downloads_over_time.first).to eq(:total=>10, :year_month=>"2015-06") + + download = doi.downloads.first + expect(download.target_doi).to eq(doi.uid) + expect(download.total).to eq(10) + end + end + + describe "references", elasticsearch: true do + let(:client) { create(:client) } + let(:doi) { create(:doi, client: client, aasm_state: "findable") } + let!(:references) { create_list(:event_for_crossref, 3, subj_id: "https://doi.org/#{doi.doi}", relation_type_id: "references") } + + before do + Doi.import + sleep 1 + end + + it "has references" do + expect(doi.references.count).to eq(3) + expect(doi.reference_ids.count).to eq(3) + expect(doi.reference_count).to eq(3) + + reference = doi.references.first + expect(reference.source_doi).to eq(doi.uid) + expect(reference.total).to eq(1) + end + end + + describe "citations", elasticsearch: true do + let(:client) { create(:client) } + let(:doi) { create(:doi, client: client, aasm_state: "findable") } + let!(:citations) { create_list(:event_for_datacite_crossref, 1, subj_id: "https://doi.org/#{doi.doi}", relation_type_id: "is-referenced-by") } + + before do + Doi.import + sleep 1 + end + + it "has citations" do + expect(doi.citations.count).to eq(1) + expect(doi.citation_ids.count).to eq(1) + expect(doi.citation_count).to eq(1) + + citation = doi.citations.first + expect(citation.target_doi).to eq(doi.uid) + expect(citation.total).to eq(1) + end + end + + describe "parts", elasticsearch: true do + let(:client) { create(:client) } + let(:doi) { create(:doi, client: client, aasm_state: "findable") } + let!(:parts) { create_list(:event_for_datacite_parts, 3, subj_id: "https://doi.org/#{doi.doi}", relation_type_id: "has-part") } + + before do + Doi.import + sleep 1 + end + + it "has parts" do + expect(doi.parts.count).to eq(3) + expect(doi.part_ids.count).to eq(3) + expect(doi.part_count).to eq(3) + + part = doi.parts.first + expect(part.source_doi).to eq(doi.uid) + expect(part.total).to eq(1) + end + end + + describe "part of", elasticsearch: true do + let(:client) { create(:client) } + let(:doi) { create(:doi, client: client, aasm_state: "findable") } + let!(:part_of) { create_list(:event_for_datacite_part_of, 1, subj_id: "https://doi.org/#{doi.doi}", relation_type_id: "is-part-of") } + + before do + Doi.import + sleep 1 + end + + it "has part of" do + expect(doi.part_of.count).to eq(1) + expect(doi.part_of_ids.count).to eq(1) + expect(doi.part_of_count).to eq(1) + + part_of = doi.part_of.first + expect(part_of.target_doi).to eq(doi.uid) + expect(part_of.total).to eq(1) + end + end + + describe "versions", elasticsearch: true do + let(:client) { create(:client) } + let(:doi) { create(:doi, client: client, aasm_state: "findable") } + let!(:versions) { create_list(:event_for_datacite_parts, 3, subj_id: "https://doi.org/#{doi.doi}", relation_type_id: "has-version") } + + before do + Doi.import + sleep 1 + end + + it "has versions" do + expect(doi.versions.count).to eq(3) + expect(doi.version_ids.count).to eq(3) + expect(doi.version_count).to eq(3) + + version = doi.versions.first + expect(version.source_doi).to eq(doi.uid) + expect(version.total).to eq(1) + end + end + + describe "version of", elasticsearch: true do + let(:client) { create(:client) } + let(:doi) { create(:doi, client: client, aasm_state: "findable") } + let!(:part_of) { create_list(:event_for_datacite_part_of, 1, subj_id: "https://doi.org/#{doi.doi}", relation_type_id: "is-version-of") } + + before do + Doi.import + sleep 1 + end + + it "has version of" do + expect(doi.version_of.count).to eq(1) + expect(doi.version_of_ids.count).to eq(1) + expect(doi.version_of_count).to eq(1) + + version_of = doi.version_of.first + expect(version_of.target_doi).to eq(doi.uid) + expect(version_of.total).to eq(1) + end + end + describe "convert_affiliations" do let(:doi) { create(:doi)} diff --git a/spec/models/provider_spec.rb b/spec/models/provider_spec.rb index 3a474df8b..b5c157eae 100644 --- a/spec/models/provider_spec.rb +++ b/spec/models/provider_spec.rb @@ -135,6 +135,28 @@ end end + describe "globus_uuid" do + let(:provider) { build(:provider) } + + it "should support version 1 UUID" do + provider.globus_uuid = "6d133cee-3d3f-11ea-b77f-2e728ce88125" + expect(provider.save).to be true + expect(provider.errors.details).to be_empty + end + + it "should support version 4 UUID" do + provider.globus_uuid = "9908a164-1e4f-4c17-ae1b-cc318839d6c8" + expect(provider.save).to be true + expect(provider.errors.details).to be_empty + end + + it "should reject string that is not a UUID" do + provider.globus_uuid = "abc" + expect(provider.save).to be false + expect(provider.errors.details).to eq(:globus_uuid=>[{:error=>"abc is not a valid UUID"}]) + end + end + describe "cumulative_years" do before(:each) do allow(Time).to receive(:now).and_return(Time.mktime(2015, 4, 8)) diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 7722396d5..070cc31b2 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -40,6 +40,8 @@ config.include StripAttributes::Matchers config.include RSpec::Benchmark::Matchers config.include Rack::Test::Methods, type: :request + config.include RSpec::GraphqlMatchers::TypesHelper + # don't use transactions, use database_clear gem via support file config.use_transactional_fixtures = false diff --git a/spec/requests/clients_spec.rb b/spec/requests/clients_spec.rb index 0f828f41e..6ef90d57a 100644 --- a/spec/requests/clients_spec.rb +++ b/spec/requests/clients_spec.rb @@ -79,6 +79,7 @@ expect(last_response.status).to eq(200) expect(json.dig('data', 'attributes', 'name')).to eq(client.name) + expect(json.dig('data', 'attributes', 'globusUuid')).to eq("bc7d0274-3472-4a79-b631-e4c7baccc667") end end @@ -152,7 +153,8 @@ let(:params) do { "data" => { "type" => "clients", "attributes" => { - "name" => "Imperial College 2"}} } + "name" => "Imperial College 2", + "globusUuid" => "9908a164-1e4f-4c17-ae1b-cc318839d6c8" }} } end it 'updates the record' do @@ -160,6 +162,7 @@ expect(last_response.status).to eq(200) expect(json.dig('data', 'attributes', 'name')).to eq("Imperial College 2") + expect(json.dig('data', 'attributes', 'globusUuid')).to eq("9908a164-1e4f-4c17-ae1b-cc318839d6c8") expect(json.dig('data', 'attributes', 'name')).not_to eq(client.name) end end @@ -179,6 +182,37 @@ end end + context 'removes the globus_uuid' do + let(:params) do + { "data" => { "type" => "clients", + "attributes" => { + "globusUuid" => nil }} } + end + + it 'updates the record' do + put "/clients/#{client.symbol}", params, headers + + expect(last_response.status).to eq(200) + expect(json.dig('data', 'attributes', 'name')).to eq("My data center") + expect(json.dig('data', 'attributes', 'globusUuid')).to be_nil + end + end + + context 'invalid globus_uuid' do + let(:params) do + { "data" => { "type" => "clients", + "attributes" => { + "globusUuid" => "abc" }} } + end + + it 'updates the record' do + put "/clients/#{client.symbol}", params, headers + + expect(last_response.status).to eq(422) + expect(json["errors"].first).to eq("source"=>"globus_uuid", "title"=>"Abc is not a valid UUID") + end + end + context 'using basic auth', vcr: true do let(:params) do { "data" => { "type" => "clients", diff --git a/spec/requests/events_spec.rb b/spec/requests/events_spec.rb index ab5ad0ded..ec97d9ada 100644 --- a/spec/requests/events_spec.rb +++ b/spec/requests/events_spec.rb @@ -923,6 +923,20 @@ # expect(attributes["subj-id"]).to eq(event.subj_id) # end # end + context "query by source-id by Crawler" do + let(:uri) { "/events?query=datacite" } + + # Exclude the token header. + let(:headers) do + { "HTTP_ACCEPT" => "application/json", + "HTTP_USER_AGENT" => "Mozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" } + end + + it "json" do + get uri, nil, headers + expect(last_response.status).to eq(404) + end + end end context "destroy" do diff --git a/spec/requests/providers_spec.rb b/spec/requests/providers_spec.rb index bb3bbae4e..833f5892e 100644 --- a/spec/requests/providers_spec.rb +++ b/spec/requests/providers_spec.rb @@ -96,29 +96,28 @@ end end - # describe 'GET /providers/totals' do - # let(:provider) { create(:provider) } - # let(:client) { create(:client, provider: provider) } - # let!(:prefixes) { create_list(:prefix, 10) } - # let!(:dois) { create_list(:doi, 3, client: client, aasm_state: "findable") } - - # before do - # Provider.import - # Client.import - # sleep 2 - # end + describe 'GET /providers/totals' do + let(:provider) { create(:provider) } + let(:client) { create(:client, provider: provider) } + let!(:prefixes) { create_list(:prefix, 10) } + let!(:dois) { create_list(:doi, 3, client: client, aasm_state: "findable") } - # it "returns providers" do - # get "/providers/totals", nil, headers + before do + Provider.import + Client.import + Doi.import + sleep 3 + end - # puts last_response.body + it "returns providers" do + get "/providers/totals", nil, headers - # expect(last_response.status).to eq(200) - # expect(json['data'].size).to eq(4) - # expect(json.dig('meta', 'total')).to eq(4) - # expect(json.dig('meta')).to eq(4) - # end - # end + expect(last_response.status).to eq(200) + # expect(json['data'].size).to eq(4) + expect(json.first.dig('count')).to eq(3) + expect(json.first.dig('temporal')).not_to be_nil + end + end describe 'POST /providers' do context 'request is valid' do @@ -594,6 +593,7 @@ { "data" => { "type" => "providers", "attributes" => { "name" => "British Library", + "globusUuid" => "9908a164-1e4f-4c17-ae1b-cc318839d6c8", "displayName" => "British Library", "website" => "https://www.bl.uk", "region" => "Americas", @@ -606,6 +606,38 @@ expect(last_response.status).to eq(200) expect(json.dig('data', 'attributes', 'displayName')).to eq("British Library") + expect(json.dig('data', 'attributes', 'globusUuid')).to eq("9908a164-1e4f-4c17-ae1b-cc318839d6c8") + end + end + + context 'removes globus_uuid' do + let(:params) do + { "data" => { "type" => "providers", + "attributes" => { + "globusUuid" => nil } } } + end + + it 'updates the record' do + put "/providers/#{provider.symbol}", params, headers + + expect(last_response.status).to eq(200) + expect(json.dig('data', 'attributes', 'displayName')).to eq("My provider") + expect(json.dig('data', 'attributes', 'globusUuid')).to be_nil + end + end + + context 'invalid globus_uuid' do + let(:params) do + { "data" => { "type" => "providers", + "attributes" => { + "globusUuid" => "abc" } } } + end + + it 'updates the record' do + put "/providers/#{provider.symbol}", params, headers + + expect(last_response.status).to eq(422) + expect(json["errors"].first).to eq("source"=>"globus_uuid", "title"=>"Abc is not a valid UUID") end end diff --git a/spec/requests/repositories_spec.rb b/spec/requests/repositories_spec.rb index 0f02ee2e8..e59e645cf 100644 --- a/spec/requests/repositories_spec.rb +++ b/spec/requests/repositories_spec.rb @@ -81,6 +81,7 @@ expect(last_response.status).to eq(200) expect(json.dig('data', 'attributes', 'name')).to eq(client.name) + expect(json.dig('data', 'attributes', 'globusUuid')).to eq("bc7d0274-3472-4a79-b631-e4c7baccc667") end end @@ -143,7 +144,8 @@ { "data" => { "type" => "repositories", "attributes" => { "name" => "Imperial College 2", - "clientType" => "periodical" }} } + "clientType" => "periodical", + "globusUuid" => "9908a164-1e4f-4c17-ae1b-cc318839d6c8" }} } end it 'updates the record' do @@ -151,11 +153,43 @@ expect(last_response.status).to eq(200) expect(json.dig('data', 'attributes', 'name')).to eq("Imperial College 2") + expect(json.dig('data', 'attributes', 'globusUuid')).to eq("9908a164-1e4f-4c17-ae1b-cc318839d6c8") expect(json.dig('data', 'attributes', 'name')).not_to eq(client.name) expect(json.dig('data', 'attributes', 'clientType')).to eq("periodical") end end + context 'removes the globus_uuid' do + let(:params) do + { "data" => { "type" => "repositories", + "attributes" => { + "globusUuid" => nil }} } + end + + it 'updates the record' do + put "/repositories/#{client.symbol}", params, headers + + expect(last_response.status).to eq(200) + expect(json.dig('data', 'attributes', 'name')).to eq("My data center") + expect(json.dig('data', 'attributes', 'globusUuid')).to be_nil + end + end + + context 'invalid globus_uuid' do + let(:params) do + { "data" => { "type" => "repositories", + "attributes" => { + "globusUuid" => "abc" }} } + end + + it 'updates the record' do + put "/repositories/#{client.symbol}", params, headers + + expect(last_response.status).to eq(422) + expect(json["errors"].first).to eq("source"=>"globus_uuid", "title"=>"Abc is not a valid UUID") + end + end + context 'using basic auth', vcr: true do let(:params) do { "data" => { "type" => "repositories",