diff --git a/app/graphql/sources/linked_to_editions_source.rb b/app/graphql/sources/linked_to_editions_source.rb index 4c85a18e4..637dc2bd8 100644 --- a/app/graphql/sources/linked_to_editions_source.rb +++ b/app/graphql/sources/linked_to_editions_source.rb @@ -8,11 +8,18 @@ def initialize(parent_object:) # rubocop:enable Lint/MissingSuper def fetch(link_types) - all_links = @object.document.link_set_links + link_set_links = @object.document.link_set_links .includes(target_documents: @content_store) # content_store is :live or :draft (a Document has_one Edition by that name) .where(link_type: link_types) .where(target_documents: { locale: "en" }) + edition_links = @object.links + .includes(target_documents: @content_store) # content_store is :live or :draft (a Document has_one Edition by that name) + .where(link_type: link_types) + .where(target_documents: { locale: "en" }) + + all_links = link_set_links + edition_links + link_types_map = link_types.index_with { [] } all_links.each_with_object(link_types_map) { |link, hash| diff --git a/app/graphql/sources/reverse_linked_to_editions_source.rb b/app/graphql/sources/reverse_linked_to_editions_source.rb new file mode 100644 index 000000000..70ac0d693 --- /dev/null +++ b/app/graphql/sources/reverse_linked_to_editions_source.rb @@ -0,0 +1,34 @@ +module Sources + class ReverseLinkedToEditionsSource < GraphQL::Dataloader::Source + # rubocop:disable Lint/MissingSuper + def initialize(parent_object:) + @object = parent_object + @content_store = parent_object.content_store.to_sym + end + # rubocop:enable Lint/MissingSuper + + def fetch(link_types) + all_links = @object + .document + .reverse_links + .includes(source_documents: @content_store) + .where(link_type: link_types) + + link_types_map = link_types.index_with { [] } + + all_links.each_with_object(link_types_map) { |link, hash| + if link.link_set + hash[link.link_type].concat(editions_for_link_set_link(link)) + elsif link.edition + hash[link.link_type] << link.edition + end + }.values + end + + private + + def editions_for_link_set_link(link) + link.source_documents.map { |document| document.send(@content_store) } + end + end +end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index e6c92d6fb..5b52b28fd 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -14,5 +14,14 @@ def self.links_field(field_name_and_link_type, graphql_field_type) .load(field_name_and_link_type.to_s) end end + + def self.reverse_links_field(field_name_and_link_type, belongs_to, graphql_field_type) + field(field_name_and_link_type.to_sym, graphql_field_type) + + define_method(field_name_and_link_type.to_sym) do + dataloader.with(Sources::ReverseLinkedToEditionsSource, parent_object: object) + .load(belongs_to.to_s) + end + end end end diff --git a/app/graphql/types/edition_type.rb b/app/graphql/types/edition_type.rb index 6687ddef9..2983a17c3 100644 --- a/app/graphql/types/edition_type.rb +++ b/app/graphql/types/edition_type.rb @@ -7,14 +7,168 @@ class WithdrawnNotice < Types::BaseObject field :withdrawn_at, GraphQL::Types::ISO8601DateTime end + class EditionLinks < Types::BaseObject + links_field :active_top_level_browse_page, [EditionType] + links_field :associated_taxons, [EditionType] + links_field :available_translations, [EditionType] + links_field :contact, [EditionType] + links_field :contacts, [EditionType] + links_field :content_owners, [EditionType] + links_field :corporate_information_pages, [EditionType] + links_field :current_prime_minister, [EditionType] + links_field :documents, [EditionType] + links_field :email_alert_signup, [EditionType] + links_field :embed, [EditionType] + links_field :fatality_notices, [EditionType] + links_field :featured_policies, [EditionType] + links_field :field_of_operation, [EditionType] + links_field :fields_of_operation, [EditionType] + links_field :finder, [EditionType] + links_field :government, [EditionType] + links_field :historical_accounts, [EditionType] + links_field :home_page_offices, [EditionType] + links_field :lead_organisations, [EditionType] + links_field :linked_items, [EditionType] + links_field :main_office, [EditionType] + links_field :mainstream_browse_pages, [EditionType] + links_field :manual, [EditionType] + links_field :meets_user_needs, [EditionType] + links_field :ministerial, [EditionType] + links_field :office_staff, [EditionType] + links_field :ordered_board_members, [EditionType] + links_field :ordered_chief_professional_officers, [EditionType] + links_field :ordered_child_organisations, [EditionType] + links_field :ordered_contacts, [EditionType] + links_field :ordered_featured_policies, [EditionType] + links_field :ordered_foi_contacts, [EditionType] + links_field :ordered_high_profile_groups, [EditionType] + links_field :ordered_military_personnel, [EditionType] + links_field :ordered_ministers, [EditionType] + links_field :ordered_parent_organisations, [EditionType] + links_field :ordered_related_items_overrides, [EditionType] + links_field :ordered_related_items, [EditionType] + links_field :ordered_roles, [EditionType] + links_field :ordered_special_representatives, [EditionType] + links_field :ordered_successor_organisations, [EditionType] + links_field :ordered_traffic_commissioners, [EditionType] + links_field :organisations, [EditionType] + links_field :original_primary_publishing_organisation, [EditionType] + links_field :pages_part_of_step_nav, [EditionType] + links_field :pages_related_to_step_nav, [EditionType] + links_field :pages_secondary_to_step_nav, [EditionType] + links_field :parent_taxons, [EditionType] + links_field :parent, [EditionType] + links_field :people, [EditionType] + links_field :person, [EditionType] + links_field :policy_areas, [EditionType] + links_field :popular_links, [EditionType] + links_field :primary_publishing_organisation, [EditionType] + links_field :primary_role_person, [EditionType] + links_field :related_guides, [EditionType] + links_field :related_mainstream_content, [EditionType] + links_field :related_policies, [EditionType] + links_field :related_statistical_data_sets, [EditionType] + links_field :related_topics, [EditionType] + links_field :related, [EditionType] + links_field :role, [EditionType] + links_field :roles, [EditionType] + links_field :root_taxon, [EditionType] + links_field :second_level_browse_pages, [EditionType] + links_field :secondary_role_person, [EditionType] + links_field :sections, [EditionType] + links_field :service_manual_topics, [EditionType] + links_field :speaker, [EditionType] + links_field :sponsoring_organisations, [EditionType] + links_field :suggested_ordered_related_items, [EditionType] + links_field :take_part_pages, [EditionType] + links_field :taxonomy_topic_email_override, [EditionType] + links_field :taxons, [EditionType] + links_field :top_level_browse_pages, [EditionType] + links_field :topical_events, [EditionType] + links_field :world_locations, [EditionType] + links_field :worldwide_organisation, [EditionType] + links_field :worldwide_organisations, [EditionType] + + reverse_links_field :child_taxons, :parent_taxons, [EditionType] + reverse_links_field :children, :parent, [EditionType] + reverse_links_field :document_collections, :documents, [EditionType] + reverse_links_field :level_one_taxons, :root_taxon, [EditionType] + reverse_links_field :ministers, :ministerial, [EditionType] + reverse_links_field :part_of_step_navs, :pages_part_of_step_nav, [EditionType] + reverse_links_field :policies, :working_groups, [EditionType] + reverse_links_field :related_to_step_navs, :pages_related_to_step_nav, [EditionType] + reverse_links_field :secondary_to_step_navs, :secondary_to_step_navs, [EditionType] + + field :role_appointments, [EditionType] + + def role_appointments + if object.document_type == "role" || object.document_type == "ministerial_role" + dataloader.with(Sources::ReverseLinkedToEditionsSource, parent_object: object) + .load("role") + else + dataloader.with(Sources::ReverseLinkedToEditionsSource, parent_object: object) + .load("person") + end + end + + def available_translations + Presenters::Queries::AvailableTranslations.by_edition(object) + .translations.fetch(:available_translations, []) + end + end + + class Details < Types::BaseObject + class Image < Types::BaseObject + field :url, String + field :alt_text, String + end + + class Logo < Types::BaseObject + field :crest, String + field :formatted_title, String + end + + class WhipOrganisation < Types::BaseObject + field :label, String + field :sort_order, Integer + end + + field :body, String + field :brand, String + field :change_history, GraphQL::Types::JSON + field :current, Boolean + field :default_news_image, Image + field :display_date, GraphQL::Types::ISO8601DateTime + field :emphasised_organisations, GraphQL::Types::JSON + field :ended_on, GraphQL::Types::ISO8601DateTime + field :first_public_at, GraphQL::Types::ISO8601DateTime + field :image, Image + field :international_delegations, [EditionType], null: false + field :logo, Logo + field :political, Boolean + field :privy_counsellor, Boolean + field :role_payment_type, String + field :seniority, Integer + field :started_on, GraphQL::Types::ISO8601DateTime + field :supports_historical_accounts, Boolean + field :whip_organisation, WhipOrganisation + field :world_locations, [EditionType], null: false + end + + field :active, Boolean, null: false field :analytics_identifier, String field :base_path, String field :content_id, ID + field :current, Boolean field :description, String - field :details, GraphQL::Types::JSON, null: false + field :details, Details field :document_type, String + field :ended_on, GraphQL::Types::ISO8601DateTime field :first_published_at, GraphQL::Types::ISO8601DateTime, null: false + field :iso2, String + field :links, EditionLinks, method: :itself field :locale, String, null: false + field :name, String, null: false field :phase, String, null: false field :public_updated_at, GraphQL::Types::ISO8601DateTime, null: false field :publishing_app, String @@ -23,11 +177,25 @@ class WithdrawnNotice < Types::BaseObject field :rendering_app, String field :scheduled_publishing_delay_seconds, Int field :schema_name, String + field :slug, String, null: false + field :started_on, GraphQL::Types::ISO8601DateTime field :state, String + field :supports_historical_accounts, Boolean field :title, String, null: false field :updated_at, GraphQL::Types::ISO8601DateTime + field :web_url, String field :withdrawn_notice, WithdrawnNotice + def details + Presenters::ContentTypeResolver.new("text/html").resolve( + Presenters::DetailsPresenter.new( + object.details, + Presenters::ChangeHistoryPresenter.new(object), + Presenters::ContentEmbedPresenter.new(object), + ).details, + ) + end + def withdrawn_notice return nil unless object.unpublishing&.withdrawal? @@ -43,79 +211,6 @@ def not_stored_in_publishing_api alias_method :publishing_scheduled_at, :not_stored_in_publishing_api alias_method :scheduled_publishing_delay_seconds, :not_stored_in_publishing_api - class Translation < Types::BaseObject - field :locale, String - field :base_path, String - end - - class GovernmentDetails < Types::BaseObject - field :current, Boolean - end - - class GovernmentLink < Types::BaseObject - field :base_path, String - field :content_id, String - field :details, GovernmentDetails - field :title, String - end - - class OrganisationLink < Types::BaseObject - field :base_path, String - field :content_id, String - field :title, String - end - - class PersonLink < Types::BaseObject - field :base_path, String - field :content_id, String - field :title, String - end - - class Taxon < Types::BaseObject - field :base_path, String - field :content_id, String - field :document_type, String - field :phase, String - field :title, String - end - - class TaxonLink < Taxon - class TaxonLinks < Types::BaseObject - links_field :parent_taxons, [Taxon] - end - - field :links, TaxonLinks, method: :itself - end - - class TopicalEventLink < Types::BaseObject - field :base_path, String - field :content_id, String - field :title, String - end - - class WorldLocationLink < Types::BaseObject - field :base_path, String - field :content_id, String - field :title, String - end - - class EditionLinks < Types::BaseObject - field :available_translations, [Translation] - links_field :government, [GovernmentLink] - links_field :organisations, [OrganisationLink] - links_field :people, [PersonLink] - links_field :taxons, [TaxonLink] - links_field :topical_events, [TopicalEventLink] - links_field :world_locations, [WorldLocationLink] - - def available_translations - Presenters::Queries::AvailableTranslations.by_edition(object) - .translations.fetch(:available_translations, []) - end - end - - field :links, EditionLinks, method: :itself - private def presented_edition diff --git a/app/graphql/types/news_article_type.rb b/app/graphql/types/news_article_type.rb deleted file mode 100644 index dbc2e6bf0..000000000 --- a/app/graphql/types/news_article_type.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module Types - class NewsArticleType < Types::EditionType - def self.document_types = %w[ - government_response - news_story - press_release - world_news_story - ] - end -end diff --git a/app/graphql/types/query_type/edition_type_or_subtype.rb b/app/graphql/types/query_type/edition_type_or_subtype.rb index 787f689e1..4a62ab6f1 100644 --- a/app/graphql/types/query_type/edition_type_or_subtype.rb +++ b/app/graphql/types/query_type/edition_type_or_subtype.rb @@ -6,9 +6,6 @@ class EditionTypeOrSubtype < Types::BaseUnion EDITION_TYPES = [ Types::EditionType, Types::MinistersIndexType, - Types::NewsArticleType, - Types::RoleType, - Types::WorldIndexType, ].freeze possible_types(*EDITION_TYPES) @@ -18,6 +15,8 @@ def resolve_type(object, _context) document_type = object.document_type matching_edition_subtype = Types::EditionType.descendants.find do |edition_subtype| + next unless edition_subtype.respond_to?(:document_types) + edition_subtype.document_types.include?(document_type) end diff --git a/app/graphql/types/role_type.rb b/app/graphql/types/role_type.rb deleted file mode 100644 index b364953e1..000000000 --- a/app/graphql/types/role_type.rb +++ /dev/null @@ -1,102 +0,0 @@ -# frozen_string_literal: true - -module Types - class RoleType < Types::EditionType - def self.document_types = %w[ministerial_role] - - class Organisation < Types::BaseObject - field :base_path, String - field :title, String, null: false - end - - class RoleAppointment < Types::BaseObject - class Person < Types::BaseObject - class PersonDetails < Types::BaseObject - field :body, String - - def body - govspeak = object.fetch(:body, []) - .filter { _1[:content_type] == "text/govspeak" } - .map { _1[:content] } - .first - - Govspeak::Document.new(govspeak).to_html if govspeak.present? - end - end - - field :base_path, String - field :details, PersonDetails - field :title, String, null: false - end - - class RoleAppointmentDetails < Types::BaseObject - field :current, Boolean - field :ended_on, GraphQL::Types::ISO8601DateTime - field :started_on, GraphQL::Types::ISO8601DateTime - end - - field :details, RoleAppointmentDetails - - class RoleAppointmentLinks < Types::BaseObject - field :person, [Person] - - def person - Edition - .live - .joins(document: { reverse_links: :link_set }) - .where( - document: { locale: "en" }, - link_set: { content_id: object.content_id }, - reverse_links: { link_type: "person" }, - ) - .limit(1) - end - end - - field :links, RoleAppointmentLinks, method: :itself - end - - class RoleDetails < Types::BaseObject - field :body, String - field :supports_historical_accounts, Boolean - - def body - govspeak = object.fetch(:body, []) - .filter { _1[:content_type] == "text/govspeak" } - .map { _1[:content] } - .first - - Govspeak::Document.new(govspeak).to_html if govspeak.present? - end - end - - class RoleLinks < EditionType::EditionLinks - field :ordered_parent_organisations, [Organisation] - field :role_appointments, [RoleAppointment] - - def ordered_parent_organisations - Edition - .live - .joins(document: { reverse_links: :link_set }) - .where( - document: { locale: "en" }, - link_set: { content_id: object.content_id }, - reverse_links: { link_type: "ordered_parent_organisations" }, - ) - end - - def role_appointments - Edition - .live - .joins(document: :link_set_links) - .where( - document: { locale: "en" }, - link_set_links: { target_content_id: object.content_id, link_type: "role" }, - ) - end - end - - field :details, RoleDetails - field :links, RoleLinks, method: :itself - end -end diff --git a/app/graphql/types/world_index_type.rb b/app/graphql/types/world_index_type.rb deleted file mode 100644 index 5e48ab573..000000000 --- a/app/graphql/types/world_index_type.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module Types - class WorldIndexType < Types::EditionType - def self.document_types = %w[world_index] - - class WorldLocation < Types::BaseObject - field :active, Boolean, null: false - field :analytics_identifier, String - field :content_id, ID, null: false - field :iso2, String - field :name, String, null: false - field :slug, String, null: false - field :updated_at, GraphQL::Types::ISO8601DateTime, null: false - end - - class WorldIndexDetails < BaseObject - field :body, String - field :international_delegations, [WorldLocation], null: false - field :world_locations, [WorldLocation], null: false - end - - field :details, WorldIndexDetails - end -end diff --git a/app/presenters/content_type_resolver.rb b/app/presenters/content_type_resolver.rb new file mode 100644 index 000000000..9db5081d4 --- /dev/null +++ b/app/presenters/content_type_resolver.rb @@ -0,0 +1,43 @@ +class Presenters::ContentTypeResolver + def initialize(content_type) + self.content_type = content_type + end + + def resolve(object) + case object + when Hash + resolve_hash(object) + when Array + resolve_array(object) + else + object + end + end + +private + + def resolve_hash(hash) + hash.inject({}) do |memo, (key, value)| + memo.merge(key => resolve(value)) + end + end + + def resolve_array(array) + if array.all? { |v| v.is_a?(Hash) } + array = array.map(&:symbolize_keys) + content_for_type = extract_content(array) + end + + if content_for_type + content_for_type[:content] + else + array.map { |e| resolve(e) } + end + end + + def extract_content(array) + array.detect { |h| h[:content_type] == content_type && h[:content] } + end + + attr_accessor :content_type +end diff --git a/spec/graphql/sources/linked_to_editions_source_spec.rb b/spec/graphql/sources/linked_to_editions_source_spec.rb index debc94851..4fa110869 100644 --- a/spec/graphql/sources/linked_to_editions_source_spec.rb +++ b/spec/graphql/sources/linked_to_editions_source_spec.rb @@ -1,18 +1,36 @@ RSpec.describe Sources::LinkedToEditionsSource do it "returns the specified link set links" do - source_document = create(:edition) - target_document_1 = create(:edition) - target_document_2 = create(:edition) - target_document_3 = create(:edition) - link_set = create(:link_set, content_id: source_document.content_id) - create(:link, link_set: link_set, target_content_id: target_document_1.content_id, link_type: "test_link") - create(:link, link_set: link_set, target_content_id: target_document_2.content_id, link_type: "another_link_type") - create(:link, link_set: link_set, target_content_id: target_document_3.content_id, link_type: "test_link") + source_edition = create(:edition) + target_edition_1 = create(:edition) + target_edition_2 = create(:edition) + target_edition_3 = create(:edition) + link_set = create(:link_set, content_id: source_edition.content_id) + create(:link, link_set: link_set, target_content_id: target_edition_1.content_id, link_type: "test_link") + create(:link, link_set: link_set, target_content_id: target_edition_2.content_id, link_type: "another_link_type") + create(:link, link_set: link_set, target_content_id: target_edition_3.content_id, link_type: "test_link") GraphQL::Dataloader.with_dataloading do |dataloader| - request = dataloader.with(described_class, parent_object: source_document).request("test_link") + request = dataloader.with(described_class, parent_object: source_edition).request("test_link") - expect(request.load).to eq([target_document_1, target_document_3]) + expect(request.load).to eq([target_edition_1, target_edition_3]) + end + end + + it "returns the specified edition links" do + target_edition_1 = create(:edition) + target_edition_2 = create(:edition) + target_edition_3 = create(:edition) + + source_edition = create(:edition, + links_hash: { + "test_link" => [target_edition_1.content_id, target_edition_3.content_id], + "another_link_type" => [target_edition_2.content_id], + }) + + GraphQL::Dataloader.with_dataloading do |dataloader| + request = dataloader.with(described_class, parent_object: source_edition).request("test_link") + + expect(request.load).to eq([target_edition_1, target_edition_3]) end end end diff --git a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb new file mode 100644 index 000000000..941ed0d8d --- /dev/null +++ b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb @@ -0,0 +1,45 @@ +RSpec.describe Sources::ReverseLinkedToEditionsSource do + it "returns the specified reverse link set links" do + target_edition = create(:edition) + source_edition_1 = create(:edition) + source_edition_2 = create(:edition) + source_edition_3 = create(:edition) + link_set_1 = create(:link_set, content_id: source_edition_1.content_id) + link_set_2 = create(:link_set, content_id: source_edition_2.content_id) + link_set_3 = create(:link_set, content_id: source_edition_3.content_id) + create(:link, link_set: link_set_1, target_content_id: target_edition.content_id, link_type: "test_link") + create(:link, link_set: link_set_2, target_content_id: target_edition.content_id, link_type: "another_link_type") + create(:link, link_set: link_set_3, target_content_id: target_edition.content_id, link_type: "test_link") + + GraphQL::Dataloader.with_dataloading do |dataloader| + request = dataloader.with(described_class, parent_object: target_edition).request("test_link") + + expect(request.load).to eq([source_edition_1, source_edition_3]) + end + end + + it "returns the specified reverse edition links" do + target_edition = create(:edition) + + source_edition_1 = create(:edition, + links_hash: { + "test_link" => [target_edition.content_id], + }) + + source_edition_2 = create(:edition, + links_hash: { + "test_link" => [target_edition.content_id], + }) + + create(:edition, + links_hash: { + "another_link_type" => [target_edition.content_id], + }) + + GraphQL::Dataloader.with_dataloading do |dataloader| + request = dataloader.with(described_class, parent_object: target_edition).request("test_link") + + expect(request.load).to eq([source_edition_1, source_edition_2]) + end + end +end diff --git a/spec/graphql/types/edition_type_spec.rb b/spec/graphql/types/edition_type_spec.rb index 490e72b7e..ee02011dc 100644 --- a/spec/graphql/types/edition_type_spec.rb +++ b/spec/graphql/types/edition_type_spec.rb @@ -30,4 +30,54 @@ end end end + + context "content types in the details" do + context "when the body is a string" do + it "returns the string" do + edition = create(:edition, details: { body: "some text" }) + + expect( + run_graphql_field( + "Edition.details", + edition, + )[:body], + ).to eq("some text") + end + end + + context "when there are multiple content types and one is html" do + it "returns the html" do + edition = create(:edition, details: { + body: [ + { content_type: "text/govspeak", content: "some text" }, + { content_type: "text/html", content: "

