From 6964076cd18e0b84b6d473f33bdaaf90d3161d1d Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Thu, 26 Feb 2026 17:42:49 +0000 Subject: [PATCH 01/20] Default to live edition in dataloader specs When introducing support for drafts, using the default draft edition factory will become an issue --- .../sources/linked_to_editions_source_spec.rb | 54 ++++++++-------- .../reverse_linked_to_editions_source_spec.rb | 64 +++++++++---------- 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/spec/graphql/sources/linked_to_editions_source_spec.rb b/spec/graphql/sources/linked_to_editions_source_spec.rb index ce0bac4c23..278a2a3ff6 100644 --- a/spec/graphql/sources/linked_to_editions_source_spec.rb +++ b/spec/graphql/sources/linked_to_editions_source_spec.rb @@ -39,10 +39,10 @@ def check_links! context "when the same source content has a mix of link set links and edition links for the same link type" do it "returns only the edition links" do - target_edition_1 = create(:edition, title: "edition 1, test link, edition link") - target_edition_2 = create(:edition, title: "edition 2, test link, link set link") + target_edition_1 = create(:live_edition, title: "edition 1, test link, edition link") + target_edition_2 = create(:live_edition, title: "edition 2, test link, link set link") - source_edition = create(:edition, + source_edition = create(:live_edition, edition_links: [ { link_type: "test_link", target_content_id: target_edition_1.content_id }, ], @@ -58,11 +58,11 @@ def check_links! %i[link_set_links edition_links].each do |links_kind| context "when the link kind is #{links_kind}" do it "returns the specified links" do - target_edition_1 = create(:edition, title: "edition 1, test link") - target_edition_2 = create(:edition, title: "edition 2, another link type") - target_edition_3 = create(:edition, title: "edition 3, test link") + target_edition_1 = create(:live_edition, title: "edition 1, test link") + target_edition_2 = create(:live_edition, title: "edition 2, another link type") + target_edition_3 = create(:live_edition, title: "edition 3, test link") - source_edition = create(:edition, + source_edition = create(:live_edition, links_kind => [ { link_type: "test_link", target_content_id: target_edition_1.content_id }, { link_type: "another_link_type", target_content_id: target_edition_2.content_id }, @@ -88,11 +88,11 @@ def check_links! end it "returns editions in order of their associated link's `position`" do - target_edition_0 = create(:edition, title: "edition 0, position 1") - target_edition_1 = create(:edition, title: "edition 1, position 2") - target_edition_2 = create(:edition, title: "edition 2, position 0") + target_edition_0 = create(:live_edition, title: "edition 0, position 1") + target_edition_1 = create(:live_edition, title: "edition 1, position 2") + target_edition_2 = create(:live_edition, title: "edition 2, position 0") - source_edition = create(:edition, + source_edition = create(:live_edition, links_kind => [ { link_type: "test_link", target_content_id: target_edition_0.content_id, position: 1 }, { link_type: "test_link", target_content_id: target_edition_1.content_id, position: 2 }, @@ -105,12 +105,12 @@ def check_links! context "when links have the same `position`" do it "returns editions reverse-ordered by their associated links' `id`" do - target_edition_0 = create(:edition, title: "edition 0, third link id") - target_edition_1 = create(:edition, title: "edition 1, first link id") - target_edition_2 = create(:edition, title: "edition 2, second link id") - target_edition_3 = create(:edition, title: "edition 3, fourth link id") + target_edition_0 = create(:live_edition, title: "edition 0, third link id") + target_edition_1 = create(:live_edition, title: "edition 1, first link id") + target_edition_2 = create(:live_edition, title: "edition 2, second link id") + target_edition_3 = create(:live_edition, title: "edition 3, fourth link id") - source_edition = create(:edition, + source_edition = create(:live_edition, links_kind => [ { link_type: "test_link", target_content_id: target_edition_1.content_id, position: 0 }, { link_type: "test_link", target_content_id: target_edition_2.content_id, position: 0 }, @@ -192,11 +192,11 @@ def check_links! describe "links between documents with different locales" do it "includes links matching the specified locale (french)" do target_content_id = SecureRandom.uuid - create(:edition, document: create(:document, locale: "en", content_id: target_content_id), title: "english") - french_edition = create(:edition, document: create(:document, locale: "fr", content_id: target_content_id), title: "french") + create(:live_edition, document: create(:document, locale: "en", content_id: target_content_id), title: "english") + french_edition = create(:live_edition, document: create(:document, locale: "fr", content_id: target_content_id), title: "french") source_edition = create( - :edition, + :live_edition, document: create(:document, locale: "fr"), links_kind => [ { link_type: "test_link", target_content_id: }, @@ -209,11 +209,11 @@ def check_links! it "includes English language links if there's no better match available" do target_content_id = SecureRandom.uuid - english_edition = create(:edition, document: create(:document, locale: "en", content_id: target_content_id), title: "english edition") - create(:edition, document: create(:document, locale: "fr", content_id: target_content_id), title: "french edition") + english_edition = create(:live_edition, document: create(:document, locale: "en", content_id: target_content_id), title: "english edition") + create(:live_edition, document: create(:document, locale: "fr", content_id: target_content_id), title: "french edition") source_edition = create( - :edition, + :live_edition, document: create(:document, locale: "de"), links_kind => [ { link_type: "test_link", target_content_id: }, @@ -226,11 +226,11 @@ def check_links! it "doesn't include a link if none match the locale or English" do target_content_id = SecureRandom.uuid - create(:edition, document: create(:document, locale: "de", content_id: target_content_id), title: "german") - create(:edition, document: create(:document, locale: "fr", content_id: target_content_id), title: "french") + create(:live_edition, document: create(:document, locale: "de", content_id: target_content_id), title: "german") + create(:live_edition, document: create(:document, locale: "fr", content_id: target_content_id), title: "french") source_edition = create( - :edition, + :live_edition, document: create(:document, locale: "hu"), links_kind => [ { link_type: "test_link", target_content_id: }, @@ -364,11 +364,11 @@ def check_links! end it "doesn't include linked editions of non-renderable document types" do - renderable_edition = create(:edition, title: "renderable edition") + renderable_edition = create(:live_edition, title: "renderable edition") non_renderable_edition = create(:redirect_edition, title: "non-renderable edition (redirect)") source_edition = create( - :edition, + :live_edition, links_kind => [ { link_type: "test_link", target_content_id: renderable_edition.content_id }, { link_type: "test_link", target_content_id: non_renderable_edition.content_id }, diff --git a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb index 94d52c36cb..3118fc0a43 100644 --- a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb +++ b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb @@ -39,9 +39,9 @@ def check_links! context "when the same target content has a mix of link set links and edition links for the same link type" do it "returns all valid source editions" do - target_edition = create(:edition) + target_edition = create(:live_edition) - source_edition_1 = create(:edition, + source_edition_1 = create(:live_edition, title: "edition 1, edition link", edition_links: [ { @@ -49,7 +49,7 @@ def check_links! target_content_id: target_edition.content_id, }, ]) - source_edition_2 = create(:edition, + source_edition_2 = create(:live_edition, title: "edition 2, link set link", link_set_links: [ { @@ -65,9 +65,9 @@ def check_links! context "when the same document is both a link set link and an edition link" do it "only returns the document once" do - target_edition = create(:edition) + target_edition = create(:live_edition) - source_edition = create(:edition, + source_edition = create(:live_edition, edition_links: [ { link_type: "test_link", target_content_id: target_edition.content_id }, ], @@ -90,21 +90,21 @@ def check_links! %i[link_set_links edition_links].each do |links_kind| context "when the link kind is #{links_kind}" do it "returns the specified reverse links" do - target_edition = create(:edition) + target_edition = create(:live_edition) - source_edition_1 = create(:edition, + source_edition_1 = create(:live_edition, title: "edition 1, test link", links_kind => [ { link_type: "test_link", target_content_id: target_edition.content_id }, ]) - source_edition_2 = create(:edition, + source_edition_2 = create(:live_edition, title: "edition 2, test link", links_kind => [ { link_type: "test_link", target_content_id: target_edition.content_id }, ]) - create(:edition, + create(:live_edition, title: "edition 3, another link type", links_kind => [ { link_type: "another_link_type", target_content_id: target_edition.content_id }, @@ -115,19 +115,19 @@ def check_links! end it "returns editions ordered by their reverse links' `position`" do - target_edition = create(:edition) + target_edition = create(:live_edition) - source_edition_0 = create(:edition, + source_edition_0 = create(:live_edition, title: "edition 0, position 1", links_kind => [ { link_type: "test_link", target_content_id: target_edition.content_id, position: 1 }, ]) - source_edition_1 = create(:edition, + source_edition_1 = create(:live_edition, title: "edition 1, position 2", links_kind => [ { link_type: "test_link", target_content_id: target_edition.content_id, position: 2 }, ]) - source_edition_2 = create(:edition, + source_edition_2 = create(:live_edition, title: "edition 2, position 0", links_kind => [ { link_type: "test_link", target_content_id: target_edition.content_id, position: 0 }, @@ -139,9 +139,9 @@ def check_links! context "when reverse links have the same `position`" do it "returns editions reverse-ordered by their associated reverse links' `id`" do - target_edition = create(:edition) + target_edition = create(:live_edition) - source_edition_1 = create(:edition, + source_edition_1 = create(:live_edition, title: "edition 1, second link id", links_kind => [ { @@ -151,7 +151,7 @@ def check_links! id: 10_002, }, ]) - source_edition_0 = create(:edition, + source_edition_0 = create(:live_edition, title: "edition 0, first link id", links_kind => [ { @@ -161,7 +161,7 @@ def check_links! id: 10_001, }, ]) - source_edition_2 = create(:edition, + source_edition_2 = create(:live_edition, title: "edition 2, third link id", links_kind => [ { @@ -272,10 +272,10 @@ def check_links! end it "doesn't include linked editions of non-renderable document types" do - target_edition = create(:edition) + target_edition = create(:live_edition) renderable_edition = create( - :edition, + :live_edition, title: "renderable edition", links_kind => [ { link_type: "test_link", target_content_id: target_edition.content_id }, @@ -299,11 +299,11 @@ def check_links! %i[link_set_links edition_links].each do |links_kind| context "when the link kind is #{links_kind}" do it "includes reverse links matching the specified locale" do - target_edition = create(:edition, document: create(:document, locale: "fr")) + target_edition = create(:live_edition, document: create(:document, locale: "fr")) source_content_id = SecureRandom.uuid create( - :edition, + :live_edition, title: "english, test link", document: create(:document, locale: "en", content_id: source_content_id), links_kind => [ @@ -311,7 +311,7 @@ def check_links! ], ) french_edition = create( - :edition, + :live_edition, title: "french, test link", document: create(:document, locale: "fr", content_id: source_content_id), links_kind => [ @@ -327,11 +327,11 @@ def check_links! context "when the link kind is link_set_links" do it "includes English language reverse links if there's no better match available" do - target_edition = create(:edition, document: create(:document, locale: "de")) + target_edition = create(:live_edition, document: create(:document, locale: "de")) source_content_id = SecureRandom.uuid english_edition = create( - :edition, + :live_edition, document: create(:document, locale: "en", content_id: source_content_id), title: "english, test link", link_set_links: [ @@ -339,7 +339,7 @@ def check_links! ], ) create( - :edition, + :live_edition, document: create(:document, locale: "fr", content_id: source_content_id), title: "french, test link", link_set_links: [ @@ -352,11 +352,11 @@ def check_links! end it "doesn't include a reverse link if none match the locale or English" do - target_edition = create(:edition, document: create(:document, locale: "hu")) + target_edition = create(:live_edition, document: create(:document, locale: "hu")) source_content_id = SecureRandom.uuid create( - :edition, + :live_edition, document: create(:document, locale: "de", content_id: source_content_id), title: "german", link_set_links: [ @@ -364,7 +364,7 @@ def check_links! ], ) create( - :edition, + :live_edition, document: create(:document, locale: "fr", content_id: source_content_id), title: "french", link_set_links: [ @@ -378,11 +378,11 @@ def check_links! context "when the link kind is edition_links" do it "doesn't include a reverse link if none match the locale" do - target_edition = create(:edition, document: create(:document, locale: "hu")) + target_edition = create(:live_edition, document: create(:document, locale: "hu")) source_content_id = SecureRandom.uuid create( - :edition, + :live_edition, document: create(:document, locale: "en", content_id: source_content_id), title: "english, test link", edition_links: [ @@ -390,7 +390,7 @@ def check_links! ], ) create( - :edition, + :live_edition, document: create(:document, locale: "de", content_id: source_content_id), title: "german", edition_links: [ @@ -398,7 +398,7 @@ def check_links! ], ) create( - :edition, + :live_edition, document: create(:document, locale: "fr", content_id: source_content_id), title: "french", edition_links: [ From b544f9c1662ca3df00517b60657e0022571788e7 Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Thu, 26 Feb 2026 16:51:05 +0000 Subject: [PATCH 02/20] Use custom matcher in reverse dataloader spec This was missed in 7ece3e9baa859a30dc46ac92466a91122554c9f3 --- .../sources/reverse_linked_to_editions_source_spec.rb | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb index 3118fc0a43..8f5cc31647 100644 --- a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb +++ b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb @@ -75,15 +75,8 @@ def check_links! { link_type: "test_link", target_content_id: target_edition.content_id }, ]) - GraphQL::Dataloader.with_dataloading do |dataloader| - request = dataloader.with( - described_class, - content_store: target_edition.content_store, - locale: target_edition.locale, - ).request([target_edition, "test_link"]) - - expect(request.load).to eq([source_edition]) - end + expected_titles = [source_edition.title] + expect(target_edition).to have_links("test_link").with_titles(expected_titles) end end From a7186c1130143ca2743bd1d4dbd5e7931c0add14 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Thu, 29 Jan 2026 12:09:12 +0000 Subject: [PATCH 03/20] Rename have_links matcher have_reverse_links So that it doesn't conflict with the have_links matcher in the other dataloader's spec --- .../reverse_linked_to_editions_source_spec.rb | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb index 8f5cc31647..654f128493 100644 --- a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb +++ b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb @@ -1,5 +1,5 @@ RSpec.describe Sources::ReverseLinkedToEditionsSource do - RSpec::Matchers.define :have_links do |link_type| + RSpec::Matchers.define :have_reverse_links do |link_type| def check_links! expect(@links).not_to be_empty @@ -59,7 +59,7 @@ def check_links! ]) expected_titles = [source_edition_1, source_edition_2].map(&:title) - expect(target_edition).to have_links("test_link").with_titles(expected_titles).in_any_order + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles).in_any_order end end @@ -76,7 +76,7 @@ def check_links! ]) expected_titles = [source_edition.title] - expect(target_edition).to have_links("test_link").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) end end @@ -104,7 +104,7 @@ def check_links! ]) expected_titles = [source_edition_1, source_edition_2].map(&:title) - expect(target_edition).to have_links("test_link").with_titles(expected_titles).in_any_order + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles).in_any_order end it "returns editions ordered by their reverse links' `position`" do @@ -127,7 +127,7 @@ def check_links! ]) expected_titles = [source_edition_2, source_edition_0, source_edition_1].map(&:title) - expect(target_edition).to have_links("test_link").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) end context "when reverse links have the same `position`" do @@ -166,7 +166,7 @@ def check_links! ]) expected_titles = [source_edition_2, source_edition_1, source_edition_0].map(&:title) - expect(target_edition).to have_links("test_link").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) end end @@ -183,7 +183,7 @@ def check_links! }]) expected_titles = [source_edition].map(&:title) - expect(target_edition).to have_links("parent").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("parent").with_titles(expected_titles) end it "does not include unpublished reverse links when the unpublishing type is not withdrawal" do @@ -214,7 +214,7 @@ def check_links! target_content_id: target_edition.content_id, }]) - expect(target_edition).not_to have_links("parent") + expect(target_edition).not_to have_reverse_links("parent") end end @@ -228,7 +228,7 @@ def check_links! { link_type: "test_link", target_content_id: target_edition.content_id }, ]) - expect(target_edition).not_to have_links("test_link") + expect(target_edition).not_to have_reverse_links("test_link") end it "also does not include unpublished reverse links when the unpublishing type is not withdrawal" do @@ -259,7 +259,7 @@ def check_links! target_content_id: target_edition.content_id, }]) - expect(target_edition).not_to have_links("test_link") + expect(target_edition).not_to have_reverse_links("test_link") end end end @@ -283,7 +283,7 @@ def check_links! ) expected_titles = [renderable_edition].map(&:title) - expect(target_edition).to have_links("test_link").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) end end end @@ -313,7 +313,7 @@ def check_links! ) expected_titles = [french_edition].map(&:title) - expect(target_edition).to have_links("test_link").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) end end end @@ -341,7 +341,7 @@ def check_links! ) expected_titles = [english_edition].map(&:title) - expect(target_edition).to have_links("test_link").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) end it "doesn't include a reverse link if none match the locale or English" do @@ -365,7 +365,7 @@ def check_links! ], ) - expect(target_edition).not_to have_links("test_link") + expect(target_edition).not_to have_reverse_links("test_link") end end @@ -399,7 +399,7 @@ def check_links! ], ) - expect(target_edition).not_to have_links("test_link") + expect(target_edition).not_to have_reverse_links("test_link") end end @@ -427,7 +427,7 @@ def check_links! ) expected_titles = [english_edition].map(&:title) - expect(target_edition).to have_links("test_link").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) end it "doesn't include any reverse link if none are live" do @@ -451,7 +451,7 @@ def check_links! ], ) - expect(target_edition).not_to have_links("test_link") + expect(target_edition).not_to have_reverse_links("test_link") end end @@ -477,7 +477,7 @@ def check_links! ], ) - expect(target_edition).not_to have_links("test_link") + expect(target_edition).not_to have_reverse_links("test_link") end end end @@ -507,7 +507,7 @@ def check_links! ) expected_titles = [french_edition].map(&:title) - expect(target_edition).to have_links("related_statistical_data_sets").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("related_statistical_data_sets").with_titles(expected_titles) end it "omits a locale-matching reverse link if it isn't a permitted unpublished link_type" do @@ -523,7 +523,7 @@ def check_links! ], ) - expect(target_edition).not_to have_links("test_link") + expect(target_edition).not_to have_reverse_links("test_link") end end end @@ -551,7 +551,7 @@ def check_links! ) expected_titles = [english_published_edition].map(&:title) - expect(target_edition).to have_links("related_statistical_data_sets").with_titles(expected_titles) + expect(target_edition).to have_reverse_links("related_statistical_data_sets").with_titles(expected_titles) end end @@ -577,7 +577,7 @@ def check_links! ], ) - expect(target_edition).not_to have_links("related_statistical_data_sets") + expect(target_edition).not_to have_reverse_links("related_statistical_data_sets") end end end From 32ceaab8b672dd32d51383c324cde212903acfe3 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 2 Jan 2026 17:03:25 +0000 Subject: [PATCH 04/20] Test #live_content when only drafts are available We're working towards supporting draft content via GraphQL. It seems reasonable to have this scenario covered for the #live_content controller action, whether we choose to extend this endpoint or create a separate one. --- spec/controllers/graphql_controller_spec.rb | 27 +++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 7317e5ab34..f430b69d4f 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -69,6 +69,33 @@ end end + context "when the requested base_path only has draft content" do + let(:edition) do + create( + :draft_edition, + schema_name: "person", + document_type: "person", + details: { + "body" => "Some content", + }, + ) + end + + let(:request_path) { base_path_without_leading_slash(edition.base_path) } + + before do + get :live_content, params: { base_path: request_path } + end + + it "returns a 404 Not Found response" do + expect(response.status).to eq(404) + end + + it "doesn't return any content item data" do + expect(response.body).not_to be_present + end + end + context "a content item with a non-ASCII base_path" do before(:each) do create( From 914bea8564d0452effd7c8b6b5462530f0e367fb Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Wed, 31 Dec 2025 14:48:57 +0000 Subject: [PATCH 05/20] Bump draft factory's default user_facing_version The `live_edition` and `draft_edition` (alias `edition`) spec factories were both defaulting to a user_facing_version of 1. Since a document's live and draft editions are required to have different user_facing_versions, this new default of 2 enables a spec example to create both states of editions for a single document without introducing the noise of explicitly setting this attribute for one or both of them. We can rethink this if it begins to make other tests more awkward in future. Note: that the unpublished_edition factory needed updating here too. This was to prevent its default user_facing_version from changing since it inherits from the factory we've changed. --- spec/factories/edition.rb | 2 +- spec/factories/unpublished_edition.rb | 2 ++ .../put_content/content_with_a_previous_draft_spec.rb | 1 + spec/lib/tasks/data_hygiene_spec.rb | 4 ++-- spec/presenters/queries/content_item_presenter_spec.rb | 2 ++ 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/spec/factories/edition.rb b/spec/factories/edition.rb index d7302521f1..e29c93305c 100644 --- a/spec/factories/edition.rb +++ b/spec/factories/edition.rb @@ -29,7 +29,7 @@ state { "draft" } content_store { "draft" } sequence(:base_path) { |n| "/vat-rates-#{n}" } - user_facing_version { 1 } + user_facing_version { 2 } transient do change_note { "note" } diff --git a/spec/factories/unpublished_edition.rb b/spec/factories/unpublished_edition.rb index 811921a5cf..4ddc88ec51 100644 --- a/spec/factories/unpublished_edition.rb +++ b/spec/factories/unpublished_edition.rb @@ -4,6 +4,8 @@ content_store { "live" } public_updated_at { "2014-05-14T13:00:06Z" } first_published_at { "2014-01-02T03:04:05Z" } + user_facing_version { 1 } + transient do unpublishing_type { "gone" } explanation { "Removed for testing reasons" } diff --git a/spec/integration/put_content/content_with_a_previous_draft_spec.rb b/spec/integration/put_content/content_with_a_previous_draft_spec.rb index dd5db7ee6f..b3105da003 100644 --- a/spec/integration/put_content/content_with_a_previous_draft_spec.rb +++ b/spec/integration/put_content/content_with_a_previous_draft_spec.rb @@ -21,6 +21,7 @@ title: "Old Title", publishing_app: "publisher", update_type: "major", + user_facing_version: 1, ) end diff --git a/spec/lib/tasks/data_hygiene_spec.rb b/spec/lib/tasks/data_hygiene_spec.rb index 9a5cd26fe1..5ea0513cf2 100644 --- a/spec/lib/tasks/data_hygiene_spec.rb +++ b/spec/lib/tasks/data_hygiene_spec.rb @@ -16,8 +16,8 @@ context "and when a draft is present" do before do - create(:edition, document:).publish - create(:edition, document:, user_facing_version: 2) + create(:live_edition, document:) + create(:draft_edition, document:) end it "runs the process to discard a draft with the locale" do diff --git a/spec/presenters/queries/content_item_presenter_spec.rb b/spec/presenters/queries/content_item_presenter_spec.rb index 5f0239337c..d9a4a96abc 100644 --- a/spec/presenters/queries/content_item_presenter_spec.rb +++ b/spec/presenters/queries/content_item_presenter_spec.rb @@ -18,6 +18,7 @@ first_published_at:, public_updated_at:, auth_bypass_ids: [SecureRandom.uuid], + user_facing_version: 1, ) end @@ -120,6 +121,7 @@ update_type: "major", first_published_at:, public_updated_at:, + user_facing_version: 1, ) end From a9bd65c9f9211c06dd318e62a418544bcfce4409 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 2 Jan 2026 10:31:18 +0000 Subject: [PATCH 06/20] Support with_drafts flag in LinkedToEditionsSource This replaces the content_store parameter. The behaviour of this dataloader is the same between the old default of `content_store: "live"` and the new default of `with_drafts: false`. `content_store: "draft"` won't behave like `with_drafts: true`, though, since live editions can now be included in a draft edition's links (if a more suitable draft version doesn't exist). In the near future the flag will be set in the query. For now we omit the argument when called in the link methods defined via `Types::BaseObject`, thereby defaulting to false. The inclusion tests are also updated to use the new parameter to check drafts. Co-authored-by: Ynda Jas --- .../sources/linked_to_editions_source.rb | 6 +- .../sources/queries/linked_to_editions.sql | 28 +++--- app/graphql/types/base_object.rb | 1 - .../sources/linked_to_editions_source_spec.rb | 90 ++++++++++++++++--- .../direct_link_inclusion_spec.rb | 5 +- .../direct_links/test_case.rb | 1 - 6 files changed, 98 insertions(+), 33 deletions(-) diff --git a/app/graphql/sources/linked_to_editions_source.rb b/app/graphql/sources/linked_to_editions_source.rb index d6a0b85c99..0e11578c78 100644 --- a/app/graphql/sources/linked_to_editions_source.rb +++ b/app/graphql/sources/linked_to_editions_source.rb @@ -3,10 +3,10 @@ class LinkedToEditionsSource < GraphQL::Dataloader::Source SQL = File.read(Rails.root.join("app/graphql/sources/queries/linked_to_editions.sql")) # rubocop:disable Lint/MissingSuper - def initialize(content_store:, locale:) - @content_store = content_store.to_sym + def initialize(locale:, with_drafts: false) @primary_locale = locale @secondary_locale = Edition::DEFAULT_LOCALE + @with_drafts = with_drafts end # rubocop:enable Lint/MissingSuper @@ -23,7 +23,7 @@ def fetch(editions_and_link_types) query_input_count: query_input.count, primary_locale: @primary_locale, secondary_locale: @secondary_locale, - content_store: @content_store, + state_unless_withdrawn: @with_drafts ? %i[draft published] : %i[published], unpublished_link_types: Link::PERMITTED_UNPUBLISHED_LINK_TYPES, non_renderable_formats: Edition::NON_RENDERABLE_FORMATS, } diff --git a/app/graphql/sources/queries/linked_to_editions.sql b/app/graphql/sources/queries/linked_to_editions.sql index ca6aa07573..6f7773b13a 100644 --- a/app/graphql/sources/queries/linked_to_editions.sql +++ b/app/graphql/sources/queries/linked_to_editions.sql @@ -28,13 +28,14 @@ edition_linked_editions AS ( INNER JOIN documents AS source_documents ON source_editions.document_id = source_documents.id LEFT JOIN unpublishings ON editions.id = unpublishings.edition_id WHERE - editions.content_store =:content_store - AND documents.locale IN (:primary_locale,:secondary_locale) + documents.locale IN (:primary_locale,:secondary_locale) AND editions.document_type NOT IN (:non_renderable_formats) AND ( - editions.state != 'unpublished' + editions.state IN (:state_unless_withdrawn) OR ( + editions.state = 'unpublished' + AND links.link_type IN (:unpublished_link_types) AND unpublishings.type = 'withdrawal' @@ -43,9 +44,10 @@ edition_linked_editions AS ( ORDER BY links.id ASC, CASE editions.state - WHEN 'published' THEN 0 - WHEN 'unpublished' THEN 1 - ELSE 2 + WHEN 'draft' THEN 0 + WHEN 'published' THEN 1 + WHEN 'unpublished' THEN 2 + ELSE 3 END, is_primary_locale DESC ), @@ -68,13 +70,14 @@ link_set_linked_editions AS ( ON links.link_set_content_id = query_input.content_id AND links.link_type = query_input.link_type LEFT JOIN unpublishings ON editions.id = unpublishings.edition_id WHERE - editions.content_store =:content_store - AND documents.locale IN (:primary_locale,:secondary_locale) + documents.locale IN (:primary_locale,:secondary_locale) AND editions.document_type NOT IN (:non_renderable_formats) AND ( - editions.state != 'unpublished' + editions.state IN (:state_unless_withdrawn) OR ( + editions.state = 'unpublished' + AND links.link_type IN (:unpublished_link_types) AND unpublishings.type = 'withdrawal' @@ -90,9 +93,10 @@ link_set_linked_editions AS ( ORDER BY links.id ASC, CASE editions.state - WHEN 'published' THEN 0 - WHEN 'unpublished' THEN 1 - ELSE 2 + WHEN 'draft' THEN 0 + WHEN 'published' THEN 1 + WHEN 'unpublished' THEN 2 + ELSE 3 END, is_primary_locale DESC ) diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 7f8e86180f..96a566f5e4 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -12,7 +12,6 @@ def self.links_field(link_type, graphql_field_type) define_method(link_type.to_sym) do dataloader.with( Sources::LinkedToEditionsSource, - content_store: object.content_store, locale: context[:root_edition].locale, ) .load([object, link_type.to_s]) diff --git a/spec/graphql/sources/linked_to_editions_source_spec.rb b/spec/graphql/sources/linked_to_editions_source_spec.rb index 278a2a3ff6..8108685fa5 100644 --- a/spec/graphql/sources/linked_to_editions_source_spec.rb +++ b/spec/graphql/sources/linked_to_editions_source_spec.rb @@ -16,7 +16,7 @@ def check_links! GraphQL::Dataloader.with_dataloading do |dataloader| request = dataloader.with( described_class, - content_store: source_edition.content_store, + with_drafts:, locale: source_edition.locale, ).request([source_edition, link_type]) @@ -37,6 +37,8 @@ def check_links! end end + let(:with_drafts) { false } + context "when the same source content has a mix of link set links and edition links for the same link type" do it "returns only the edition links" do target_edition_1 = create(:live_edition, title: "edition 1, test link, edition link") @@ -73,18 +75,53 @@ def check_links! expect(source_edition).to have_links("test_link").with_titles(expected_titles).in_any_order end - it "returns links from only the requested content store" do - target_edition_0 = create(:live_edition, title: "edition 0, live") - target_edition_1 = create(:draft_edition, title: "edition 1, draft") + context "when with_drafts=true" do + let(:with_drafts) { true } - source_edition = create(:draft_edition, - links_kind => [ - { link_type: "test_link", target_content_id: target_edition_0.content_id }, - { link_type: "test_link", target_content_id: target_edition_1.content_id }, - ]) + it "returns links to drafts when drafts are available" do + target_document = create(:document) + create(:live_edition, title: "edition 1, live", document: target_document) + target_edition = create(:draft_edition, title: "edition 2, draft", document: target_document) - expected_titles = [target_edition_1.title] - expect(source_edition).to have_links("test_link").with_titles(expected_titles) + source_edition = create(:draft_edition, + links_kind => [ + { link_type: "test_link", target_content_id: target_document.content_id }, + ]) + + expected_titles = [target_edition.title] + expect(source_edition).to have_links("test_link").with_titles(expected_titles) + end + + it "returns links to live editions when drafts aren't available" do + target_document = create(:document) + target_edition = create(:live_edition, title: "edition, live", document: target_document) + + source_edition = create(:draft_edition, + links_kind => [ + { link_type: "test_link", target_content_id: target_document.content_id }, + ]) + + expected_titles = [target_edition.title] + expect(source_edition).to have_links("test_link").with_titles(expected_titles) + end + end + + context "when with_drafts=false" do + it "doesn't return links to drafts" do + target_edition_1 = create(:draft_edition, title: "edition 1, draft") + target_document = create(:document) + target_edition_2 = create(:live_edition, title: "edition 2, live", document: target_document) + create(:draft_edition, title: "edition 3, draft", document: target_document) + + source_edition = create(:live_edition, + links_kind => [ + { link_type: "test_link", target_content_id: target_edition_1.content_id }, + { link_type: "test_link", target_content_id: target_document.content_id }, + ]) + + expected_titles = [target_edition_2.title] + expect(source_edition).to have_links("test_link").with_titles(expected_titles) + end end it "returns editions in order of their associated link's `position`" do @@ -240,7 +277,36 @@ def check_links! expect(source_edition).not_to have_links("test_link") end - context "when the source Edition is live" do + context "when requested with with_drafts=true" do + let(:with_drafts) { true } + + it "includes a draft 'en' link if there isn't a draft locale-matching one" do + target_content_id = SecureRandom.uuid + english_edition = create( + :draft_edition, + document: create(:document, locale: "en", content_id: target_content_id), + title: "english draft edition", + ) + create( + :live_edition, + document: create(:document, locale: "fr", content_id: target_content_id), + title: "french live edition", + ) + + source_edition = create( + :live_edition, + document: create(:document, locale: "fr"), + links_kind => [ + { link_type: "test_link", target_content_id: }, + ], + ) + + expected_titles = [english_edition.title] + expect(source_edition).to have_links("test_link").with_titles(expected_titles) + end + end + + context "when requested with with_drafts=false" do it "defaults to including a (live) 'en' link if the locale-matching one is draft" do target_content_id = SecureRandom.uuid english_edition = create( diff --git a/spec/integration/graphql/link_expansion/direct_link_inclusion_spec.rb b/spec/integration/graphql/link_expansion/direct_link_inclusion_spec.rb index 46587021ce..38ff31ff20 100644 --- a/spec/integration/graphql/link_expansion/direct_link_inclusion_spec.rb +++ b/spec/integration/graphql/link_expansion/direct_link_inclusion_spec.rb @@ -10,8 +10,8 @@ def for_graphql(source_edition, link_type:, with_drafts:) GraphQL::Dataloader.with_dataloading do |dataloader| request = dataloader.with( Sources::LinkedToEditionsSource, - content_store: with_drafts ? "draft" : "live", locale: source_edition.locale, + with_drafts:, ).request([source_edition, link_type]) request.load @@ -60,9 +60,6 @@ def for_graphql(source_edition, link_type:, with_drafts:) ) %w[content_store graphql].each do |destination| - # GraphQL doesn't yet support drafts - next if destination == "graphql" && test_case.with_drafts? - result = send( :"for_#{destination}", source_edition, diff --git a/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case.rb b/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case.rb index 3194571bff..330ef9a3fa 100644 --- a/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case.rb +++ b/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case.rb @@ -25,7 +25,6 @@ def graphql_titles @graphql_titles ||= GraphQL::Dataloader.with_dataloading do |dataloader| request = dataloader.with( Sources::LinkedToEditionsSource, - content_store: "live", locale: root_locale, ).request([source_edition, link_type]) From 082d71a9d52f7e84c31d1b966c1aee01d986f7da Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 2 Jan 2026 10:31:18 +0000 Subject: [PATCH 07/20] Support with_drafts flag in Reverse... dataloader This replaces the content_store parameter. The behaviour of this dataloader is the same between the old default of `content_store: "live"` and the new default of `with_drafts: false`. `content_store: "draft"` won't behave like `with_drafts: true`, though, since live editions can now be included in a draft edition's links (if a more suitable draft version doesn't exist). In the near future the flag will be set in the query. For now we omit the argument when called in the link methods defined via `Types::BaseObject` and `Types::EditionQuery`, thereby defaulting to false. The inclusion tests are also updated to use the new parameter to check drafts. Co-authored-by: Ynda Jas Check drafts in reverse link expansion inclusion tests --- .../queries/reverse_linked_to_editions.sql | 21 ++-- .../reverse_linked_to_editions_source.rb | 6 +- app/graphql/types/base_object.rb | 1 - app/graphql/types/edition_type.rb | 2 - .../reverse_linked_to_editions_source_spec.rb | 119 +++++++++++++++++- .../reverse_link_inclusion_spec.rb | 5 +- .../reverse_links/test_case.rb | 1 - 7 files changed, 132 insertions(+), 23 deletions(-) diff --git a/app/graphql/sources/queries/reverse_linked_to_editions.sql b/app/graphql/sources/queries/reverse_linked_to_editions.sql index 1633010891..77898be337 100644 --- a/app/graphql/sources/queries/reverse_linked_to_editions.sql +++ b/app/graphql/sources/queries/reverse_linked_to_editions.sql @@ -25,13 +25,14 @@ edition_linked_editions AS ( INNER JOIN query_input ON links.target_content_id = query_input.content_id AND links.link_type = query_input.link_type LEFT JOIN unpublishings ON editions.id = unpublishings.edition_id WHERE - editions.content_store =:content_store - AND documents.locale =:primary_locale + documents.locale =:primary_locale AND editions.document_type NOT IN (:non_renderable_formats) AND ( - editions.state != 'unpublished' + editions.state IN (:state_unless_withdrawn) OR ( + editions.state = 'unpublished' + AND links.link_type IN (:unpublished_link_types) AND unpublishings.type = 'withdrawal' @@ -55,13 +56,14 @@ link_set_linked_editions AS ( INNER JOIN query_input ON links.target_content_id = query_input.content_id AND links.link_type = query_input.link_type LEFT JOIN unpublishings ON editions.id = unpublishings.edition_id WHERE - editions.content_store =:content_store - AND documents.locale IN (:primary_locale,:secondary_locale) + documents.locale IN (:primary_locale,:secondary_locale) AND editions.document_type NOT IN (:non_renderable_formats) AND ( - editions.state != 'unpublished' + editions.state IN (:state_unless_withdrawn) OR ( + editions.state = 'unpublished' + AND links.link_type IN (:unpublished_link_types) AND unpublishings.type = 'withdrawal' @@ -86,9 +88,10 @@ SELECT editions.* FROM ( content_id ASC, target_content_id ASC, CASE state - WHEN 'published' THEN 0 - WHEN 'unpublished' THEN 1 - ELSE 2 + WHEN 'draft' THEN 0 + WHEN 'published' THEN 1 + WHEN 'unpublished' THEN 2 + ELSE 3 END, is_primary_locale DESC ) AS editions diff --git a/app/graphql/sources/reverse_linked_to_editions_source.rb b/app/graphql/sources/reverse_linked_to_editions_source.rb index 2ff9177811..74bc17a230 100644 --- a/app/graphql/sources/reverse_linked_to_editions_source.rb +++ b/app/graphql/sources/reverse_linked_to_editions_source.rb @@ -3,10 +3,10 @@ class ReverseLinkedToEditionsSource < GraphQL::Dataloader::Source SQL = File.read(Rails.root.join("app/graphql/sources/queries/reverse_linked_to_editions.sql")) # rubocop:disable Lint/MissingSuper - def initialize(content_store:, locale:) - @content_store = content_store.to_sym + def initialize(locale:, with_drafts: false) @primary_locale = locale @secondary_locale = Edition::DEFAULT_LOCALE + @with_drafts = with_drafts end # rubocop:enable Lint/MissingSuper @@ -24,7 +24,7 @@ def fetch(editions_and_link_types) query_input_count: query_input.count, secondary_locale: @secondary_locale, primary_locale: @primary_locale, - content_store: @content_store, + state_unless_withdrawn: @with_drafts ? %i[draft published] : %i[published], unpublished_link_types: Link::PERMITTED_UNPUBLISHED_LINK_TYPES, non_renderable_formats: Edition::NON_RENDERABLE_FORMATS, } diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 96a566f5e4..9494b0daca 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -24,7 +24,6 @@ def self.reverse_links_field(field_name, link_type, graphql_field_type) define_method(field_name.to_sym) do dataloader.with( Sources::ReverseLinkedToEditionsSource, - content_store: object.content_store, locale: context[:root_edition].locale, ) .load([object, link_type.to_s]) diff --git a/app/graphql/types/edition_type.rb b/app/graphql/types/edition_type.rb index 6a6d9a0e96..6d668f962d 100644 --- a/app/graphql/types/edition_type.rb +++ b/app/graphql/types/edition_type.rb @@ -103,14 +103,12 @@ def role_appointments if %w[role ministerial_role].include?(object.document_type) dataloader.with( Sources::ReverseLinkedToEditionsSource, - content_store: object.content_store, locale: context[:root_edition].locale, ) .load([object, "role"]) else dataloader.with( Sources::ReverseLinkedToEditionsSource, - content_store: object.content_store, locale: context[:root_edition].locale, ) .load([object, "person"]) diff --git a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb index 654f128493..6b756982f2 100644 --- a/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb +++ b/spec/graphql/sources/reverse_linked_to_editions_source_spec.rb @@ -16,7 +16,7 @@ def check_links! GraphQL::Dataloader.with_dataloading do |dataloader| request = dataloader.with( described_class, - content_store: target_edition.content_store, + with_drafts:, locale: target_edition.locale, ).request([target_edition, link_type]) @@ -37,6 +37,8 @@ def check_links! end end + let(:with_drafts) { false } + context "when the same target content has a mix of link set links and edition links for the same link type" do it "returns all valid source editions" do target_edition = create(:live_edition) @@ -107,6 +109,86 @@ def check_links! expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles).in_any_order end + context "when with_drafts=true" do + let(:with_drafts) { true } + + it "returns reverse links to drafts when drafts are available" do + target_edition = create(:live_edition) + + source_document = create(:document) + source_edition_1 = create( + :draft_edition, + title: "edition 1, draft", + document: source_document, + links_kind => [ + { link_type: "test_link", target_content_id: target_edition.content_id }, + ], + ) + create( + :live_edition, + title: "edition 2, live", + document: source_document, + links_kind => [ + { link_type: "test_link", target_content_id: target_edition.content_id }, + ], + ) + + expected_titles = [source_edition_1.title] + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) + end + + it "returns reverse links to live editions when drafts aren't available" do + target_edition = create(:live_edition) + + source_edition = create( + :live_edition, + title: "edition 1, live", + links_kind => [ + { link_type: "test_link", target_content_id: target_edition.content_id }, + ], + ) + + expected_titles = [source_edition.title] + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) + end + end + + context "when with_drafts=false" do + it "doesn't return reverse links to drafts" do + target_edition = create(:live_edition) + + create( + :draft_edition, + title: "edition 1, draft", + links_kind => [ + { link_type: "test_link", target_content_id: target_edition.content_id }, + ], + ) + + source_document = create(:document) + source_edition_2 = create( + :live_edition, + title: "edition 2, live", + document: source_document, + links_kind => [ + { link_type: "test_link", target_content_id: target_edition.content_id }, + ], + ) + + create( + :draft_edition, + title: "edition 3, draft", + document: source_document, + links_kind => [ + { link_type: "test_link", target_content_id: target_edition.content_id }, + ], + ) + + expected_titles = [source_edition_2.title] + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) + end + end + it "returns editions ordered by their reverse links' `position`" do target_edition = create(:live_edition) @@ -403,7 +485,38 @@ def check_links! end end - context "when the Edition is live" do + context "when requested with with_drafts=true" do + let(:with_drafts) { true } + + context "when the link kind is link_set_links" do + it "includes a draft 'en' link if there isn't a draft locale-matching one" do + target_edition = create(:live_edition, document: create(:document, locale: "fr")) + + source_content_id = SecureRandom.uuid + english_edition = create( + :draft_edition, + document: create(:document, locale: "en", content_id: source_content_id), + title: "english draft edition", + link_set_links: [ + { link_type: "test_link", target_content_id: target_edition.content_id }, + ], + ) + create( + :live_edition, + document: create(:document, locale: "fr", content_id: source_content_id), + title: "french live edition", + link_set_links: [ + { link_type: "test_link", target_content_id: target_edition.content_id }, + ], + ) + + expected_titles = [english_edition].map(&:title) + expect(target_edition).to have_reverse_links("test_link").with_titles(expected_titles) + end + end + end + + context "when requested with with_drafts=false" do context "when the link kind is link_set_links" do it "defaults to including a (live) 'en' reverse link when the locale-matching one is draft" do target_edition = create(:live_edition, document: create(:document, locale: "fr")) @@ -456,7 +569,7 @@ def check_links! end context "when the link kind is edition_links" do - it "doesn't default to including a (live) 'en' reverse link when the locale-matching one is draft" do + it "doesn't include any reverse links if there are no locale-matching live ones" do target_edition = create(:live_edition, document: create(:document, locale: "fr")) source_content_id = SecureRandom.uuid diff --git a/spec/integration/graphql/link_expansion/reverse_link_inclusion_spec.rb b/spec/integration/graphql/link_expansion/reverse_link_inclusion_spec.rb index 4ddef10da9..f7747bf578 100644 --- a/spec/integration/graphql/link_expansion/reverse_link_inclusion_spec.rb +++ b/spec/integration/graphql/link_expansion/reverse_link_inclusion_spec.rb @@ -11,8 +11,8 @@ def for_graphql(target_edition, link_type:, with_drafts:) GraphQL::Dataloader.with_dataloading do |dataloader| request = dataloader.with( Sources::ReverseLinkedToEditionsSource, - content_store: with_drafts ? "draft" : "live", locale: target_edition.locale, + with_drafts:, ).request([target_edition, link_type]) request.load @@ -60,9 +60,6 @@ def for_graphql(target_edition, link_type:, with_drafts:) end %w[content_store graphql].each do |destination| - # GraphQL doesn't yet support drafts - next if destination == "graphql" && test_case.with_drafts? - # GraphQL will only attempt to find a reverse link for a field that is declared # as a reverse_links_field in app/graphql/types/edition_type.rb. This test # calls the dataloader directly, but the dataloader won't check that the diff --git a/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case.rb b/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case.rb index 54d2adfbde..d0890c51a3 100644 --- a/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case.rb +++ b/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case.rb @@ -31,7 +31,6 @@ def graphql_result GraphQL::Dataloader.with_dataloading do |dataloader| request = dataloader.with( Sources::ReverseLinkedToEditionsSource, - content_store: "live", locale: root_locale, ).request([linked_edition, link_type]) From fab1bf7c5a004a4063a4382c1b497f5db24f3a1d Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Tue, 6 Jan 2026 09:45:51 +0000 Subject: [PATCH 08/20] Make timestamps nullable to support drafts Draft editions can legitimately have null first_published_at and/or public_updated_at. They follow different rules, but I've found Edition::Timestamps and UpdateExistingDraftEdition to be useful reading on the topic --- app/graphql/types/edition_type.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/graphql/types/edition_type.rb b/app/graphql/types/edition_type.rb index 6d668f962d..1ec635804d 100644 --- a/app/graphql/types/edition_type.rb +++ b/app/graphql/types/edition_type.rb @@ -388,13 +388,13 @@ class Details < Types::BaseObject field :details_json, GraphQL::Types::JSON field :document_type, String field :ended_on, Types::ContentApiDatetime - field :first_published_at, Types::ContentApiDatetime, null: false + field :first_published_at, Types::ContentApiDatetime 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, Types::ContentApiDatetime, null: false + field :public_updated_at, Types::ContentApiDatetime field :publishing_app, String field :publishing_request_id, String field :publishing_scheduled_at, Types::ContentApiDatetime From e2564c997dd7dcffc115f8c0d9a18a1a27f2c56b Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 2 Jan 2026 10:33:13 +0000 Subject: [PATCH 09/20] Remove some `content_store: "live"`s The `content_store` variable isn't currently being used and we're dropping support for it. Its replacement (`with_drafts`) is coming soon --- app/graphql/queries/news_article.graphql | 2 +- spec/integration/graphql/news_article_spec.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/graphql/queries/news_article.graphql b/app/graphql/queries/news_article.graphql index 205728bc78..94c34eab3f 100644 --- a/app/graphql/queries/news_article.graphql +++ b/app/graphql/queries/news_article.graphql @@ -1,5 +1,5 @@ query news_article($base_path: String!) { - edition(base_path: $base_path, content_store: "live") { + edition(base_path: $base_path) { ... on Edition { base_path content_id diff --git a/spec/integration/graphql/news_article_spec.rb b/spec/integration/graphql/news_article_spec.rb index 23b3367c32..b1f4074370 100644 --- a/spec/integration/graphql/news_article_spec.rb +++ b/spec/integration/graphql/news_article_spec.rb @@ -105,7 +105,6 @@ { edition( base_path: "/government/news/announcement", - content_store: "live", ) { ... on Edition { base_path From bed1e966380cd90445337240a5e944509911c2b1 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 19 Dec 2025 16:45:51 +0000 Subject: [PATCH 10/20] Replace QueryType content_store with with_drafts As with the dataloader sources, this content_store setting was never used and it defaulted to "live". A query with `content_store: "live"` always returned a live edition or no edition at all. That's the same behaviour as `with_drafts: false`. A query with `content_store: "draft"` always returned a draft edition or no edition. `with_drafts: true` doesn't behave like that because it can return a draft edition or a live edition or no edition, with that order of precedence. In Content Store land, there's a draft content item (i.e. in Draft Content Store) for every published content item (i.e. in Live Content Store). This `with_drafts` fallback behaviour is intended to mimick the user-visible results of that. Unlike the dataloader sources, which have to consider a linked-to edition's state, QueryType looks up the root edition by base_path, which will only have 0 or 1 exact matches for each of `content_store: "live"` and `content_store: "draft"`. --- app/graphql/types/query_type.rb | 25 +++++++-- spec/graphql/types/query_type_spec.rb | 73 +++++++++++++++++++++------ 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 566e5d41ae..ed78a9d281 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -4,14 +4,31 @@ module Types class QueryType < Types::BaseObject field :edition, EditionTypeOrSubtype, description: "An edition or one of its subtypes" do argument :base_path, String - argument :content_store, String, required: false, default_value: "live" + argument :with_drafts, Boolean, required: false, default_value: false end - def edition(base_path:, content_store:) + def edition(base_path:, with_drafts:) + content_stores = if with_drafts + %i[draft live] + else + %i[live] + end + edition = Edition .includes(:document, :unpublishing) - .where(content_store:) - .find_by(base_path:) + .where(base_path:, content_store: content_stores) + .order( + Arel.sql( + <<~SQL, + CASE editions.content_store + WHEN 'draft' THEN 0 + WHEN 'live' THEN 1 + ELSE 2 + END + SQL + ), + ) + .first return unless edition diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb index 2dedae6436..f75d88d80e 100644 --- a/spec/graphql/types/query_type_spec.rb +++ b/spec/graphql/types/query_type_spec.rb @@ -4,8 +4,8 @@ describe "#edition" do let(:query) do <<~QUERY - query($base_path: String!, $content_store: String!) { - edition(base_path: $base_path, content_store: $content_store) { + query($base_path: String!, $with_drafts: Boolean!) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { base_path state @@ -18,22 +18,34 @@ context "when there is only a draft edition" do let(:draft_edition) { create(:draft_edition) } - context "requesting the draft edition" do + context "requesting with_drafts=true" do it "returns the draft edition" do expected_data = { "base_path" => draft_edition.base_path, "state" => "draft", } - result = PublishingApiSchema.execute(query, variables: { base_path: draft_edition.base_path, content_store: "draft" }) + result = PublishingApiSchema.execute( + query, + variables: { + base_path: draft_edition.base_path, + with_drafts: true, + }, + ) edition_data = result.dig("data", "edition") expect(edition_data).to eq(expected_data) end end - context "requesting the live edition" do + context "requesting with_drafts=false" do it "returns no edition" do - result = PublishingApiSchema.execute(query, variables: { base_path: draft_edition.base_path, content_store: "live" }) + result = PublishingApiSchema.execute( + query, + variables: { + base_path: draft_edition.base_path, + with_drafts: false, + }, + ) edition_data = result.dig("data", "edition") expect(edition_data).to be_nil end @@ -43,22 +55,39 @@ context "when there is only a live edition" do let(:live_edition) { create(:live_edition) } - context "requesting the draft edition" do - it "returns no edition" do - result = PublishingApiSchema.execute(query, variables: { base_path: live_edition.base_path, content_store: "draft" }) + context "requesting with_drafts=true" do + it "returns the live edition" do + expected_data = { + "base_path" => live_edition.base_path, + "state" => "published", + } + + result = PublishingApiSchema.execute( + query, + variables: { + base_path: live_edition.base_path, + with_drafts: true, + }, + ) edition_data = result.dig("data", "edition") - expect(edition_data).to be_nil + expect(edition_data).to eq(expected_data) end end - context "requesting the live edition" do + context "requesting with_drafts=false" do it "returns the live edition" do expected_data = { "base_path" => live_edition.base_path, "state" => "published", } - result = PublishingApiSchema.execute(query, variables: { base_path: live_edition.base_path, content_store: "live" }) + result = PublishingApiSchema.execute( + query, + variables: { + base_path: live_edition.base_path, + with_drafts: false, + }, + ) edition_data = result.dig("data", "edition") expect(edition_data).to eq(expected_data) end @@ -71,27 +100,39 @@ let!(:live_edition) { create(:live_edition, document:, base_path:, user_facing_version: 1) } let!(:draft_edition) { create(:draft_edition, document:, base_path:, user_facing_version: 2) } - context "requesting the draft edition" do + context "requesting with_drafts=true" do it "returns the draft edition" do expected_data = { "base_path" => draft_edition.base_path, "state" => "draft", } - result = PublishingApiSchema.execute(query, variables: { base_path: draft_edition.base_path, content_store: "draft" }) + result = PublishingApiSchema.execute( + query, + variables: { + base_path: live_edition.base_path, + with_drafts: true, + }, + ) edition_data = result.dig("data", "edition") expect(edition_data).to eq(expected_data) end end - context "requesting the live edition" do + context "requesting with_drafts=false" do it "returns the live edition" do expected_data = { "base_path" => live_edition.base_path, "state" => "published", } - result = PublishingApiSchema.execute(query, variables: { base_path: live_edition.base_path, content_store: "live" }) + result = PublishingApiSchema.execute( + query, + variables: { + base_path: live_edition.base_path, + with_drafts: false, + }, + ) edition_data = result.dig("data", "edition") expect(edition_data).to eq(expected_data) end From c7c2a03855d2ca55fdb9a274d65688e99519676c Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 2 Jan 2026 10:29:48 +0000 Subject: [PATCH 11/20] Pass query's with_drafts setting to dataloaders Until recently, the dataloaders would only link editions together that had matching `content_store`s. Since introducing `with_drafts` to the dataloaders, we've been omitting the argument in the link methods defined in `Types::BaseObject` and thereby defaulting to `false`, only including live linked editions. Now, using the query's `with_drafts` setting in the dataloaders, when `with_drafts: true`, any draft edition can link to a live edition and vice versa, at any level of nesting in the generated response. --- app/graphql/types/base_object.rb | 2 ++ app/graphql/types/edition_type.rb | 2 ++ app/graphql/types/query_type.rb | 1 + 3 files changed, 5 insertions(+) diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 9494b0daca..7337e02c42 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -12,6 +12,7 @@ def self.links_field(link_type, graphql_field_type) define_method(link_type.to_sym) do dataloader.with( Sources::LinkedToEditionsSource, + with_drafts: context[:with_drafts], locale: context[:root_edition].locale, ) .load([object, link_type.to_s]) @@ -24,6 +25,7 @@ def self.reverse_links_field(field_name, link_type, graphql_field_type) define_method(field_name.to_sym) do dataloader.with( Sources::ReverseLinkedToEditionsSource, + with_drafts: context[:with_drafts], locale: context[:root_edition].locale, ) .load([object, link_type.to_s]) diff --git a/app/graphql/types/edition_type.rb b/app/graphql/types/edition_type.rb index 1ec635804d..4b85042a90 100644 --- a/app/graphql/types/edition_type.rb +++ b/app/graphql/types/edition_type.rb @@ -103,12 +103,14 @@ def role_appointments if %w[role ministerial_role].include?(object.document_type) dataloader.with( Sources::ReverseLinkedToEditionsSource, + with_drafts: context[:with_drafts], locale: context[:root_edition].locale, ) .load([object, "role"]) else dataloader.with( Sources::ReverseLinkedToEditionsSource, + with_drafts: context[:with_drafts], locale: context[:root_edition].locale, ) .load([object, "person"]) diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index ed78a9d281..4836ce7863 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -46,6 +46,7 @@ def edition(base_path:, with_drafts:) end context[:root_edition] = edition + context[:with_drafts] = with_drafts edition end From 733b5d795be44455c7cf48ac486575e97f8b7a6d Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Tue, 6 Jan 2026 13:23:51 +0000 Subject: [PATCH 12/20] Add with_drafts variable when generating queries We're working towards supporting draft content via GraphQL using the `with_drafts` flag. Any GraphQL queries we generate in future should have support for this variable to allow drafts to be served. --- lib/graphql_query_builder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/graphql_query_builder.rb b/lib/graphql_query_builder.rb index e30ea849b2..34b8f5139d 100644 --- a/lib/graphql_query_builder.rb +++ b/lib/graphql_query_builder.rb @@ -22,8 +22,8 @@ def initialize(schema_name) def build_query parts = [ - "query #{@schema_name}($base_path: String!) {", - " edition(base_path: $base_path) {", + "query #{@schema_name}($base_path: String!, $with_drafts: Boolean) {", + " edition(base_path: $base_path, with_drafts: $with_drafts) {", " ... on #{edition_type_or_subtype} {", build_fields(@content_item, indent: 6), " }", From 388789dd3b1bd0e5153c4b14167ebb2e2d08a61b Mon Sep 17 00:00:00 2001 From: Richard Towers Date: Thu, 18 Dec 2025 12:05:17 +0000 Subject: [PATCH 13/20] Add with_drafts variable to all GraphQL queries We're working towards supporting draft content via GraphQL using the `with_drafts` flag. Including this variable in a query enables clients to set the variable and it ultimately ends up being used by the QueryType and dataloaders. --- app/graphql/queries/answer.graphql | 4 ++-- app/graphql/queries/calendar.graphql | 4 ++-- app/graphql/queries/call_for_evidence.graphql | 4 ++-- app/graphql/queries/case_study.graphql | 4 ++-- app/graphql/queries/completed_transaction.graphql | 4 ++-- app/graphql/queries/consultation.graphql | 4 ++-- app/graphql/queries/coronavirus_landing_page.graphql | 4 ++-- app/graphql/queries/corporate_information_page.graphql | 4 ++-- app/graphql/queries/detailed_guide.graphql | 4 ++-- app/graphql/queries/document_collection.graphql | 4 ++-- app/graphql/queries/email_alert_signup.graphql | 4 ++-- app/graphql/queries/embassies_index.graphql | 4 ++-- app/graphql/queries/fatality_notice.graphql | 4 ++-- app/graphql/queries/field_of_operation.graphql | 4 ++-- app/graphql/queries/fields_of_operation.graphql | 4 ++-- app/graphql/queries/finder.graphql | 4 ++-- app/graphql/queries/finder_email_signup.graphql | 4 ++-- app/graphql/queries/generic.graphql | 4 ++-- app/graphql/queries/government.graphql | 4 ++-- app/graphql/queries/guide.graphql | 4 ++-- app/graphql/queries/help_page.graphql | 4 ++-- app/graphql/queries/historic_appointment.graphql | 4 ++-- app/graphql/queries/historic_appointments.graphql | 4 ++-- app/graphql/queries/history.graphql | 4 ++-- app/graphql/queries/hmrc_manual.graphql | 4 ++-- app/graphql/queries/hmrc_manual_section.graphql | 4 ++-- app/graphql/queries/homepage.graphql | 4 ++-- app/graphql/queries/how_government_works.graphql | 4 ++-- app/graphql/queries/html_publication.graphql | 4 ++-- app/graphql/queries/landing_page.graphql | 4 ++-- app/graphql/queries/local_transaction.graphql | 4 ++-- app/graphql/queries/mainstream_browse_page.graphql | 4 ++-- app/graphql/queries/manual.graphql | 4 ++-- app/graphql/queries/manual_section.graphql | 4 ++-- app/graphql/queries/ministers_index.graphql | 4 ++-- app/graphql/queries/news_article.graphql | 4 ++-- app/graphql/queries/organisation.graphql | 4 ++-- app/graphql/queries/organisations_homepage.graphql | 4 ++-- app/graphql/queries/person.graphql | 4 ++-- app/graphql/queries/place.graphql | 4 ++-- app/graphql/queries/publication.graphql | 4 ++-- app/graphql/queries/role.graphql | 4 ++-- app/graphql/queries/service_manual_guide.graphql | 4 ++-- app/graphql/queries/service_manual_homepage.graphql | 4 ++-- app/graphql/queries/service_manual_service_standard.graphql | 4 ++-- app/graphql/queries/service_manual_service_toolkit.graphql | 4 ++-- app/graphql/queries/service_manual_topic.graphql | 4 ++-- app/graphql/queries/simple_smart_answer.graphql | 4 ++-- app/graphql/queries/smart_answer.graphql | 4 ++-- app/graphql/queries/special_route.graphql | 4 ++-- app/graphql/queries/specialist_document.graphql | 4 ++-- app/graphql/queries/speech.graphql | 4 ++-- app/graphql/queries/statistical_data_set.graphql | 4 ++-- app/graphql/queries/statistics_announcement.graphql | 4 ++-- app/graphql/queries/step_by_step_nav.graphql | 4 ++-- app/graphql/queries/taxon.graphql | 4 ++-- app/graphql/queries/topical_event.graphql | 4 ++-- app/graphql/queries/topical_event_about_page.graphql | 4 ++-- app/graphql/queries/transaction.graphql | 4 ++-- app/graphql/queries/travel_advice.graphql | 4 ++-- app/graphql/queries/travel_advice_index.graphql | 4 ++-- app/graphql/queries/working_group.graphql | 4 ++-- app/graphql/queries/world_index.graphql | 4 ++-- app/graphql/queries/world_location_news.graphql | 4 ++-- .../queries/worldwide_corporate_information_page.graphql | 4 ++-- app/graphql/queries/worldwide_office.graphql | 4 ++-- app/graphql/queries/worldwide_organisation.graphql | 4 ++-- 67 files changed, 134 insertions(+), 134 deletions(-) diff --git a/app/graphql/queries/answer.graphql b/app/graphql/queries/answer.graphql index f74b9decba..d60832a8d9 100644 --- a/app/graphql/queries/answer.graphql +++ b/app/graphql/queries/answer.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query answer($base_path: String!) { - edition(base_path: $base_path) { +query answer($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/calendar.graphql b/app/graphql/queries/calendar.graphql index b6cd27ecb6..a60eb37f42 100644 --- a/app/graphql/queries/calendar.graphql +++ b/app/graphql/queries/calendar.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query calendar($base_path: String!) { - edition(base_path: $base_path) { +query calendar($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/call_for_evidence.graphql b/app/graphql/queries/call_for_evidence.graphql index 178b90e0c1..17f9b5ebf5 100644 --- a/app/graphql/queries/call_for_evidence.graphql +++ b/app/graphql/queries/call_for_evidence.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query call_for_evidence($base_path: String!) { - edition(base_path: $base_path) { +query call_for_evidence($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/case_study.graphql b/app/graphql/queries/case_study.graphql index ceb40deea5..6712f7f41f 100644 --- a/app/graphql/queries/case_study.graphql +++ b/app/graphql/queries/case_study.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query case_study($base_path: String!) { - edition(base_path: $base_path) { +query case_study($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/completed_transaction.graphql b/app/graphql/queries/completed_transaction.graphql index 1a83ec4ce9..b87b340d9c 100644 --- a/app/graphql/queries/completed_transaction.graphql +++ b/app/graphql/queries/completed_transaction.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query completed_transaction($base_path: String!) { - edition(base_path: $base_path) { +query completed_transaction($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/consultation.graphql b/app/graphql/queries/consultation.graphql index 015d0b9795..76aaacc32c 100644 --- a/app/graphql/queries/consultation.graphql +++ b/app/graphql/queries/consultation.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query consultation($base_path: String!) { - edition(base_path: $base_path) { +query consultation($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/coronavirus_landing_page.graphql b/app/graphql/queries/coronavirus_landing_page.graphql index f0559d2f22..c92fef73df 100644 --- a/app/graphql/queries/coronavirus_landing_page.graphql +++ b/app/graphql/queries/coronavirus_landing_page.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query coronavirus_landing_page($base_path: String!) { - edition(base_path: $base_path) { +query coronavirus_landing_page($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/corporate_information_page.graphql b/app/graphql/queries/corporate_information_page.graphql index cd20efea5d..5693664cc1 100644 --- a/app/graphql/queries/corporate_information_page.graphql +++ b/app/graphql/queries/corporate_information_page.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query corporate_information_page($base_path: String!) { - edition(base_path: $base_path) { +query corporate_information_page($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/detailed_guide.graphql b/app/graphql/queries/detailed_guide.graphql index 339f03b827..f5c94dff3f 100644 --- a/app/graphql/queries/detailed_guide.graphql +++ b/app/graphql/queries/detailed_guide.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query detailed_guide($base_path: String!) { - edition(base_path: $base_path) { +query detailed_guide($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/document_collection.graphql b/app/graphql/queries/document_collection.graphql index 0c08944972..5daa75d796 100644 --- a/app/graphql/queries/document_collection.graphql +++ b/app/graphql/queries/document_collection.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query document_collection($base_path: String!) { - edition(base_path: $base_path) { +query document_collection($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/email_alert_signup.graphql b/app/graphql/queries/email_alert_signup.graphql index bf56337849..69d8f144f5 100644 --- a/app/graphql/queries/email_alert_signup.graphql +++ b/app/graphql/queries/email_alert_signup.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query email_alert_signup($base_path: String!) { - edition(base_path: $base_path) { +query email_alert_signup($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/embassies_index.graphql b/app/graphql/queries/embassies_index.graphql index f0170a216c..c655df52af 100644 --- a/app/graphql/queries/embassies_index.graphql +++ b/app/graphql/queries/embassies_index.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query embassies_index($base_path: String!) { - edition(base_path: $base_path) { +query embassies_index($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/fatality_notice.graphql b/app/graphql/queries/fatality_notice.graphql index 89ce0b4870..42aefa75da 100644 --- a/app/graphql/queries/fatality_notice.graphql +++ b/app/graphql/queries/fatality_notice.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query fatality_notice($base_path: String!) { - edition(base_path: $base_path) { +query fatality_notice($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/field_of_operation.graphql b/app/graphql/queries/field_of_operation.graphql index e67bd29eec..c3c8801b90 100644 --- a/app/graphql/queries/field_of_operation.graphql +++ b/app/graphql/queries/field_of_operation.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query field_of_operation($base_path: String!) { - edition(base_path: $base_path) { +query field_of_operation($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/fields_of_operation.graphql b/app/graphql/queries/fields_of_operation.graphql index b2d1821c12..e498ca3f05 100644 --- a/app/graphql/queries/fields_of_operation.graphql +++ b/app/graphql/queries/fields_of_operation.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query fields_of_operation($base_path: String!) { - edition(base_path: $base_path) { +query fields_of_operation($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/finder.graphql b/app/graphql/queries/finder.graphql index 914395cc72..8dc8f28c95 100644 --- a/app/graphql/queries/finder.graphql +++ b/app/graphql/queries/finder.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query finder($base_path: String!) { - edition(base_path: $base_path) { +query finder($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/finder_email_signup.graphql b/app/graphql/queries/finder_email_signup.graphql index 04b8d704e8..224a1f34d2 100644 --- a/app/graphql/queries/finder_email_signup.graphql +++ b/app/graphql/queries/finder_email_signup.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query finder_email_signup($base_path: String!) { - edition(base_path: $base_path) { +query finder_email_signup($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/generic.graphql b/app/graphql/queries/generic.graphql index e7cc862226..cff68bbfbf 100644 --- a/app/graphql/queries/generic.graphql +++ b/app/graphql/queries/generic.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query generic($base_path: String!) { - edition(base_path: $base_path) { +query generic($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/government.graphql b/app/graphql/queries/government.graphql index 2a66645926..3affb97d4c 100644 --- a/app/graphql/queries/government.graphql +++ b/app/graphql/queries/government.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query government($base_path: String!) { - edition(base_path: $base_path) { +query government($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/guide.graphql b/app/graphql/queries/guide.graphql index a77e7fe6d8..c92dc18fbe 100644 --- a/app/graphql/queries/guide.graphql +++ b/app/graphql/queries/guide.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query guide($base_path: String!) { - edition(base_path: $base_path) { +query guide($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/help_page.graphql b/app/graphql/queries/help_page.graphql index e6d9332ffb..563b65eda3 100644 --- a/app/graphql/queries/help_page.graphql +++ b/app/graphql/queries/help_page.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query help_page($base_path: String!) { - edition(base_path: $base_path) { +query help_page($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/historic_appointment.graphql b/app/graphql/queries/historic_appointment.graphql index 041c79b2b4..76a6428386 100644 --- a/app/graphql/queries/historic_appointment.graphql +++ b/app/graphql/queries/historic_appointment.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query historic_appointment($base_path: String!) { - edition(base_path: $base_path) { +query historic_appointment($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/historic_appointments.graphql b/app/graphql/queries/historic_appointments.graphql index 0d676ecff4..f04de9374c 100644 --- a/app/graphql/queries/historic_appointments.graphql +++ b/app/graphql/queries/historic_appointments.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query historic_appointments($base_path: String!) { - edition(base_path: $base_path) { +query historic_appointments($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/history.graphql b/app/graphql/queries/history.graphql index 820bd34c71..00b111bf6c 100644 --- a/app/graphql/queries/history.graphql +++ b/app/graphql/queries/history.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query history($base_path: String!) { - edition(base_path: $base_path) { +query history($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/hmrc_manual.graphql b/app/graphql/queries/hmrc_manual.graphql index 9fff912130..44b27ffd54 100644 --- a/app/graphql/queries/hmrc_manual.graphql +++ b/app/graphql/queries/hmrc_manual.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query hmrc_manual($base_path: String!) { - edition(base_path: $base_path) { +query hmrc_manual($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/hmrc_manual_section.graphql b/app/graphql/queries/hmrc_manual_section.graphql index 141f59a892..24433ec7c1 100644 --- a/app/graphql/queries/hmrc_manual_section.graphql +++ b/app/graphql/queries/hmrc_manual_section.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query hmrc_manual_section($base_path: String!) { - edition(base_path: $base_path) { +query hmrc_manual_section($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/homepage.graphql b/app/graphql/queries/homepage.graphql index 7521938b48..7c14f50416 100644 --- a/app/graphql/queries/homepage.graphql +++ b/app/graphql/queries/homepage.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query homepage($base_path: String!) { - edition(base_path: $base_path) { +query homepage($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/how_government_works.graphql b/app/graphql/queries/how_government_works.graphql index 3666e2777a..5a3197e5e9 100644 --- a/app/graphql/queries/how_government_works.graphql +++ b/app/graphql/queries/how_government_works.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query how_government_works($base_path: String!) { - edition(base_path: $base_path) { +query how_government_works($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/html_publication.graphql b/app/graphql/queries/html_publication.graphql index e88226dcb1..1f7508b92d 100644 --- a/app/graphql/queries/html_publication.graphql +++ b/app/graphql/queries/html_publication.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query html_publication($base_path: String!) { - edition(base_path: $base_path) { +query html_publication($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/landing_page.graphql b/app/graphql/queries/landing_page.graphql index 6cea21fee4..0fa76d959a 100644 --- a/app/graphql/queries/landing_page.graphql +++ b/app/graphql/queries/landing_page.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query landing_page($base_path: String!) { - edition(base_path: $base_path) { +query landing_page($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/local_transaction.graphql b/app/graphql/queries/local_transaction.graphql index a21cd1c286..375b6d52ae 100644 --- a/app/graphql/queries/local_transaction.graphql +++ b/app/graphql/queries/local_transaction.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query local_transaction($base_path: String!) { - edition(base_path: $base_path) { +query local_transaction($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/mainstream_browse_page.graphql b/app/graphql/queries/mainstream_browse_page.graphql index f3af0fa5ae..1e813ed700 100644 --- a/app/graphql/queries/mainstream_browse_page.graphql +++ b/app/graphql/queries/mainstream_browse_page.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query mainstream_browse_page($base_path: String!) { - edition(base_path: $base_path) { +query mainstream_browse_page($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/manual.graphql b/app/graphql/queries/manual.graphql index 4c2286972d..5b9831b526 100644 --- a/app/graphql/queries/manual.graphql +++ b/app/graphql/queries/manual.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query manual($base_path: String!) { - edition(base_path: $base_path) { +query manual($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/manual_section.graphql b/app/graphql/queries/manual_section.graphql index 9f00614683..cfdd6cf0db 100644 --- a/app/graphql/queries/manual_section.graphql +++ b/app/graphql/queries/manual_section.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query manual_section($base_path: String!) { - edition(base_path: $base_path) { +query manual_section($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/ministers_index.graphql b/app/graphql/queries/ministers_index.graphql index a30e5b1953..6b7c60737e 100644 --- a/app/graphql/queries/ministers_index.graphql +++ b/app/graphql/queries/ministers_index.graphql @@ -1,5 +1,5 @@ -query ministers_index($base_path: String!) { - edition(base_path: $base_path) { +query ministers_index($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on MinistersIndex { base_path content_id diff --git a/app/graphql/queries/news_article.graphql b/app/graphql/queries/news_article.graphql index 94c34eab3f..a42bcf9d37 100644 --- a/app/graphql/queries/news_article.graphql +++ b/app/graphql/queries/news_article.graphql @@ -1,5 +1,5 @@ -query news_article($base_path: String!) { - edition(base_path: $base_path) { +query news_article($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { base_path content_id diff --git a/app/graphql/queries/organisation.graphql b/app/graphql/queries/organisation.graphql index bf59951823..0faa7b8b66 100644 --- a/app/graphql/queries/organisation.graphql +++ b/app/graphql/queries/organisation.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query organisation($base_path: String!) { - edition(base_path: $base_path) { +query organisation($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/organisations_homepage.graphql b/app/graphql/queries/organisations_homepage.graphql index 08de2b8479..d733ca7e72 100644 --- a/app/graphql/queries/organisations_homepage.graphql +++ b/app/graphql/queries/organisations_homepage.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query organisations_homepage($base_path: String!) { - edition(base_path: $base_path) { +query organisations_homepage($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/person.graphql b/app/graphql/queries/person.graphql index 5687877d93..4e515fdcf8 100644 --- a/app/graphql/queries/person.graphql +++ b/app/graphql/queries/person.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query person($base_path: String!) { - edition(base_path: $base_path) { +query person($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/place.graphql b/app/graphql/queries/place.graphql index 044f32f639..977034f0f1 100644 --- a/app/graphql/queries/place.graphql +++ b/app/graphql/queries/place.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query place($base_path: String!) { - edition(base_path: $base_path) { +query place($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/publication.graphql b/app/graphql/queries/publication.graphql index 9224b44fe4..5f4ace70d4 100644 --- a/app/graphql/queries/publication.graphql +++ b/app/graphql/queries/publication.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query publication($base_path: String!) { - edition(base_path: $base_path) { +query publication($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/role.graphql b/app/graphql/queries/role.graphql index 06bfee3854..b4eaeb28d3 100644 --- a/app/graphql/queries/role.graphql +++ b/app/graphql/queries/role.graphql @@ -1,5 +1,5 @@ -query role($base_path: String!) { - edition(base_path: $base_path) { +query role($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { base_path content_id diff --git a/app/graphql/queries/service_manual_guide.graphql b/app/graphql/queries/service_manual_guide.graphql index bc78b6a399..ba4a803f2b 100644 --- a/app/graphql/queries/service_manual_guide.graphql +++ b/app/graphql/queries/service_manual_guide.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query service_manual_guide($base_path: String!) { - edition(base_path: $base_path) { +query service_manual_guide($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/service_manual_homepage.graphql b/app/graphql/queries/service_manual_homepage.graphql index 5bcf79955b..6f58cbd221 100644 --- a/app/graphql/queries/service_manual_homepage.graphql +++ b/app/graphql/queries/service_manual_homepage.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query service_manual_homepage($base_path: String!) { - edition(base_path: $base_path) { +query service_manual_homepage($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/service_manual_service_standard.graphql b/app/graphql/queries/service_manual_service_standard.graphql index cf18646605..1a89443125 100644 --- a/app/graphql/queries/service_manual_service_standard.graphql +++ b/app/graphql/queries/service_manual_service_standard.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query service_manual_service_standard($base_path: String!) { - edition(base_path: $base_path) { +query service_manual_service_standard($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/service_manual_service_toolkit.graphql b/app/graphql/queries/service_manual_service_toolkit.graphql index c91b6978c6..d29351c152 100644 --- a/app/graphql/queries/service_manual_service_toolkit.graphql +++ b/app/graphql/queries/service_manual_service_toolkit.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query service_manual_service_toolkit($base_path: String!) { - edition(base_path: $base_path) { +query service_manual_service_toolkit($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/service_manual_topic.graphql b/app/graphql/queries/service_manual_topic.graphql index 5c19d1c2b0..979cc5e371 100644 --- a/app/graphql/queries/service_manual_topic.graphql +++ b/app/graphql/queries/service_manual_topic.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query service_manual_topic($base_path: String!) { - edition(base_path: $base_path) { +query service_manual_topic($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/simple_smart_answer.graphql b/app/graphql/queries/simple_smart_answer.graphql index 8f58fcaa3c..d35d1ad38d 100644 --- a/app/graphql/queries/simple_smart_answer.graphql +++ b/app/graphql/queries/simple_smart_answer.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query simple_smart_answer($base_path: String!) { - edition(base_path: $base_path) { +query simple_smart_answer($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/smart_answer.graphql b/app/graphql/queries/smart_answer.graphql index 5b5dafd2e9..cd023a70e2 100644 --- a/app/graphql/queries/smart_answer.graphql +++ b/app/graphql/queries/smart_answer.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query smart_answer($base_path: String!) { - edition(base_path: $base_path) { +query smart_answer($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/special_route.graphql b/app/graphql/queries/special_route.graphql index 0697dc2e52..2f685f3c25 100644 --- a/app/graphql/queries/special_route.graphql +++ b/app/graphql/queries/special_route.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query special_route($base_path: String!) { - edition(base_path: $base_path) { +query special_route($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/specialist_document.graphql b/app/graphql/queries/specialist_document.graphql index b641135a20..84f643861a 100644 --- a/app/graphql/queries/specialist_document.graphql +++ b/app/graphql/queries/specialist_document.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query specialist_document($base_path: String!) { - edition(base_path: $base_path) { +query specialist_document($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/speech.graphql b/app/graphql/queries/speech.graphql index 75bd371155..548a016982 100644 --- a/app/graphql/queries/speech.graphql +++ b/app/graphql/queries/speech.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query speech($base_path: String!) { - edition(base_path: $base_path) { +query speech($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/statistical_data_set.graphql b/app/graphql/queries/statistical_data_set.graphql index f9f3910068..93bfd94eb2 100644 --- a/app/graphql/queries/statistical_data_set.graphql +++ b/app/graphql/queries/statistical_data_set.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query statistical_data_set($base_path: String!) { - edition(base_path: $base_path) { +query statistical_data_set($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/statistics_announcement.graphql b/app/graphql/queries/statistics_announcement.graphql index e62261dca6..73bf290e52 100644 --- a/app/graphql/queries/statistics_announcement.graphql +++ b/app/graphql/queries/statistics_announcement.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query statistics_announcement($base_path: String!) { - edition(base_path: $base_path) { +query statistics_announcement($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/step_by_step_nav.graphql b/app/graphql/queries/step_by_step_nav.graphql index c63da15376..663fe893f8 100644 --- a/app/graphql/queries/step_by_step_nav.graphql +++ b/app/graphql/queries/step_by_step_nav.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query step_by_step_nav($base_path: String!) { - edition(base_path: $base_path) { +query step_by_step_nav($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/taxon.graphql b/app/graphql/queries/taxon.graphql index cc9131e996..1cdf741773 100644 --- a/app/graphql/queries/taxon.graphql +++ b/app/graphql/queries/taxon.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query taxon($base_path: String!) { - edition(base_path: $base_path) { +query taxon($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/topical_event.graphql b/app/graphql/queries/topical_event.graphql index 3cd577cb16..e1943a84c3 100644 --- a/app/graphql/queries/topical_event.graphql +++ b/app/graphql/queries/topical_event.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query topical_event($base_path: String!) { - edition(base_path: $base_path) { +query topical_event($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/topical_event_about_page.graphql b/app/graphql/queries/topical_event_about_page.graphql index 72ffb42bc2..f436fc4cbf 100644 --- a/app/graphql/queries/topical_event_about_page.graphql +++ b/app/graphql/queries/topical_event_about_page.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query topical_event_about_page($base_path: String!) { - edition(base_path: $base_path) { +query topical_event_about_page($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/transaction.graphql b/app/graphql/queries/transaction.graphql index 8e7d15dfca..0732d84811 100644 --- a/app/graphql/queries/transaction.graphql +++ b/app/graphql/queries/transaction.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query transaction($base_path: String!) { - edition(base_path: $base_path) { +query transaction($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/travel_advice.graphql b/app/graphql/queries/travel_advice.graphql index 5160577aee..7d1737d6ae 100644 --- a/app/graphql/queries/travel_advice.graphql +++ b/app/graphql/queries/travel_advice.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query travel_advice($base_path: String!) { - edition(base_path: $base_path) { +query travel_advice($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/travel_advice_index.graphql b/app/graphql/queries/travel_advice_index.graphql index e13cfff830..7015b19325 100644 --- a/app/graphql/queries/travel_advice_index.graphql +++ b/app/graphql/queries/travel_advice_index.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query travel_advice_index($base_path: String!) { - edition(base_path: $base_path) { +query travel_advice_index($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/working_group.graphql b/app/graphql/queries/working_group.graphql index 6a0876ef3b..743498de72 100644 --- a/app/graphql/queries/working_group.graphql +++ b/app/graphql/queries/working_group.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query working_group($base_path: String!) { - edition(base_path: $base_path) { +query working_group($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/world_index.graphql b/app/graphql/queries/world_index.graphql index 89e2d47fd0..ad9e2b7e6e 100644 --- a/app/graphql/queries/world_index.graphql +++ b/app/graphql/queries/world_index.graphql @@ -1,5 +1,5 @@ -query world_index($base_path: String!) { - edition(base_path: $base_path) { +query world_index($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { base_path content_id diff --git a/app/graphql/queries/world_location_news.graphql b/app/graphql/queries/world_location_news.graphql index 2e6ed0591a..783a84675b 100644 --- a/app/graphql/queries/world_location_news.graphql +++ b/app/graphql/queries/world_location_news.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query world_location_news($base_path: String!) { - edition(base_path: $base_path) { +query world_location_news($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/worldwide_corporate_information_page.graphql b/app/graphql/queries/worldwide_corporate_information_page.graphql index f7d5bfcf34..6b5297bab6 100644 --- a/app/graphql/queries/worldwide_corporate_information_page.graphql +++ b/app/graphql/queries/worldwide_corporate_information_page.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query worldwide_corporate_information_page($base_path: String!) { - edition(base_path: $base_path) { +query worldwide_corporate_information_page($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/worldwide_office.graphql b/app/graphql/queries/worldwide_office.graphql index 1245647fe8..c268faabef 100644 --- a/app/graphql/queries/worldwide_office.graphql +++ b/app/graphql/queries/worldwide_office.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query worldwide_office($base_path: String!) { - edition(base_path: $base_path) { +query worldwide_office($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path diff --git a/app/graphql/queries/worldwide_organisation.graphql b/app/graphql/queries/worldwide_organisation.graphql index 0aea615548..4afb54e535 100644 --- a/app/graphql/queries/worldwide_organisation.graphql +++ b/app/graphql/queries/worldwide_organisation.graphql @@ -12,8 +12,8 @@ fragment defaultFields on Edition { web_url } -query worldwide_organisation($base_path: String!) { - edition(base_path: $base_path) { +query worldwide_organisation($base_path: String!, $with_drafts: Boolean) { + edition(base_path: $base_path, with_drafts: $with_drafts) { ... on Edition { analytics_identifier base_path From 2399481bad9d29661c754219806568bf9c36414d Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 2 Jan 2026 14:43:17 +0000 Subject: [PATCH 14/20] Default EditionFinderService to live content_store All existing uses of this class specify content_store: "live". Note: the line that changed in the controller action resulted in an entry in the Brakeman ignore file needing updating. I ran `brakeman -I` to remove the old version and introduce the new version. --- app/controllers/graphql_controller.rb | 2 +- app/services/edition_finder_service.rb | 2 +- config/brakeman.ignore | 46 ++++++++++---------- spec/services/edition_finder_service_spec.rb | 2 +- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 03ea2f25c7..19f17060e4 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -15,7 +15,7 @@ def live_content execute_in_read_replica do begin encoded_base_path = Addressable::URI.encode("/#{params[:base_path]}") - edition = EditionFinderService.new(encoded_base_path, "live").find + edition = EditionFinderService.new(encoded_base_path).find set_cache_headers(edition) return head :not_found unless edition diff --git a/app/services/edition_finder_service.rb b/app/services/edition_finder_service.rb index dc672541c3..5625cbc18d 100644 --- a/app/services/edition_finder_service.rb +++ b/app/services/edition_finder_service.rb @@ -1,7 +1,7 @@ class EditionFinderService attr_reader :path, :content_store - def initialize(path, content_store) + def initialize(path, content_store = "live") @path = path @content_store = content_store end diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 8b1d93850f..6f31cddb25 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -23,29 +23,6 @@ ], "note": "The included SQL is a constant string with no user input." }, - { - "warning_type": "File Access", - "warning_code": 16, - "fingerprint": "fd291054c7ec05ad94a610720679b4679e738f04204e1993fdeda694ee7c5f1d", - "check_name": "FileAccess", - "message": "Parameter value used in file name", - "file": "app/controllers/graphql_controller.rb", - "line": 28, - "link": "https://brakemanscanner.org/docs/warning_types/file_access/", - "code": "File.read(Rails.root.join(\"app/graphql/queries/#{EditionFinderService.new(Addressable::URI.encode(\"/#{params[:base_path]}\"), \"live\").find.schema_name}.graphql\"))", - "render_path": null, - "location": { - "type": "method", - "class": "GraphqlController", - "method": "live_content" - }, - "user_input": "params[:base_path]", - "confidence": "Weak", - "cwe_id": [ - 22 - ], - "note": "" - }, { "warning_type": "SQL Injection", "warning_code": 0, @@ -69,6 +46,29 @@ ], "note": "This SQL is generated using valid content IDs and configuration data, not with arbitrary user input" }, + { + "warning_type": "File Access", + "warning_code": 16, + "fingerprint": "8bb962377b1903768a01fad99f2970a86d362bc80ab38bb1327411d6b8572e55", + "check_name": "FileAccess", + "message": "Parameter value used in file name", + "file": "app/controllers/graphql_controller.rb", + "line": 28, + "link": "https://brakemanscanner.org/docs/warning_types/file_access/", + "code": "File.read(Rails.root.join(\"app/graphql/queries/#{EditionFinderService.new(Addressable::URI.encode(\"/#{params[:base_path]}\")).find.schema_name}.graphql\"))", + "render_path": null, + "location": { + "type": "method", + "class": "GraphqlController", + "method": "live_content" + }, + "user_input": "params[:base_path]", + "confidence": "Weak", + "cwe_id": [ + 22 + ], + "note": "" + }, { "warning_type": "SQL Injection", "warning_code": 0, diff --git a/spec/services/edition_finder_service_spec.rb b/spec/services/edition_finder_service_spec.rb index 0ff3eec3ad..ea296f9633 100644 --- a/spec/services/edition_finder_service_spec.rb +++ b/spec/services/edition_finder_service_spec.rb @@ -14,7 +14,7 @@ end describe ".find" do - subject { described_class.new(request_path, "live").find } + subject { described_class.new(request_path).find } context "when there isn't an item matching the path" do let(:request_path) { "/path" } From 014d670c13edeba7bb183d65b0316c06e4965373 Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Fri, 2 Jan 2026 17:03:57 +0000 Subject: [PATCH 15/20] Find editions by base_path and with_drafts flag This flag replaces the content_store parameter. With `content_store: "live"` this service always returned a live edition or no edition at all. That's the same behaviour as `with_drafts: false`. With `content_store: "draft"`, this service always returned a draft edition or no edition. `with_drafts: true` doesn't behave like that exactly, it can return a draft edition or a live edition or no edition, with that order of precedence. I don't think there's any need to compare a published edition with an unpublished edition at any point in this service. For base_paths that match edition records exactly, there can't be more than 1 record with that same base_path and `content_store: "live"`. So I believe we're expecting 0 or 1 draft editions and 0 or 1 live editions. This service then does its own sorting once it gets into the routes and redirects, so I *think* any more sorting by state or content_store precedence is moot. I'm hoping that the matching rules should take precedence over them, anyway, but it might be possible that we need to modify this code to group pairs of sibling draft and live editions? Co-authored-by: Ynda Jas --- app/services/edition_finder_service.rb | 27 ++++++++-- spec/services/edition_finder_service_spec.rb | 54 ++++++++++++++++++-- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/app/services/edition_finder_service.rb b/app/services/edition_finder_service.rb index 5625cbc18d..3de9ccaaba 100644 --- a/app/services/edition_finder_service.rb +++ b/app/services/edition_finder_service.rb @@ -1,13 +1,30 @@ class EditionFinderService - attr_reader :path, :content_store + attr_reader :path, :content_stores - def initialize(path, content_store = "live") + def initialize(path, with_drafts: false) @path = path - @content_store = content_store + @content_stores = if with_drafts + %i[draft live] + else + %i[live] + end end def find - exact_match = scope.find_by(base_path: path) + exact_match = scope + .where(base_path: path) + .order( + Arel.sql( + <<~SQL, + CASE editions.content_store + WHEN 'draft' THEN 0 + WHEN 'live' THEN 1 + ELSE 2 + END + SQL + ), + ) + .first return exact_match if exact_match if route_matches.present? @@ -18,7 +35,7 @@ def find private def scope - Edition.where(content_store:) + Edition.where(content_store: @content_stores) end def route_matches diff --git a/spec/services/edition_finder_service_spec.rb b/spec/services/edition_finder_service_spec.rb index ea296f9633..e12009b46e 100644 --- a/spec/services/edition_finder_service_spec.rb +++ b/spec/services/edition_finder_service_spec.rb @@ -10,7 +10,7 @@ end before do - @edition = create(:live_edition, base_path:, routes:, redirects:) + @live = @edition = create(:live_edition, base_path:, routes:, redirects:) end describe ".find" do @@ -22,10 +22,58 @@ it { is_expected.to be_nil } end - context "when there is a base_path that matches the path" do + context "when there are base_paths that match the path" do let(:request_path) { "/base-path" } - it { is_expected.to eq @edition } + context "with_drafts=true" do + subject { described_class.new(request_path, with_drafts: true).find } + + context "when there are both draft and live base_path matches" do + before do + @draft = create(:draft_edition, base_path:, routes:, redirects:) + end + + it { is_expected.to eq @draft } + end + + context "when there's only a draft base_path match" do + before do + @live.destroy! + + @draft = create(:draft_edition, base_path:, routes:, redirects:) + end + + it { is_expected.to eq @draft } + end + + context "when there's only a live base_path match" do + it { is_expected.to eq @live } + end + end + + context "with_drafts=false" do + context "when there are both draft and live base_path matches" do + before do + @draft = create(:draft_edition, base_path:, routes:, redirects:) + end + + it { is_expected.to eq @live } + end + + context "when there's only a draft base_path match" do + before do + @live.destroy! + + @draft = create(:draft_edition, base_path:, routes:, redirects:) + end + + it { is_expected.to be_nil } + end + + context "when there's only a live base_path match" do + it { is_expected.to eq @live } + end + end end context "when there is a matching exact route for the path" do From 02b9372186760e4d0a053800b171f0d09664fa19 Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Thu, 5 Mar 2026 11:53:50 +0000 Subject: [PATCH 16/20] Use shared examples in GraphQL controller spec In a subsequent commit we'll introduce a draft content action which will duplicate most of the behaviour of the live content action, so this makes it easier to extend to other actions by wrapping most of the behaviour in shared examples --- spec/controllers/graphql_controller_spec.rb | 324 ++++++++++---------- 1 file changed, 166 insertions(+), 158 deletions(-) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index f430b69d4f..06ec298049 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -1,110 +1,82 @@ RSpec.describe GraphqlController do - describe "#live_content" do - shared_examples "a response with default public cache headers" do - it "sets cache headers to expire in the default TTL" do - expect(cache_control["max-age"]).to eq(default_ttl.to_s) - end - - it "sets a cache-control directive of public" do - expect(cache_control["public"]).to eq(true) - end + shared_examples "a response with default public cache headers" do + it "sets cache headers to expire in the default TTL" do + expect(cache_control["max-age"]).to eq(default_ttl.to_s) end - context "when the requested base_path has live content" do - let(:edition) do - create( - :live_edition, - schema_name: "news_article", - document_type: "news_story", - details: { - "body" => "Some content", - }, - ) - end + it "sets a cache-control directive of public" do + expect(cache_control["public"]).to eq(true) + end + end - let(:request_path) { base_path_without_leading_slash(edition.base_path) } + shared_examples "a content endpoint with a matching edition" do |content_store| + let(:edition) { create(edition_factory, **edition_properties) } + let(:request_path) { base_path_without_leading_slash(edition.base_path) } - before do - get :live_content, params: { base_path: request_path } - end + before do + edition - it "returns a 200 OK response" do - expect(response.status).to eq(200) - end + get action, params: { base_path: request_path } + end - it "returns the content item as JSON data" do - expect(response.media_type).to eq("application/json") - end + it "returns a 200 OK response" do + expect(response.status).to eq(200) + end - it "sets document_type and schema_type as prometheus labels" do - expect(request.env.dig("govuk.prometheus_labels", "document_type")).to eq(edition.document_type) - expect(request.env.dig("govuk.prometheus_labels", "schema_name")).to eq(edition.schema_name) - end + it "returns the content item as JSON data" do + expect(response.media_type).to eq("application/json") + end - it_behaves_like "a response with default public cache headers" + it "sets document_type and schema_type as prometheus labels" do + expect(request.env.dig("govuk.prometheus_labels", "document_type")).to eq(edition.document_type) + expect(request.env.dig("govuk.prometheus_labels", "schema_name")).to eq(edition.schema_name) + end - context "but the requested route does not match the base_path" do - let(:edition) do - create( - :live_edition, - base_path: "/base-path", - document_type: "news_story", - routes: [ - { path: "/base-path", type: "exact" }, - { path: "/base-path/exact", type: "exact" }, - ], - schema_name: "news_article", - ) - end + it "defers to a service to process the result into a content item" do + edition = create(edition_factory, + schema_name: "news_article", + document_type: "news_story", + details: { "body" => "Some content" }) - let(:request_path) { base_path_without_leading_slash(edition.routes.second[:path]) } + allow_any_instance_of(GraphqlContentItemService) + .to receive(:process) + .and_return({ "details" => { "something" => "jason!" } }) - it "returns a 303 See Other response" do - expect(response.status).to eq(303) - end + # we have a redundant request in the before block for this example + get action, params: { + base_path: base_path_without_leading_slash(edition.base_path), + } - it "returns a redirect to the item by base_path" do - expect(response).to redirect_to("/graphql/content/base-path") - end - end + expect(JSON.parse(response.body)).to eq({ + "details" => { "something" => "jason!" }, + }) end - context "when the requested base_path only has draft content" do + it_behaves_like "a response with default public cache headers" + + context "when the edition has a max cache time" do let(:edition) do create( - :draft_edition, - schema_name: "person", - document_type: "person", - details: { - "body" => "Some content", - }, + edition_factory, + **edition_properties, + details: { max_cache_time: 10 }, ) end - let(:request_path) { base_path_without_leading_slash(edition.base_path) } - - before do - get :live_content, params: { base_path: request_path } - end - - it "returns a 404 Not Found response" do - expect(response.status).to eq(404) - end - - it "doesn't return any content item data" do - expect(response.body).not_to be_present + it "sets cache headers based on the edition's max cache time value" do + expect(cache_control["max-age"]).to eq("10") end end - context "a content item with a non-ASCII base_path" do - before(:each) do + context "when the edition has a non-ASCII base_path" do + let(:edition) do create( - :live_edition, + edition_factory, + **edition_properties, base_path: "/news/%D7%91%D7%95%D7%98%20%D7%9C%D7%90%D7%99%D7%A0%D7%93", - schema_name: "news_article", ) - get :live_content, params: { base_path: "news/בוט לאינד" } end + let(:request_path) { "news/בוט לאינד" } it "returns a 200 OK response" do expect(response.status).to eq(200) @@ -115,36 +87,40 @@ end end - context "a content item with an invalid path" do - it "returns a 400 Bad Request response" do - # we can't run the test with an actual invalid URI so we have to mock that - expect(Addressable::URI).to receive(:encode).and_wrap_original do |m| - m.call("/path\nprotocol:") - end - get :live_content, params: { base_path: "content/invalid-uri" } - expect(response.status).to eq(400) - end - end - - context "when the requested base_path is not a format supported by GraphQL" do + context "when the requested route does not match the base_path" do let(:edition) do create( - :live_edition, - schema_name: "get_involved", + edition_factory, + **edition_properties, + base_path: "/base-path", + routes: [ + { path: "/base-path", type: "exact" }, + { path: "/base-path/exact", type: "exact" }, + ], ) end + let(:request_path) { base_path_without_leading_slash(edition.routes.second[:path]) } - before do - get :live_content, params: { base_path: base_path_without_leading_slash(edition.base_path) } + it "returns a 303 See Other response" do + expect(response.status).to eq(303) end - it "returns a 404 not_found response" do - expect(response.status).to eq(404) + it "returns a redirect to the item by base_path" do + redirect_path = case action + when :draft_content + "/graphql/draft/base-path" + when :live_content + "/graphql/content/base-path" + end + + expect(response).to redirect_to(redirect_path) end + + it_behaves_like "a response with default public cache headers" end - context "a non-existent content item" do - before(:each) { get :live_content, params: { base_path: "unknown-content" } } + context "when the edition is of a schema unsupported by GraphQL" do + let(:edition) { create(edition_factory, schema_name: "get_involved") } it "returns a 404 Not Found response" do expect(response.status).to eq(404) @@ -153,87 +129,119 @@ it_behaves_like "a response with default public cache headers" end - context "a gone content item without an explanation and without an alternative_path" do - let(:edition) do - create( - :gone_unpublished_edition_without_explanation, - schema_name: "news_article", - ) - end + if content_store == :live + context "when the edition is gone without an explanation or alternative_path" do + let(:edition) do + create( + :gone_unpublished_edition_without_explanation, + schema_name: "news_article", + ) + end - before do - get :live_content, params: { base_path: base_path_without_leading_slash(edition.base_path) } + it "responds with 410" do + expect(response.status).to eq(410) + end + + it_behaves_like "a response with default public cache headers" end - it "responds with 410" do - expect(response.status).to eq(410) + context "when the edition is gone with an explanation and alternative_path" do + let(:edition) do + create( + :gone_unpublished_edition, + schema_name: "news_article", + ) + end + + it "responds with 200" do + expect(response.status).to eq(200) + end + + it "includes the details" do + details = JSON.parse(response.body)["details"] + expect(details["explanation"]).to eq(edition.unpublishing.explanation) + expect(details["alternative_path"]).to eq(edition.unpublishing.alternative_path) + end + + it_behaves_like "a response with default public cache headers" end + end + end - it_behaves_like "a response with default public cache headers" + shared_examples "a content endpoint" do + it "defers to a service to find the correct edition" do + expect_any_instance_of(EditionFinderService).to receive(:find) + + get :live_content, params: { + base_path: base_path_without_leading_slash("/base-path"), + } end - context "a gone content item with an explanation and alternative_path" do - let(:edition) do - create( - :gone_unpublished_edition, - schema_name: "news_article", - ) - end + context "when there's no matching edition" do + before(:each) { get action, params: { base_path: "unknown-content" } } - before do - get :live_content, params: { base_path: base_path_without_leading_slash(edition.base_path) } + it "returns a 404 Not Found response" do + expect(response.status).to eq(404) end - it "responds with 200" do - expect(response.status).to eq(200) - end + it_behaves_like "a response with default public cache headers" + end - it "includes the details" do - details = JSON.parse(response.body)["details"] - expect(details["explanation"]).to eq(edition.unpublishing.explanation) - expect(details["alternative_path"]).to eq(edition.unpublishing.alternative_path) + context "when the requested base path is invalid" do + it "returns a 400 Bad Request response" do + # we can't run the test with an actual invalid URI so we have to mock that + expect(Addressable::URI).to receive(:encode).and_wrap_original do |m| + m.call("/path\nprotocol:") + end + get action, params: { base_path: "content/invalid-uri" } + expect(response.status).to eq(400) end end + end - it "defers to a service to process the result into a content item" do - edition = create(:live_edition, - schema_name: "news_article", - document_type: "news_story", - details: { - "body" => "Some content", - }) + describe "#live_content" do + let(:action) { :live_content } - allow_any_instance_of(GraphqlContentItemService) - .to receive(:process) - .and_return({ "details" => { "something" => "jason!" } }) + it_behaves_like "a content endpoint" - get :live_content, params: { - base_path: base_path_without_leading_slash(edition.base_path), - } + context "when the requested base_path has live content" do + let(:edition_factory) { :live_edition } + let(:edition_properties) do + { + schema_name: "news_article", + document_type: "news_story", + details: { "body" => "Some content" }, + } + end - expect(JSON.parse(response.body)).to eq({ - "details" => { "something" => "jason!" }, - }) + it_behaves_like "a content endpoint with a matching edition", :live end - it "defers to a service to find the correct edition" do - expect_any_instance_of(EditionFinderService).to receive(:find) + context "when the requested base_path only has draft content" do + let(:edition) do + create( + :draft_edition, + schema_name: "person", + document_type: "person", + details: { + "body" => "Some content", + }, + ) + end - get :live_content, params: { - base_path: base_path_without_leading_slash("/base-path"), - } - end + let(:request_path) { base_path_without_leading_slash(edition.base_path) } - it "sets cache headers based on the edition's max cache time value" do - edition = create( - :live_edition, - schema_name: "specialist_document", - details: { max_cache_time: 10 }, - ) + before do + get :live_content, params: { base_path: request_path } + end - get :live_content, params: { base_path: base_path_without_leading_slash(edition.base_path) } + it "returns a 404 Not Found response" do + expect(response.status).to eq(404) + end - expect(cache_control["max-age"]).to eq("10") + it "doesn't return any content item data" do + expect(response.body).not_to be_present + end end end From 1d4d6bb99feef8a4ba625a243de8514ed293e30d Mon Sep 17 00:00:00 2001 From: Mike Patrick Date: Thu, 18 Dec 2025 11:38:34 +0000 Subject: [PATCH 17/20] Introduce /graphql/draft draft content endpoint Which behaves a lot like the /graphql/content live content endpoint, only with `with_drafts: true`. I've left all the duplication here because it's tricky to see where to begin with that. If we settle on having 2 separate controller actions like this, then we can spend the time to make it easier and less error prone to work with. One consequence of the duplication is that it includes another file access Brakeman warning to ignore (as the code that we duplicated did). I've introduced a spec example here that demonstrates how this endpoint's use of with_drafts=true enables a live edition to link to a draft edition and vice versa. I've put it in its own separate integration spec because it seems less focussed on endpoint behaviour than the controller specs since it covers QueryType and dataloader behaviour. Co-authored-by: Ynda Jas --- app/controllers/graphql_controller.rb | 41 +++++++++++++++ config/brakeman.ignore | 25 ++++++++- config/routes.rb | 1 + spec/controllers/graphql_controller_spec.rb | 32 ++++++++++++ .../integration/graphql/draft_content_spec.rb | 52 +++++++++++++++++++ 5 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 spec/integration/graphql/draft_content_spec.rb diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 19f17060e4..34a71c52fa 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -11,6 +11,47 @@ class GraphqlController < ApplicationController DEFAULT_TTL = ENV.fetch("DEFAULT_TTL", 5.minutes).to_i.seconds MINIMUM_TTL = [DEFAULT_TTL, 5.seconds].min + def draft_content + execute_in_read_replica do + begin + encoded_base_path = Addressable::URI.encode("/#{params[:base_path]}") + edition = EditionFinderService.new(encoded_base_path, with_drafts: true).find + set_cache_headers(edition) + return head :not_found unless edition + + if edition.base_path != encoded_base_path + return redirect_to graphql_draft_content_path(base_path: edition.base_path.gsub(/^\//, "")), status: :see_other + end + + begin + query = File.read(Rails.root.join("app/graphql/queries/#{edition.schema_name}.graphql")) + rescue Errno::ENOENT + return head :not_found + end + + result = PublishingApiSchema.execute(query, variables: { base_path: encoded_base_path, with_drafts: true }).to_hash + report_result(result) + + content_item = GraphqlContentItemService.for_edition(edition).process(result) + + http_status = if content_item["schema_name"] == "gone" && (content_item["details"].nil? || content_item["details"].values.reject(&:blank?).empty?) + 410 + else + 200 + end + + render json: content_item, status: http_status + end + rescue Addressable::URI::InvalidURIError + Rails.logger.warn "Can't encode request_path '#{params[:base_path]}'" + return head :bad_request + rescue StandardError => e + raise e unless Rails.env.development? + + handle_error_in_development(e) + end + end + def live_content execute_in_read_replica do begin diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 6f31cddb25..0245a69cca 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -53,7 +53,7 @@ "check_name": "FileAccess", "message": "Parameter value used in file name", "file": "app/controllers/graphql_controller.rb", - "line": 28, + "line": 70, "link": "https://brakemanscanner.org/docs/warning_types/file_access/", "code": "File.read(Rails.root.join(\"app/graphql/queries/#{EditionFinderService.new(Addressable::URI.encode(\"/#{params[:base_path]}\")).find.schema_name}.graphql\"))", "render_path": null, @@ -69,6 +69,29 @@ ], "note": "" }, + { + "warning_type": "File Access", + "warning_code": 16, + "fingerprint": "caea2a7c4a7bdd253244a55991bdb9f5ef30073e70834adccefbee128af05cb5", + "check_name": "FileAccess", + "message": "Parameter value used in file name", + "file": "app/controllers/graphql_controller.rb", + "line": 28, + "link": "https://brakemanscanner.org/docs/warning_types/file_access/", + "code": "File.read(Rails.root.join(\"app/graphql/queries/#{EditionFinderService.new(Addressable::URI.encode(\"/#{params[:base_path]}\"), :with_drafts => true).find.schema_name}.graphql\"))", + "render_path": null, + "location": { + "type": "method", + "class": "GraphqlController", + "method": "draft_content" + }, + "user_input": "params[:base_path]", + "confidence": "Weak", + "cwe_id": [ + 22 + ], + "note": "" + }, { "warning_type": "SQL Injection", "warning_code": 0, diff --git a/config/routes.rb b/config/routes.rb index 1bba5ff136..35078ce186 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,6 +15,7 @@ def content_id_constraint(request) post "/lookup-by-base-path", to: "lookups#by_base_path" + get "/graphql/draft(/*base_path)" => "graphql#draft_content", as: :graphql_draft_content get "/graphql/content(/*base_path)" => "graphql#live_content", as: :graphql_live_content post "/graphql", to: "graphql#execute" diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index 06ec298049..573e4d2320 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -199,6 +199,38 @@ end end + describe "#draft_content" do + let(:action) { :draft_content } + + it_behaves_like "a content endpoint" + + context "when the requested base_path has draft content" do + let(:edition_factory) { :draft_edition } + let(:edition_properties) do + { + schema_name: "person", + document_type: "person", + details: { "body" => "Some content" }, + } + end + + it_behaves_like "a content endpoint with a matching edition", :draft + end + + context "when the requested base_path only has live content" do + let(:edition_factory) { :live_edition } + let(:edition_properties) do + { + schema_name: "news_article", + document_type: "news_story", + details: { "body" => "Some content" }, + } + end + + it_behaves_like "a content endpoint with a matching edition", :live + end + end + describe "#live_content" do let(:action) { :live_content } diff --git a/spec/integration/graphql/draft_content_spec.rb b/spec/integration/graphql/draft_content_spec.rb new file mode 100644 index 0000000000..60f2d86085 --- /dev/null +++ b/spec/integration/graphql/draft_content_spec.rb @@ -0,0 +1,52 @@ +RSpec.describe "Requesting draft content by base path" do + context "when not all content is available as a draft" do + it "will link from live editions to draft editions and vice versa" do + level_2_target_edition = create(:live_edition, title: "level 2 edition 1, live") + + level_1_target_document = create(:document) + create( + :live_edition, + title: "level 1 edition 1, live", + document: level_1_target_document, + ) + create( + :draft_edition, + title: "level 1 edition 2, draft", + document: level_1_target_document, + link_set_links: [ + { link_type: "parent_taxons", target_content_id: level_2_target_edition.content_id }, + ], + ) + + edition = create( + :live_edition, + title: "root edition, live", + edition_links: [ + { link_type: "taxons", target_content_id: level_1_target_document.content_id }, + ], + ) + + get "/graphql/draft/#{edition.base_path}" + + parsed_response = JSON.parse(response.body) + + expect(parsed_response).to match( + a_hash_including("title" => "root edition, live"), + ) + expect(parsed_response["links"]).to match( + a_hash_including( + "taxons" => match_array( + a_hash_including("title" => "level 1 edition 2, draft"), + ), + ), + ) + expect(parsed_response["links"]["taxons"].first["links"]).to match( + a_hash_including( + "parent_taxons" => match_array( + a_hash_including("title" => "level 2 edition 1, live"), + ), + ), + ) + end + end +end From 3e8b3eaba4aa58b327a977433f24fa87ae3b99d9 Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Tue, 17 Feb 2026 10:34:19 +0000 Subject: [PATCH 18/20] Check drafts in link expansion precedence tests These tests provide us with reassurance that we're in line with Content Store other than in a few known scenarios. The new examples added by considering drafts all pass, suggesting there are no draft-specific divergences from Content Store (unless any of our existing ignore rules are masking novel diffs) --- ...cedence_with_different_content_ids_spec.rb | 30 ++++++++++--------- ...k_precedence_with_same_content_ids_spec.rb | 16 +++++----- .../direct_links/test_case.rb | 12 ++++++-- .../direct_links/test_case_factory.rb | 6 ++-- .../direct_links/test_linked_edition.rb | 2 +- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/spec/integration/graphql/link_expansion/direct_link_precedence_with_different_content_ids_spec.rb b/spec/integration/graphql/link_expansion/direct_link_precedence_with_different_content_ids_spec.rb index 6d29cc561f..8e8aa8f807 100644 --- a/spec/integration/graphql/link_expansion/direct_link_precedence_with_different_content_ids_spec.rb +++ b/spec/integration/graphql/link_expansion/direct_link_precedence_with_different_content_ids_spec.rb @@ -2,23 +2,25 @@ GraphqlLinkExpansionPrecedenceHelpers::DirectLinks::TestCaseFactory .all(target_content_ids_differ: true) .each do |test_case| # rubocop:disable Rails/FindEach - context test_case.source_edition_locale_description do - it test_case.description do - aggregate_failures do - if test_case.graphql_titles != test_case.content_store_titles && - test_case.invalid_edition_and_valid_link_set_linked_editions? - # this is a known diff between Content Store and GraphQL so - # we're just checking that the GraphQL result is 'correct' and - # not that it matches Content Store + context test_case.with_drafts_description do + context test_case.source_edition_locale_description do + it test_case.description do + aggregate_failures do + if test_case.graphql_titles != test_case.content_store_titles && + test_case.invalid_edition_and_valid_link_set_linked_editions? + # this is a known diff between Content Store and GraphQL so + # we're just checking that the GraphQL result is 'correct' and + # not that it matches Content Store - expect(test_case.content_store_titles.size).to be(0) - expect(test_case.graphql_titles.size).to be(1) - expect(test_case.graphql_titles.first).to match(/link_set/) - else - expect(test_case.graphql_titles).to eq(test_case.content_store_titles) + expect(test_case.content_store_titles.size).to be(0) + expect(test_case.graphql_titles.size).to be(1) + expect(test_case.graphql_titles.first).to match(/link_set/) + else + expect(test_case.graphql_titles).to eq(test_case.content_store_titles) + end end end end end - end + end end diff --git a/spec/integration/graphql/link_expansion/direct_link_precedence_with_same_content_ids_spec.rb b/spec/integration/graphql/link_expansion/direct_link_precedence_with_same_content_ids_spec.rb index 294d8be3f2..0e9e47fc7c 100644 --- a/spec/integration/graphql/link_expansion/direct_link_precedence_with_same_content_ids_spec.rb +++ b/spec/integration/graphql/link_expansion/direct_link_precedence_with_same_content_ids_spec.rb @@ -2,14 +2,16 @@ GraphqlLinkExpansionPrecedenceHelpers::DirectLinks::TestCaseFactory .all(target_content_ids_differ: false) .each do |test_case| # rubocop:disable Rails/FindEach - context test_case.source_edition_locale_description do - it test_case.description do - aggregate_failures do - expect(test_case.graphql_titles).to eq(test_case.content_store_titles) - expect(test_case.content_store_titles.size).to be <= 1 - expect(test_case.graphql_titles.size).to be <= 1 + context test_case.with_drafts_description do + context test_case.source_edition_locale_description do + it test_case.description do + aggregate_failures do + expect(test_case.graphql_titles).to eq(test_case.content_store_titles) + expect(test_case.content_store_titles.size).to be <= 1 + expect(test_case.graphql_titles.size).to be <= 1 + end end end end - end + end end diff --git a/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case.rb b/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case.rb index 330ef9a3fa..6f2a587e95 100644 --- a/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case.rb +++ b/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case.rb @@ -4,10 +4,12 @@ class TestCase include FactoryBot::Syntax::Methods def initialize( + with_drafts:, root_locale:, linked_editions:, target_content_ids_differ: ) + @with_drafts = with_drafts @root_locale = root_locale @linked_editions_input = linked_editions @target_content_ids_differ = target_content_ids_differ @@ -15,7 +17,7 @@ def initialize( def content_store_titles @content_store_titles ||= Presenters::Queries::ExpandedLinkSet - .by_edition(source_edition, with_drafts: false) + .by_edition(source_edition, with_drafts:) .links .fetch(link_type.to_sym, []) .map { it[:title] } @@ -25,6 +27,7 @@ def graphql_titles @graphql_titles ||= GraphQL::Dataloader.with_dataloading do |dataloader| request = dataloader.with( Sources::LinkedToEditionsSource, + with_drafts:, locale: root_locale, ).request([source_edition, link_type]) @@ -47,9 +50,13 @@ def source_edition_locale_description "when the source edition's locale is \"#{root_locale}\"" end + def with_drafts_description + "when #{with_drafts ? 'accepting' : 'rejecting'} drafts" + end + private - attr_reader :root_locale, :linked_editions_input, :target_content_ids_differ + attr_reader :with_drafts, :root_locale, :linked_editions_input, :target_content_ids_differ def source_edition @source_edition ||= create( @@ -101,6 +108,7 @@ def valid_linked_edition_of_kind?(kind) linked_editions[kind].any? do |edition| next if Edition::NON_RENDERABLE_FORMATS.include?(edition.document_type) next unless [Edition::DEFAULT_LOCALE, root_locale].include?(edition.locale) + next if edition.draft? && !with_drafts if edition.unpublished? next unless edition.withdrawn? diff --git a/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case_factory.rb b/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case_factory.rb index 4643fc3e6e..20fce01525 100644 --- a/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case_factory.rb +++ b/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_case_factory.rb @@ -4,8 +4,10 @@ class TestCaseFactory class << self def all(target_content_ids_differ:) query_values = [ - { root_locale: Edition::DEFAULT_LOCALE }, - { root_locale: "fr" }, + { with_drafts: true, root_locale: Edition::DEFAULT_LOCALE }, + { with_drafts: true, root_locale: "fr" }, + { with_drafts: false, root_locale: Edition::DEFAULT_LOCALE }, + { with_drafts: false, root_locale: "fr" }, ] link_kind_values = %w[link_set edition] diff --git a/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_linked_edition.rb b/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_linked_edition.rb index 348dd827df..a0b8008b8e 100644 --- a/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_linked_edition.rb +++ b/spec/support/graphql_link_expansion_precedence_helpers/direct_links/test_linked_edition.rb @@ -22,7 +22,7 @@ def initialize( def call Edition.find_by(state:, document:) || create( - :live_edition, + state == "draft" ? :edition : :live_edition, title: "edition #{Edition.count} (#{link_kind})", state:, document_type:, From 8862626162a37626b7c1d0263f0c9ed73bc4a0e2 Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Wed, 25 Feb 2026 23:49:39 +0000 Subject: [PATCH 19/20] Check drafts in reverse link exp. precedence tests These tests provide us with reassurance that we're in line with Content Store other than in a few known scenarios. The new examples added by considering drafts all pass, suggesting there are no draft-specific divergences from Content Store (unless any of our existing ignore rules are masking novel diffs) --- ...cedence_with_different_content_ids_spec.rb | 42 ++++++++------- ...k_precedence_with_same_content_ids_spec.rb | 54 ++++++++++--------- .../reverse_links/test_case.rb | 12 ++++- .../reverse_links/test_case_factory.rb | 6 ++- .../test_source_edition_factory.rb | 2 +- 5 files changed, 65 insertions(+), 51 deletions(-) diff --git a/spec/integration/graphql/link_expansion/reverse_link_precedence_with_different_content_ids_spec.rb b/spec/integration/graphql/link_expansion/reverse_link_precedence_with_different_content_ids_spec.rb index 27363e0d5b..26e19dfee8 100644 --- a/spec/integration/graphql/link_expansion/reverse_link_precedence_with_different_content_ids_spec.rb +++ b/spec/integration/graphql/link_expansion/reverse_link_precedence_with_different_content_ids_spec.rb @@ -2,28 +2,30 @@ GraphqlLinkExpansionPrecedenceHelpers::ReverseLinks::TestCaseFactory .all(source_content_ids_differ: true) .each do |test_case| # rubocop:disable Rails/FindEach - context test_case.linked_edition_locale_description do - it test_case.description do - aggregate_failures do - if test_case.graphql_titles != test_case.content_store_titles && - test_case.invalid_edition_and_valid_link_set_linking_source_editions? - # this is a known diff between Content Store and GraphQL so - # we're just checking that the GraphQL result is 'correct' and - # not that it matches Content Store + context test_case.with_drafts_description do + context test_case.linked_edition_locale_description do + it test_case.description do + aggregate_failures do + if test_case.graphql_titles != test_case.content_store_titles && + test_case.invalid_edition_and_valid_link_set_linking_source_editions? + # this is a known diff between Content Store and GraphQL so + # we're just checking that the GraphQL result is 'correct' and + # not that it matches Content Store - expect(test_case.content_store_titles.size).to be(0) - expect(test_case.graphql_titles.size).to be(1) - expect(test_case.graphql_titles.first).to match(/link_set/) - elsif test_case.graphql_titles != test_case.content_store_titles && - test_case.valid_edition_and_link_set_linking_editions? - # this is a known diff between Content Store and GraphQL so - # we're just checking that the results match our expectations + expect(test_case.content_store_titles.size).to be(0) + expect(test_case.graphql_titles.size).to be(1) + expect(test_case.graphql_titles.first).to match(/link_set/) + elsif test_case.graphql_titles != test_case.content_store_titles && + test_case.valid_edition_and_link_set_linking_editions? + # this is a known diff between Content Store and GraphQL so + # we're just checking that the results match our expectations - expect(test_case.content_store_titles.size).to be(1) - expect(test_case.content_store_titles.first).to match(/edition/) - expect(test_case.graphql_titles.size).to be(2) - else - expect(test_case.graphql_titles).to eq(test_case.content_store_titles) + expect(test_case.content_store_titles.size).to be(1) + expect(test_case.content_store_titles.first).to match(/edition/) + expect(test_case.graphql_titles.size).to be(2) + else + expect(test_case.graphql_titles).to eq(test_case.content_store_titles) + end end end end diff --git a/spec/integration/graphql/link_expansion/reverse_link_precedence_with_same_content_ids_spec.rb b/spec/integration/graphql/link_expansion/reverse_link_precedence_with_same_content_ids_spec.rb index 1e0e4809c0..cc9bfe6ca9 100644 --- a/spec/integration/graphql/link_expansion/reverse_link_precedence_with_same_content_ids_spec.rb +++ b/spec/integration/graphql/link_expansion/reverse_link_precedence_with_same_content_ids_spec.rb @@ -2,35 +2,37 @@ GraphqlLinkExpansionPrecedenceHelpers::ReverseLinks::TestCaseFactory .all(source_content_ids_differ: false) .each do |test_case| # rubocop:disable Rails/FindEach - context test_case.linked_edition_locale_description do - it test_case.description do - aggregate_failures do - if test_case.graphql_titles != test_case.content_store_titles && - test_case.invalid_root_locale_and_valid_default_locale_edition_linking_editions? - # this is a known diff between Content Store and GraphQL so - # we're just checking that the results match our expectations + context test_case.with_drafts_description do + context test_case.linked_edition_locale_description do + it test_case.description do + aggregate_failures do + if test_case.graphql_titles != test_case.content_store_titles && + test_case.invalid_root_locale_and_valid_default_locale_edition_linking_editions? + # this is a known diff between Content Store and GraphQL so + # we're just checking that the results match our expectations - expect(test_case.content_store_titles.size).to eq(1) - expect(test_case.content_store_result.first[:locale]) - .to eq(Edition::DEFAULT_LOCALE) - expect(test_case.graphql_titles.size).to eq(0) - elsif test_case.graphql_titles != test_case.content_store_titles && - test_case.valid_published_default_locale_and_unpublished_differing_root_locale_edition_linking_editions? - # this is a known diff between Content Store and GraphQL so - # we're just checking that the results match our expectations + expect(test_case.content_store_titles.size).to eq(1) + expect(test_case.content_store_result.first[:locale]) + .to eq(Edition::DEFAULT_LOCALE) + expect(test_case.graphql_titles.size).to eq(0) + elsif test_case.graphql_titles != test_case.content_store_titles && + test_case.valid_published_default_locale_and_unpublished_differing_root_locale_edition_linking_editions? + # this is a known diff between Content Store and GraphQL so + # we're just checking that the results match our expectations - expect(test_case.content_store_titles.size).to eq(1) - expect(test_case.content_store_result.first[:locale]) - .to eq(Edition::DEFAULT_LOCALE) - expect(test_case.graphql_titles.size).to eq(1) - test_case.graphql_result.first.then do |edition| - expect(edition.state).to eq("unpublished") - expect(edition.locale).not_to eq(Edition::DEFAULT_LOCALE) + expect(test_case.content_store_titles.size).to eq(1) + expect(test_case.content_store_result.first[:locale]) + .to eq(Edition::DEFAULT_LOCALE) + expect(test_case.graphql_titles.size).to eq(1) + test_case.graphql_result.first.then do |edition| + expect(edition.state).to eq("unpublished") + expect(edition.locale).not_to eq(Edition::DEFAULT_LOCALE) + end + else + expect(test_case.graphql_titles).to eq(test_case.content_store_titles) + expect(test_case.content_store_titles.size).to be <= 1 + expect(test_case.graphql_titles.size).to be <= 1 end - else - expect(test_case.graphql_titles).to eq(test_case.content_store_titles) - expect(test_case.content_store_titles.size).to be <= 1 - expect(test_case.graphql_titles.size).to be <= 1 end end end diff --git a/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case.rb b/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case.rb index d0890c51a3..c2c83e1063 100644 --- a/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case.rb +++ b/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case.rb @@ -4,10 +4,12 @@ class TestCase include FactoryBot::Syntax::Methods def initialize( + with_drafts:, root_locale:, source_editions:, source_content_ids_differ: ) + @with_drafts = with_drafts @root_locale = root_locale @source_editions_input = source_editions @source_content_ids_differ = source_content_ids_differ @@ -18,7 +20,7 @@ def content_store_result source_editions Presenters::Queries::ExpandedLinkSet - .by_edition(linked_edition, with_drafts: false) + .by_edition(linked_edition, with_drafts:) .links .fetch(ExpansionRules.reverse_link_type(link_type), []) end @@ -31,6 +33,7 @@ def graphql_result GraphQL::Dataloader.with_dataloading do |dataloader| request = dataloader.with( Sources::ReverseLinkedToEditionsSource, + with_drafts:, locale: root_locale, ).request([linked_edition, link_type]) @@ -89,9 +92,13 @@ def linked_edition_locale_description "when the linked (root) edition's locale is \"#{root_locale}\"" end + def with_drafts_description + "when #{with_drafts ? 'accepting' : 'rejecting'} drafts" + end + private - attr_reader :root_locale, :source_editions_input, :source_content_ids_differ + attr_reader :with_drafts, :root_locale, :source_editions_input, :source_content_ids_differ def linked_edition @linked_edition ||= create( @@ -172,6 +179,7 @@ def valid_edition_linking_source_edition_of_locale?(locale) def valid_edition?(edition) return false if Edition::NON_RENDERABLE_FORMATS.include?(edition.document_type) return false unless [Edition::DEFAULT_LOCALE, root_locale].include?(edition.locale) + return false if edition.draft? && !with_drafts if edition.unpublished? return false unless edition.withdrawn? diff --git a/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case_factory.rb b/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case_factory.rb index 10f40d1652..d4d1a11e5c 100644 --- a/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case_factory.rb +++ b/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_case_factory.rb @@ -4,8 +4,10 @@ class TestCaseFactory class << self def all(source_content_ids_differ:) query_values = [ - { root_locale: Edition::DEFAULT_LOCALE }, - { root_locale: "fr" }, + { with_drafts: true, root_locale: Edition::DEFAULT_LOCALE }, + { with_drafts: true, root_locale: "fr" }, + { with_drafts: false, root_locale: Edition::DEFAULT_LOCALE }, + { with_drafts: false, root_locale: "fr" }, ] link_kind_values = %w[link_set edition] diff --git a/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_source_edition_factory.rb b/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_source_edition_factory.rb index f514255799..0fc8685c79 100644 --- a/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_source_edition_factory.rb +++ b/spec/support/graphql_link_expansion_precedence_helpers/reverse_links/test_source_edition_factory.rb @@ -22,7 +22,7 @@ def initialize( def call Edition.find_by(state:, document:) || create( - :live_edition, + state == "draft" ? :edition : :live_edition, title: "edition #{Edition.count} (#{link_kind})", state:, document_type:, From 4577adf62f48ef4f1c878da0036c68b735823ee4 Mon Sep 17 00:00:00 2001 From: Ynda Jas Date: Thu, 26 Feb 2026 00:00:05 +0000 Subject: [PATCH 20/20] Speed up GraphQL link expansion integration tests For the GraphQL link expansion integration tests, in CI we have one job for each of the four specs to speed things up. However, when introducing drafts the number of examples in each spec increases substantially, leading to each of the specs taking longer than the job for the main test suite, and about two minutes longer for the different content ID specs We can double down on the parallelisation by introducing the parallel_rspec gem, which by default runs four examples at a time (if the machine has enough cores - GitHub Actions does by default). This doesn't neatly divide the run time by four but it does bring the GraphQL link expansion integration test run times in line with or below the main test suite The main test suite fails when running examples in parallel, so we can't currently use this to speed up the overall CI run time In order to run the examples in parallel, the gem creates three additional databases. When doing this preparation it tries to drop the initial database, which fails if the initial database is using the default name of 'postgres' we think because the postgres server relies on this default-named database. Therefore for these jobs we use a non-default name for the initial database (the specific name of which doesn't matter beyond not being 'postgres') --- .github/workflows/ci.yml | 48 +++++++++++++++++++++++++++++++++------- Gemfile | 1 + Gemfile.lock | 4 ++++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 73775bc3fa..a1763844fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,8 @@ jobs: - name: Setup Postgres id: setup-postgres-graphql-direct-link-precedence-same-content-id uses: alphagov/govuk-infrastructure/.github/actions/setup-postgres@main + with: + POSTGRES_DB: postgres-graphql-link-precedence - name: Setup Redis uses: alphagov/govuk-infrastructure/.github/actions/setup-redis@main @@ -115,11 +117,17 @@ jobs: TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-direct-link-precedence-same-content-id.outputs.db-url }} run: bundle exec rails db:setup - - name: Run RSpec + - name: Prepare for parallel testing + env: + RAILS_ENV: test + TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-direct-link-precedence-same-content-id.outputs.db-url }} + run: bundle exec rake db:parallel:create db:parallel:prepare + + - name: Run RSpec in parallel env: RAILS_ENV: test TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-direct-link-precedence-same-content-id.outputs.db-url }} - run: bundle exec rspec spec/integration/graphql/link_expansion/direct_link_precedence_with_same_content_ids_spec.rb + run: bundle exec prspec spec/integration/graphql/link_expansion/direct_link_precedence_with_same_content_ids_spec.rb test-graphql-direct-link-precedence-different-content-id: name: "Test GraphQL direct link expansion precedence with different content IDs" @@ -128,6 +136,8 @@ jobs: - name: Setup Postgres id: setup-postgres-graphql-direct-link-precedence-different-content-id uses: alphagov/govuk-infrastructure/.github/actions/setup-postgres@main + with: + POSTGRES_DB: postgres-graphql-link-precedence - name: Setup Redis uses: alphagov/govuk-infrastructure/.github/actions/setup-redis@main @@ -146,11 +156,17 @@ jobs: TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-direct-link-precedence-different-content-id.outputs.db-url }} run: bundle exec rails db:setup - - name: Run RSpec + - name: Prepare for parallel testing + env: + RAILS_ENV: test + TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-direct-link-precedence-different-content-id.outputs.db-url }} + run: bundle exec rake db:parallel:create db:parallel:prepare + + - name: Run RSpec in parallel env: RAILS_ENV: test TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-direct-link-precedence-different-content-id.outputs.db-url }} - run: bundle exec rspec spec/integration/graphql/link_expansion/direct_link_precedence_with_different_content_ids_spec.rb + run: bundle exec prspec spec/integration/graphql/link_expansion/direct_link_precedence_with_different_content_ids_spec.rb test-graphql-reverse-link-precedence-same-content-id: name: "Test GraphQL reverse link expansion precedence with same content ID" @@ -159,6 +175,8 @@ jobs: - name: Setup Postgres id: setup-postgres-graphql-reverse-link-precedence-same-content-id uses: alphagov/govuk-infrastructure/.github/actions/setup-postgres@main + with: + POSTGRES_DB: postgres-graphql-link-precedence - name: Setup Redis uses: alphagov/govuk-infrastructure/.github/actions/setup-redis@main @@ -177,11 +195,17 @@ jobs: TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-reverse-link-precedence-same-content-id.outputs.db-url }} run: bundle exec rails db:setup - - name: Run RSpec + - name: Prepare for parallel testing + env: + RAILS_ENV: test + TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-reverse-link-precedence-same-content-id.outputs.db-url }} + run: bundle exec rake db:parallel:create db:parallel:prepare + + - name: Run RSpec in parallel env: RAILS_ENV: test TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-reverse-link-precedence-same-content-id.outputs.db-url }} - run: bundle exec rspec spec/integration/graphql/link_expansion/reverse_link_precedence_with_same_content_ids_spec.rb + run: bundle exec prspec spec/integration/graphql/link_expansion/reverse_link_precedence_with_same_content_ids_spec.rb test-graphql-reverse-link-precedence-different-content-id: name: "Test GraphQL reverse link expansion precedence with different content IDs" @@ -190,6 +214,8 @@ jobs: - name: Setup Postgres id: setup-postgres-graphql-reverse-link-precedence-different-content-id uses: alphagov/govuk-infrastructure/.github/actions/setup-postgres@main + with: + POSTGRES_DB: postgres-graphql-link-precedence - name: Setup Redis uses: alphagov/govuk-infrastructure/.github/actions/setup-redis@main @@ -208,11 +234,17 @@ jobs: TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-reverse-link-precedence-different-content-id.outputs.db-url }} run: bundle exec rails db:setup - - name: Run RSpec + - name: Prepare for parallel testing + env: + RAILS_ENV: test + TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-reverse-link-precedence-different-content-id.outputs.db-url }} + run: bundle exec rake db:parallel:create db:parallel:prepare + + - name: Run RSpec in parallel env: RAILS_ENV: test TEST_DATABASE_URL: ${{ steps.setup-postgres-graphql-reverse-link-precedence-different-content-id.outputs.db-url }} - run: bundle exec rspec spec/integration/graphql/link_expansion/reverse_link_precedence_with_different_content_ids_spec.rb + run: bundle exec prspec spec/integration/graphql/link_expansion/reverse_link_precedence_with_different_content_ids_spec.rb check-schemas-build: name: Check content schemas are built diff --git a/Gemfile b/Gemfile index 97f6a298ee..acd5b04258 100644 --- a/Gemfile +++ b/Gemfile @@ -46,6 +46,7 @@ group :development, :test do gem "govuk_test" gem "pact", require: false gem "pact_broker-client", require: false + gem "parallel_rspec" gem "rspec-rails" gem "rubocop-govuk", require: false gem "simplecov", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 61d1d8f627..a1ce07b737 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -615,6 +615,9 @@ GEM term-ansicolor (~> 1.7) thor (>= 0.20, < 2.0) parallel (1.27.0) + parallel_rspec (3.0.0) + rake (> 10.0) + rspec parser (3.3.10.1) ast (~> 2.4.1) racc @@ -935,6 +938,7 @@ DEPENDENCIES oj pact pact_broker-client + parallel_rspec pg plek prometheus-client