some other text

" }, + ], + }) + + expect( + run_graphql_field( + "Edition.details", + edition, + )[:body], + ).to eq("

some other text

") + end + end + + context "when there are multiple content types and none are html" do + it "converts the govspeak to html" do + edition = create(:edition, details: { + body: [ + { content_type: "text/govspeak", content: "some text" }, + ], + }) + + expect( + run_graphql_field( + "Edition.details", + edition, + )[:body], + ).to eq("

some text

\n") + end + end + end end diff --git a/spec/graphql/types/news_article_type_spec.rb b/spec/graphql/types/news_article_type_spec.rb deleted file mode 100644 index a4d586e04..000000000 --- a/spec/graphql/types/news_article_type_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -RSpec.describe Types::NewsArticleType do - describe ".document_types" do - it "defines the document types for .resolve_type to distinguish the type from the generic Edition type" do - expect(described_class.document_types).to eq(%w[ - government_response - news_story - press_release - world_news_story - ]) - end - end -end diff --git a/spec/graphql/types/query_type/edition_type_or_subtype_spec.rb b/spec/graphql/types/query_type/edition_type_or_subtype_spec.rb index afc67712d..48087b19e 100644 --- a/spec/graphql/types/query_type/edition_type_or_subtype_spec.rb +++ b/spec/graphql/types/query_type/edition_type_or_subtype_spec.rb @@ -1,13 +1,14 @@ RSpec.describe "Types::QueryType::EditionTypeOrSubtype" do describe "EDITION_TYPES" do - it "includes all EditionType descendants" do + it "includes all EditionType descendants that have a value for `document_types`" do Rails.application.eager_load! - expected = [Types::EditionType, *Types::EditionType.descendants].map(&:name) - actual = Types::QueryType::EditionTypeOrSubtype::EDITION_TYPES.map(&:name) - diff = expected - actual + expected_subtypes = [*Types::EditionType.descendants] + .select { |type| type.respond_to?(:document_types) } + expected = [Types::EditionType] + expected_subtypes + actual = Types::QueryType::EditionTypeOrSubtype::EDITION_TYPES - expect(diff).to be_empty + expect(actual).to eq(expected) end end @@ -16,10 +17,10 @@ it "returns the Edition subtype" do expect( Types::QueryType::EditionTypeOrSubtype.resolve_type( - build(:live_edition, document_type: "world_index"), + build(:live_edition, document_type: "ministers_index"), {}, ), - ).to be Types::WorldIndexType + ).to be Types::MinistersIndexType end end diff --git a/spec/graphql/types/world_index_type_spec.rb b/spec/graphql/types/world_index_type_spec.rb deleted file mode 100644 index f20f9c06a..000000000 --- a/spec/graphql/types/world_index_type_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -RSpec.describe "Types::WorldIndexType" do - describe ".document_types" do - it "defines the document types for .resolve_type to distinguish the type from the generic Edition type" do - expect(Types::WorldIndexType.document_types).to eq(%w[world_index]) - end - end -end diff --git a/spec/integration/graphql/generic_edition_spec.rb b/spec/integration/graphql/generic_edition_spec.rb index 526f8c43c..39cb492f7 100644 --- a/spec/integration/graphql/generic_edition_spec.rb +++ b/spec/integration/graphql/generic_edition_spec.rb @@ -16,7 +16,9 @@ base_path content_id description - details + details { + body + } document_type first_published_at locale @@ -45,7 +47,9 @@ "base_path": @edition.base_path, "content_id": @edition.content_id, "description": @edition.description, - "details": @edition.details, + "details": { + "body": @edition.details[:body], + }, "document_type": @edition.document_type, "first_published_at": @edition.first_published_at.iso8601, "locale": @edition.locale, @@ -111,32 +115,5 @@ expect(parsed_response).to eq(expected) end end - - it "does not expose non-generic edition fields" do - post "/graphql", params: { - query: - "{ - edition(base_path: \"/my/generic/edition\") { - ... on Edition { - world_locations { - active - } - } - } - }", - } - - parsed_response = JSON.parse(response.body).deep_symbolize_keys - - expect(parsed_response).to match( - { - errors: [ - hash_including( - message: "Field 'world_locations' doesn't exist on type 'Edition'", - ), - ], - }, - ) - end end end diff --git a/spec/integration/graphql/news_article_spec.rb b/spec/integration/graphql/news_article_spec.rb index 91dd259ee..004539840 100644 --- a/spec/integration/graphql/news_article_spec.rb +++ b/spec/integration/graphql/news_article_spec.rb @@ -14,6 +14,9 @@ @government = create( :live_edition, document_type: "government", + details: { + "current" => true, + }, ) @organisation = create( @@ -86,10 +89,12 @@ base_path: "/government/news/announcement", content_store: "live", ) { - ... on NewsArticle { + ... on Edition { base_path title - details + details { + body + } links { available_translations { base_path @@ -153,7 +158,7 @@ government: [ { details: { - current: @government.details["current"], + current: @government.details[:current], }, title: @government.title, }, diff --git a/spec/integration/graphql/roles_spec.rb b/spec/integration/graphql/roles_spec.rb index 94f9092e6..29fee4709 100644 --- a/spec/integration/graphql/roles_spec.rb +++ b/spec/integration/graphql/roles_spec.rb @@ -114,7 +114,7 @@ query: "{ edition(base_path: \"/government/ministers/prime-minister\") { - ... on Role { + ... on Edition { base_path locale title diff --git a/spec/integration/graphql/world_index_spec.rb b/spec/integration/graphql/world_index_spec.rb index 104834cdc..e5bb133d8 100644 --- a/spec/integration/graphql/world_index_spec.rb +++ b/spec/integration/graphql/world_index_spec.rb @@ -34,10 +34,10 @@ ) end - it "exposes world index specific fields in addition to generic edition fields" do + it "has world index specific fields in the generic edition type" do post "/graphql", params: { query: - "fragment worldLocationInfo on WorldLocation { + "fragment worldLocationInfo on Edition { active analytics_identifier content_id @@ -48,7 +48,7 @@ { edition(base_path: \"/world\") { - ... on WorldIndex { + ... on Edition { title details { diff --git a/spec/presenters/content_type_resolver_spec.rb b/spec/presenters/content_type_resolver_spec.rb new file mode 100644 index 000000000..3b6913a73 --- /dev/null +++ b/spec/presenters/content_type_resolver_spec.rb @@ -0,0 +1,130 @@ +RSpec.describe Presenters::ContentTypeResolver do + subject { described_class.new("html") } + + it "inlines content of the specified content type" do + result = subject.resolve( + body: [ + { content_type: "html", content: "

body

" }, + { content_type: "text", content: "body" }, + ], + ) + + expect(result).to eq( + body: "

body

", + ) + end + + it "works for string keys as well as symbols" do + result = subject.resolve( + "body" => [ + { "content_type" => "html", "content" => "

body

" }, + { "content_type" => "text", "content" => "body" }, + ], + ) + + expect(result).to eq( + "body" => "

body

", + ) + end + + it "does not affect other fields" do + result = subject.resolve( + string: "string", + array: [], + number: 123, + ) + + expect(result).to eq( + string: "string", + array: [], + number: 123, + ) + end + + it "handles hashes with content types but no content field" do + result = subject.resolve([ + content_type: "application/pdf", + path: "some/document.pdf", + ]) + + expect(result).to eq([ + content_type: "application/pdf", + path: "some/document.pdf", + ]) + end + + it "recurses on nested hashes" do + result = subject.resolve( + details: { + foo: { + bar: { + content: [ + { content_type: "html", content: "

body

" }, + ], + }, + }, + }, + ) + + expect(result).to eq( + details: { + foo: { + bar: { + content: "

body

", + }, + }, + }, + ) + end + + it "recurses on nested arrays" do + result = subject.resolve( + paragraphs: [ + [ + [ + { + body: [ + { content_type: "html", content: "

body

" }, + ], + }, + ], + ], + ], + ) + + expect(result).to eq( + paragraphs: [ + [ + [ + { + body: "

body

", + }, + ], + ], + ], + ) + end + + it "doesn't resolve incomplete multi-type content" do + result = subject.resolve( + details: { + body: { + content: [ + { content: "

body

" }, + { content_type: "html" }, + ], + }, + }, + ) + expect(result).to eq( + details: { + body: { + content: [ + { content: "

body

" }, + { content_type: "html" }, + ], + }, + }, + ) + end +end