diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index db80ebb7dd..99e0bb57b1 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -8,6 +8,26 @@ class GraphqlController < ApplicationController skip_before_action :authenticate_user!, only: [:execute] + def content + execute_in_read_replica do + schema_name = Edition.live.where(base_path:).pick(:schema_name) + + # TODO: handle unsupported schema_name + if schema_name && (class_name = "queries/graphql/#{schema_name}_query".camelize.constantize) + query = class_name.query(base_path:) + result = PublishingApiSchema.execute(query).to_hash + process_graphql_result(result) + # TODO: handle 404s + + render json: result.dig("data", "edition") + end + rescue StandardError => e + raise e unless Rails.env.development? + + handle_error_in_development(e) + end + end + def execute execute_in_read_replica do variables = prepare_variables(params[:variables]) @@ -24,15 +44,7 @@ def execute operation_name:, ).to_hash - set_prometheus_labels(result.dig("data", "edition")&.slice("document_type", "schema_name")) - - if result.key?("errors") - logger.warn("GraphQL query result contained errors: #{result['errors']}") - set_prometheus_labels("contains_errors" => true) - else - logger.debug("GraphQL query result: #{result}") - set_prometheus_labels("contains_errors" => false) - end + process_graphql_result(result) render json: result rescue StandardError => e @@ -44,6 +56,22 @@ def execute private + def base_path + "/#{params[:path_without_root]}" + end + + def process_graphql_result(result) + set_prometheus_labels(result.dig("data", "edition")&.slice("document_type", "schema_name")) + + if result.key?("errors") + logger.warn("GraphQL query result contained errors: #{result['errors']}") + set_prometheus_labels("contains_errors" => true) + else + logger.debug("GraphQL query result: #{result}") + set_prometheus_labels("contains_errors" => false) + end + end + def execute_in_read_replica(&block) if Rails.env.production_replica? ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do diff --git a/app/graphql/types/edition_type.rb b/app/graphql/types/edition_type.rb index a84b58a243..4f370bd280 100644 --- a/app/graphql/types/edition_type.rb +++ b/app/graphql/types/edition_type.rb @@ -144,23 +144,31 @@ class WhipOrganisation < Types::BaseObject field :sort_order, Integer end + field :acronym, String field :body, String field :brand, String field :change_history, GraphQL::Types::JSON + field :change_notes, GraphQL::Types::JSON + field :child_section_groups, GraphQL::Types::JSON field :current, Boolean field :default_news_image, Image field :display_date, Types::ContentApiDatetime field :emphasised_organisations, GraphQL::Types::JSON + field :end_date, Types::ContentApiDatetime field :ended_on, Types::ContentApiDatetime field :first_public_at, Types::ContentApiDatetime field :image, Image field :international_delegations, [EditionType], null: false field :logo, Logo + field :ordered_featured_documents, GraphQL::Types::JSON + field :organisation_govuk_status, GraphQL::Types::JSON field :political, Boolean field :privy_counsellor, Boolean field :reshuffle, GraphQL::Types::JSON field :role_payment_type, String field :seniority, Integer + field :social_media_links, GraphQL::Types::JSON + field :start_date, Types::ContentApiDatetime field :started_on, Types::ContentApiDatetime field :supports_historical_accounts, Boolean field :url_override, String @@ -170,6 +178,8 @@ class WhipOrganisation < Types::BaseObject field :active, Boolean, null: false field :analytics_identifier, String + field :api_path, String + field :api_url, String field :base_path, String field :change_history, GraphQL::Types::JSON field :content_id, ID @@ -198,6 +208,7 @@ class WhipOrganisation < Types::BaseObject field :title, String, null: false field :updated_at, Types::ContentApiDatetime field :web_url, String + field :withdrawn, Boolean field :withdrawn_notice, WithdrawnNotice def details(lookahead:) diff --git a/app/models/edition.rb b/app/models/edition.rb index 55db8900e4..9bfb0baeb8 100644 --- a/app/models/edition.rb +++ b/app/models/edition.rb @@ -196,6 +196,7 @@ def redirect? def withdrawn? unpublished? && unpublishing.withdrawal? end + alias_method :withdrawn, :withdrawn? def substitute? unpublished? && unpublishing.substitute? diff --git a/app/queries/graphql/fragments/_available_translations.graphql.erb b/app/queries/graphql/fragments/_available_translations.graphql.erb new file mode 100644 index 0000000000..0fa2d60626 --- /dev/null +++ b/app/queries/graphql/fragments/_available_translations.graphql.erb @@ -0,0 +1,15 @@ +fragment AvailableTranslations on Links { + available_translations { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_child_and_parent_taxons.graphql.erb b/app/queries/graphql/fragments/_child_and_parent_taxons.graphql.erb new file mode 100644 index 0000000000..f8f1337eb0 --- /dev/null +++ b/app/queries/graphql/fragments/_child_and_parent_taxons.graphql.erb @@ -0,0 +1,48 @@ +fragment Taxon on Edition { + api_path + api_url + base_path + content_id + description + details { + internal_name + notes_for_editors + visible_to_departmental_editors + } + document_type + locale + phase + public_updated_at + schema_name + title + web_url + withdrawn +} + +<%# The deepest nodes in GOV.UK's taxonomy are 5 levels deep (i.e. the root, plus 4 child taxons) %> +<%# For example https://www.gov.uk/api/content/transport/hs2-phase-2b %> +<%# We don't expand the whole taxonomy when looking at the root, so we only need the 4 levels. %> +<%# The first level expands the backlink to parent, but lower levels do not. %> +fragment ChildAndParentTaxons on Links { + child_taxons: links_of_type(type: parent_taxons, reverse: true) { + ...Taxon + links { + child_taxons: links_of_type(type: parent_taxons, reverse: true) { + ...Taxon + links { + child_taxons: links_of_type(type: parent_taxons, reverse: true) { + ...Taxon + links { + child_taxons: links_of_type(type: parent_taxons, reverse: true) { + ...Taxon + } + } + } + } + } + parent_taxons: links_of_type(type: parent_taxons) { + ...Taxon + } + } + } +} diff --git a/app/queries/graphql/fragments/_children.graphql.erb b/app/queries/graphql/fragments/_children.graphql.erb new file mode 100644 index 0000000000..044655610f --- /dev/null +++ b/app/queries/graphql/fragments/_children.graphql.erb @@ -0,0 +1,39 @@ +<%# TODO: this is used in ~10 places, but they all have different kinds of children so it's not really reusable %> +fragment Children on Links { + children: links_of_type(type: parent, reverse: true) { + api_path + api_url + base_path + content_id + document_type + details { + access_and_opening_times + } + links { + <%# + The back link to parent is here for compatibility with current link-expansion behaviour, + but I'm reasonably sure it's accidental and unnecessary. Why would you want to link from + a document to its child, and then back up to the parent? + %> + parent: links_of_type(type: parent) { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } + } + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_content_owners.graphql.erb b/app/queries/graphql/fragments/_content_owners.graphql.erb new file mode 100644 index 0000000000..2bccce9f45 --- /dev/null +++ b/app/queries/graphql/fragments/_content_owners.graphql.erb @@ -0,0 +1,15 @@ +fragment ContentOwners on Links { + content_owners: links_of_type(type: content_owners) { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_default_top_level_fields.graphql.erb b/app/queries/graphql/fragments/_default_top_level_fields.graphql.erb new file mode 100644 index 0000000000..e73f8b7289 --- /dev/null +++ b/app/queries/graphql/fragments/_default_top_level_fields.graphql.erb @@ -0,0 +1,19 @@ +fragment DefaultTopLevelFields on Edition { + analytics_identifier + base_path + content_id + description + document_type + first_published_at + locale + phase + public_updated_at + publishing_app + publishing_request_id + publishing_scheduled_at + rendering_app + scheduled_publishing_delay_seconds + schema_name + title + updated_at +} diff --git a/app/queries/graphql/fragments/_document_collections.graphql.erb b/app/queries/graphql/fragments/_document_collections.graphql.erb new file mode 100644 index 0000000000..8a96f16226 --- /dev/null +++ b/app/queries/graphql/fragments/_document_collections.graphql.erb @@ -0,0 +1,37 @@ +fragment DocumentCollections on Links { + document_collections: links_of_type(type: documents, reverse: true) { + api_path + api_url + base_path + content_id + document_type + <%# + Getting all of the documents in a collection would sometimes be prohibitively expensive. + Current link expansion behaviour special cases this so that only the grandparent document + is expanded. Nobody's quite sure why this behaviour is desirable though, so I'm not going + to reimplement it until we know why it's needed. + + links { + documents: links_of_type(type: documents) { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } + } + %> + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_government.graphql.erb b/app/queries/graphql/fragments/_government.graphql.erb new file mode 100644 index 0000000000..1792bb76eb --- /dev/null +++ b/app/queries/graphql/fragments/_government.graphql.erb @@ -0,0 +1,17 @@ +fragment Government on Links { + government: links_of_type(type: government) { + api_path + api_url + base_path + content_id + details { + current + ended_on + started_on + } + document_type + locale + title + web_url + } +} diff --git a/app/queries/graphql/fragments/_level_one_taxons.graphql.erb b/app/queries/graphql/fragments/_level_one_taxons.graphql.erb new file mode 100644 index 0000000000..e0353fabe0 --- /dev/null +++ b/app/queries/graphql/fragments/_level_one_taxons.graphql.erb @@ -0,0 +1,37 @@ +fragment LevelOneTaxons on Links { + level_one_taxons: links_of_type(type: root_taxon, reverse: true) { + api_path + api_url + base_path + content_id + description + details { + internal_name + notes_for_editors + visible_to_departmental_editors + } + document_type + links { + root_taxon: links_of_type(type: root_taxon) { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } + } + locale + phase + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_mainstream_browse_pages.graphql.erb b/app/queries/graphql/fragments/_mainstream_browse_pages.graphql.erb new file mode 100644 index 0000000000..b017598c08 --- /dev/null +++ b/app/queries/graphql/fragments/_mainstream_browse_pages.graphql.erb @@ -0,0 +1,16 @@ +fragment MainstreamBrowsePages on Links { + mainstream_browse_pages: links_of_type(type: mainstream_browse_pages) { + api_path + api_url + base_path + content_id + description + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_ordered_parent_organisations.graphql.erb b/app/queries/graphql/fragments/_ordered_parent_organisations.graphql.erb new file mode 100644 index 0000000000..b9993a9e66 --- /dev/null +++ b/app/queries/graphql/fragments/_ordered_parent_organisations.graphql.erb @@ -0,0 +1,28 @@ +fragment OrderedParentOrganisations on Links { + ordered_parent_organisations: links_of_type(type: ordered_parent_organisations) { + analytics_identifier + api_path + api_url + base_path + content_id + details { + acronym + brand + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + organisation_govuk_status + } + document_type + locale + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_ordered_related_items.graphql.erb b/app/queries/graphql/fragments/_ordered_related_items.graphql.erb new file mode 100644 index 0000000000..6e645d35d8 --- /dev/null +++ b/app/queries/graphql/fragments/_ordered_related_items.graphql.erb @@ -0,0 +1,47 @@ +fragment OrderedRelatedItems on Links { + ordered_related_items: links_of_type(type: ordered_related_items) { + api_path + api_url + base_path + content_id + document_type + links { + mainstream_browse_pages: links_of_type(type: mainstream_browse_pages) { + api_path + api_url + base_path + content_id + description + document_type + links { + parent: links_of_type(type: parent) { + api_path + api_url + base_path + content_id + description + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } + } + locale + public_updated_at + schema_name + title + web_url + withdrawn + } + } + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_organisations.graphql.erb b/app/queries/graphql/fragments/_organisations.graphql.erb new file mode 100644 index 0000000000..9251c0afcf --- /dev/null +++ b/app/queries/graphql/fragments/_organisations.graphql.erb @@ -0,0 +1,28 @@ +fragment Organisations on Links { + organisations: links_of_type(type: organisations) { + analytics_identifier + api_path + api_url + base_path + content_id + details { + acronym + brand + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + organisation_govuk_status + } + document_type + locale + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_original_primary_publishing_organisation.graphql.erb b/app/queries/graphql/fragments/_original_primary_publishing_organisation.graphql.erb new file mode 100644 index 0000000000..8cabe30685 --- /dev/null +++ b/app/queries/graphql/fragments/_original_primary_publishing_organisation.graphql.erb @@ -0,0 +1,28 @@ +fragment OriginalPrimaryPublishingOrganisation on Links { + original_primary_publishing_organisation: links_of_type(type: original_primary_publishing_organisation) { + analytics_identifier + api_path + api_url + base_path + content_id + details { + acronym + brand + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + organisation_govuk_status + } + document_type + locale + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_parent_and_root_taxons.graphql.erb b/app/queries/graphql/fragments/_parent_and_root_taxons.graphql.erb new file mode 100644 index 0000000000..778ab2282c --- /dev/null +++ b/app/queries/graphql/fragments/_parent_and_root_taxons.graphql.erb @@ -0,0 +1,64 @@ +fragment RootTaxon on Edition { + api_path + api_url + base_path + content_id + description + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn +} + +fragment ParentTaxon on Edition { + ...RootTaxon + details { + internal_name + notes_for_editors + visible_to_departmental_editors + } + phase +} + +<%# The deepest nodes in GOV.UK's taxonomy are 5 levels deep (i.e. 4 parent taxons + 1 root) %> +<%# For example https://www.gov.uk/api/content/transport/hs2-phase-2b %> +fragment ParentAndRootTaxons on Links { + parent_taxons: links_of_type(type: parent_taxons) { + ...ParentTaxon + links { + parent_taxons: links_of_type(type: parent_taxons) { + ...ParentTaxon + links { + parent_taxons: links_of_type(type: parent_taxons) { + ...ParentTaxon + links { + parent_taxons: links_of_type(type: parent_taxons) { + ...ParentTaxon + links { + root_taxon: links_of_type(type: root_taxon) { + ...RootTaxon + } + } + } + root_taxon: links_of_type(type: root_taxon) { + ...RootTaxon + } + } + } + root_taxon: links_of_type(type: root_taxon) { + ...RootTaxon + } + } + } + root_taxon: links_of_type(type: root_taxon) { + ...RootTaxon + } + } + } + root_taxon: links_of_type(type: root_taxon) { + ...RootTaxon + } +} \ No newline at end of file diff --git a/app/queries/graphql/fragments/_parents.graphql.erb b/app/queries/graphql/fragments/_parents.graphql.erb new file mode 100644 index 0000000000..b22ed95bd8 --- /dev/null +++ b/app/queries/graphql/fragments/_parents.graphql.erb @@ -0,0 +1,36 @@ +<%# +The deepest child pages on GOV.UK have 3 generations of parents. Examples: +- https://www.gov.uk/api/content/claim-rural-payments/sign-in +- https://www.gov.uk/api/content/foreign-travel-advice/anguilla +%> + +fragment Parent on Edition { + api_path + api_url + base_path + content_id + description + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn +} + +fragment Parents on Links { + parent: links_of_type(type: parent) { + ...Parent + links { + parent: links_of_type(type: parent) { + ...Parent + links { + parent: links_of_type(type: parent) { + ...Parent + } + }, + } + }, + } +} diff --git a/app/queries/graphql/fragments/_popular_links.graphql.erb b/app/queries/graphql/fragments/_popular_links.graphql.erb new file mode 100644 index 0000000000..7a5e648493 --- /dev/null +++ b/app/queries/graphql/fragments/_popular_links.graphql.erb @@ -0,0 +1,14 @@ +fragment PopularLinks on Links { + popular_links: links_of_type(type: popular_links) { + content_id + details { + link_items + } + document_type + locale + public_updated_at + schema_name + title + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_primary_publishing_organisation.graphql.erb b/app/queries/graphql/fragments/_primary_publishing_organisation.graphql.erb new file mode 100644 index 0000000000..550efd435e --- /dev/null +++ b/app/queries/graphql/fragments/_primary_publishing_organisation.graphql.erb @@ -0,0 +1,28 @@ +fragment PrimaryPublishingOrganisation on Links { + primary_publishing_organisation: links_of_type(type: primary_publishing_organisation) { + analytics_identifier + api_path + api_url + base_path + content_id + details { + acronym + brand + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + organisation_govuk_status + } + document_type + locale + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_related.graphql.erb b/app/queries/graphql/fragments/_related.graphql.erb new file mode 100644 index 0000000000..1b64a202ae --- /dev/null +++ b/app/queries/graphql/fragments/_related.graphql.erb @@ -0,0 +1,15 @@ +fragment Related on Links { + related: links_of_type(type: related) { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_role_appointments.graphql.erb b/app/queries/graphql/fragments/_role_appointments.graphql.erb new file mode 100644 index 0000000000..caafc6a0cf --- /dev/null +++ b/app/queries/graphql/fragments/_role_appointments.graphql.erb @@ -0,0 +1,6 @@ +fragment RoleAppointments on Links { + # TODO: Role Appointments need special handling, as sometimes they go role -> person and sometimes person -> role + role_appointments: links_of_type(type: TODO, reverse: true) { + __typename + } +} diff --git a/app/queries/graphql/fragments/_roles.graphql.erb b/app/queries/graphql/fragments/_roles.graphql.erb new file mode 100644 index 0000000000..5a3ef2a4f6 --- /dev/null +++ b/app/queries/graphql/fragments/_roles.graphql.erb @@ -0,0 +1,15 @@ +fragment Roles on Links { + roles: links_of_type(type: roles) { + content_id + details { + body + role_payment_type + } + document_type + locale + public_updated_at + schema_name + title + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_suggested_ordered_related_items.graphql.erb b/app/queries/graphql/fragments/_suggested_ordered_related_items.graphql.erb new file mode 100644 index 0000000000..0a35ba29e4 --- /dev/null +++ b/app/queries/graphql/fragments/_suggested_ordered_related_items.graphql.erb @@ -0,0 +1,15 @@ +fragment SuggestedOrderedRelatedItems on Links { + suggested_ordered_related_items: links_of_type(type: suggested_ordered_related_items) { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/fragments/_taxons.graphql.erb b/app/queries/graphql/fragments/_taxons.graphql.erb new file mode 100644 index 0000000000..9d6bb2a249 --- /dev/null +++ b/app/queries/graphql/fragments/_taxons.graphql.erb @@ -0,0 +1,27 @@ +<%= render "fragments/parent_and_root_taxons" %> + +fragment Taxons on Links { + taxons: links_of_type(type: taxons) { + api_path + api_url + base_path + content_id + description + details { + internal_name + notes_for_editors + visible_to_departmental_editors + } + document_type + links { + ...ParentAndRootTaxons + } + locale + phase + public_updated_at + schema_name + title + web_url + withdrawn + } +} \ No newline at end of file diff --git a/app/queries/graphql/fragments/_world_locations.graphql.erb b/app/queries/graphql/fragments/_world_locations.graphql.erb new file mode 100644 index 0000000000..0cdefb8df7 --- /dev/null +++ b/app/queries/graphql/fragments/_world_locations.graphql.erb @@ -0,0 +1,9 @@ +fragment WorldLocations on Links { + world_locations: links_of_type(type: world_locations) { + analytics_identifier + content_id + locale + schema_name + title + } +} diff --git a/app/queries/graphql/fragments/_worldwide_organisation.graphql.erb b/app/queries/graphql/fragments/_worldwide_organisation.graphql.erb new file mode 100644 index 0000000000..fab6b92c48 --- /dev/null +++ b/app/queries/graphql/fragments/_worldwide_organisation.graphql.erb @@ -0,0 +1,28 @@ +fragment WorldwideOrganisation on Links { + worldwide_organisation: links_of_type(type: worldwide_organisation) { + analytics_identifier + api_path + api_url + base_path + content_id + description + details { + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + world_location_names + } + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } +} diff --git a/app/queries/graphql/hmrc_manual_query.rb b/app/queries/graphql/hmrc_manual_query.rb new file mode 100644 index 0000000000..8281755940 --- /dev/null +++ b/app/queries/graphql/hmrc_manual_query.rb @@ -0,0 +1,113 @@ +class Queries::Graphql::HmrcManualQuery + def self.query(base_path:) + <<~QUERY + { + edition(base_path: "#{base_path}") { + ... on Edition { + analytics_identifier + base_path + content_id + description + details { + change_notes + child_section_groups + } + document_type + first_published_at + links { + available_translations { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } + organisations { + analytics_identifier + api_path + api_url + base_path + content_id + details { + acronym + brand + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + organisation_govuk_status + } + document_type + locale + schema_name + title + web_url + withdrawn + } + primary_publishing_organisation { + analytics_identifier + api_path + api_url + base_path + content_id + details { + acronym + brand + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + organisation_govuk_status + } + document_type + locale + schema_name + title + web_url + withdrawn + } + suggested_ordered_related_items { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } + } + locale + phase + public_updated_at + publishing_app + publishing_request_id + publishing_scheduled_at + rendering_app + scheduled_publishing_delay_seconds + schema_name + title + updated_at + } + } + } + QUERY + end +end diff --git a/app/queries/graphql/ministers_index_query.rb b/app/queries/graphql/ministers_index_query.rb new file mode 100644 index 0000000000..eb483213dd --- /dev/null +++ b/app/queries/graphql/ministers_index_query.rb @@ -0,0 +1,121 @@ +class Queries::Graphql::MinistersIndexQuery + def self.query(_base_path) + <<-QUERY + { + edition(base_path: "/government/ministers") { + ... on MinistersIndex { + base_path + content_id + document_type + first_published_at + locale + public_updated_at + publishing_app + rendering_app + schema_name + updated_at + + details { + body + reshuffle + } + + links { + ordered_cabinet_ministers { + ...basePersonInfo + } + + ordered_also_attends_cabinet { + ...basePersonInfo + } + + ordered_assistant_whips { + ...basePersonInfo + } + + ordered_baronesses_and_lords_in_waiting_whips { + ...basePersonInfo + } + + ordered_house_lords_whips { + ...basePersonInfo + } + + ordered_house_of_commons_whips { + ...basePersonInfo + } + + ordered_junior_lords_of_the_treasury_whips { + ...basePersonInfo + } + + ordered_ministerial_departments { + title + web_url + + details { + brand + + logo { + crest + formatted_title + } + } + + links { + ordered_ministers { + ...basePersonInfo + } + + ordered_roles { + content_id + } + } + } + } + } + } + } + + fragment basePersonInfo on MinistersIndexPerson { + title + base_path + web_url + + details { + privy_counsellor + + image { + url + alt_text + } + } + + links { + role_appointments { + details { + current + } + + links { + role { + content_id + title + web_url + + details { + role_payment_type + seniority + whip_organisation { + label + sort_order + } + } + } + } + } + } + } + QUERY + end +end diff --git a/app/queries/graphql/news_article_query.rb b/app/queries/graphql/news_article_query.rb new file mode 100644 index 0000000000..7f0d00a4b9 --- /dev/null +++ b/app/queries/graphql/news_article_query.rb @@ -0,0 +1,175 @@ +class Queries::Graphql::NewsArticleQuery + def self.query(base_path:) + <<-QUERY + { + edition( + base_path: "#{base_path}", + content_store: "live", + ) { + ... on Edition { + base_path + content_id + description + details { + body + change_history + display_date + emphasised_organisations + first_public_at + image { + alt_text + caption + credit + high_resolution_url + url + } + political + } + document_type + first_published_at + links { + available_translations { + base_path + locale + } + document_collections { + ...RelatedItem + web_url + } + government { + details { + current + } + title + } + mainstream_browse_pages { + ...RelatedItem + } + ordered_related_items { + ...RelatedItem + links { + mainstream_browse_pages { + links { + parent { + title + } + } + } + } + } + ordered_related_items_overrides { + ...RelatedItem + } + organisations { + analytics_identifier + base_path + content_id + title + } + people { + base_path + content_id + title + } + primary_publishing_organisation { + base_path + details { + default_news_image { + alt_text + url + } + } + title + } + related { + ...RelatedItem + } + related_guides { + ...RelatedItem + } + related_mainstream_content { + ...RelatedItem + } + related_statistical_data_sets { + ...RelatedItem + } + suggested_ordered_related_items { + ...RelatedItem + } + taxons { + ...Taxon + links { + parent_taxons { + ...Taxon + links { + parent_taxons { + ...Taxon + links { + parent_taxons { + ...Taxon + links { + parent_taxons { + ...Taxon + } + } + } + } + } + } + } + } + } + topical_events { + ...RelatedItem + content_id + } + world_locations { + analytics_identifier + base_path + content_id + locale + title + } + worldwide_organisations { + analytics_identifier + base_path + title + } + } + locale + public_updated_at + publishing_app + rendering_app + schema_name + title + updated_at + withdrawn_notice { + explanation + withdrawn_at + } + } + } + } + + fragment RelatedItem on Edition { + base_path + document_type + locale + title + } + + fragment Taxon on Edition { + base_path + content_id + details { + url_override + } + document_type + locale + phase + title + web_url + } + QUERY + end +end diff --git a/app/queries/graphql/role_query.rb b/app/queries/graphql/role_query.rb new file mode 100644 index 0000000000..2478f42d8b --- /dev/null +++ b/app/queries/graphql/role_query.rb @@ -0,0 +1,96 @@ +class Queries::Graphql::WorldIndexQuery + def self.query(base_path:) + <<-QUERY + { + edition(base_path: "#{base_path}") { + ... on Edition { + base_path + content_id + document_type + first_published_at + locale + public_updated_at + publishing_app + rendering_app + schema_name + title + updated_at + + details { + body + supports_historical_accounts + } + + links { + available_translations { + base_path + locale + } + + role_appointments { + details { + current + ended_on + started_on + } + + links { + person { + base_path + title + + details { + body + } + } + } + } + + ordered_parent_organisations { + analytics_identifier + base_path + title + } + + organisations { + analytics_identifier + } + + taxons { + ...Taxon + links { + parent_taxons { + ...Taxon + links { + parent_taxons { + ...Taxon + links { + parent_taxons { + ...Taxon + links { + parent_taxons { + ...Taxon + } + } + } + } + } + } + } + } + } + } + } + } + } + + fragment Taxon on Edition { + base_path + content_id + document_type + phase + title + } + QUERY + end +end diff --git a/app/queries/graphql/topical_event_query.rb b/app/queries/graphql/topical_event_query.rb new file mode 100644 index 0000000000..959411f7a8 --- /dev/null +++ b/app/queries/graphql/topical_event_query.rb @@ -0,0 +1,104 @@ +class Queries::Graphql::TopicalEventQuery + def self.query(base_path:) + <<~QUERY + { + edition(base_path: "#{base_path}") { + ... on Edition { + analytics_identifier + base_path + content_id + description + details { + body + emphasised_organisations + end_date + ordered_featured_documents + social_media_links + start_date + } + document_type + first_published_at + links { + available_translations { + api_path + api_url + base_path + content_id + document_type + locale + public_updated_at + schema_name + title + web_url + withdrawn + } + organisations { + analytics_identifier + api_path + api_url + base_path + content_id + details { + acronym + brand + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + organisation_govuk_status + } + document_type + locale + schema_name + title + web_url + withdrawn + } + primary_publishing_organisation { + analytics_identifier + api_path + api_url + base_path + content_id + details { + acronym + brand + default_news_image { + alt_text + url + } + logo { + crest + formatted_title + } + organisation_govuk_status + } + document_type + locale + schema_name + title + web_url + withdrawn + } + } + locale + phase + public_updated_at + publishing_app + publishing_request_id + publishing_scheduled_at + rendering_app + scheduled_publishing_delay_seconds + schema_name + title + updated_at + } + } + } + QUERY + end +end diff --git a/app/queries/graphql/world_index_query.rb b/app/queries/graphql/world_index_query.rb new file mode 100644 index 0000000000..180ec22549 --- /dev/null +++ b/app/queries/graphql/world_index_query.rb @@ -0,0 +1,38 @@ +class Queries::Graphql::WorldIndexQuery + def self.query(_base_path) + <<-QUERY + fragment worldLocationInfo on Edition { + active + name + slug + } + + { + edition(base_path: "/world") { + ... on Edition { + content_id + document_type + first_published_at + locale + public_updated_at + publishing_app + rendering_app + schema_name + title + updated_at + + details { + world_locations { + ...worldLocationInfo + } + + international_delegations { + ...worldLocationInfo + } + } + } + } + } + QUERY + end +end diff --git a/config/routes.rb b/config/routes.rb index 4c35b14b4d..d8a25d0e39 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/content/*path_without_root" => "graphql#content" post "/graphql", to: "graphql#execute" namespace :v2 do diff --git a/lib/graphql_query_builder.rb b/lib/graphql_query_builder.rb new file mode 100644 index 0000000000..a3c8752c23 --- /dev/null +++ b/lib/graphql_query_builder.rb @@ -0,0 +1,151 @@ +require "json" + +class GraphqlQueryBuilder + FRAGMENTS = Dir.glob(Rails.root.join("app/graphql/queries/fragments/_*.graphql.erb")) + .map { |file| file.match(/fragments\/_(.*)\.graphql\.erb\Z/)[1] } + .to_set + .freeze + + FRAGMENT_NAME_OVERRIDES = (Hash.new { |_, key| key }).merge( + "parent" => "parents", + "parent_taxons" => "parent_and_root_taxons", + "root_taxon" => "parent_and_root_taxons", + ).freeze + + DEFAULT_TOP_LEVEL_FIELDS = %w[ + analytics_identifier + base_path + content_id + description + document_type + first_published_at + locale + phase + public_updated_at + publishing_app + publishing_request_id + publishing_scheduled_at + rendering_app + scheduled_publishing_delay_seconds + schema_name + title + updated_at + ].freeze + + REVERSE_LINK_TYPES = { + "children" => "parent", + "document_collections" => "documents", + "policies" => "working_groups", + "child_taxons" => "parent_taxons", + "level_one_taxons" => "root_taxon", + "part_of_step_navs" => "pages_part_of_step_nav", + "related_to_step_navs" => "pages_related_to_step_nav", + "secondary_to_step_navs" => "pages_secondary_to_step_nav", + "role_appointments" => "TODO - can be person or role, depending on the expected type of its parent", + "ministers" => "ministerial", + }.freeze + + def initialize + SchemaService.all_schemas.each_key do |schema_name| + example_base_path = Edition.live.find_by(schema_name:, state: "published")&.base_path + next unless example_base_path + + output_query_class = build_query(example_base_path, schema_name) + File.write("app/queries/graphql/#{schema_name}.rb", output_query_class) + end + end + + def build_query(base_path, schema_name) + data = fetch_content(base_path) + + parts = if @use_fragments + [ + "<%= render \"fragments/default_top_level_fields\" %>", + (data["links"]&.keys&.map { FRAGMENT_NAME_OVERRIDES[it] }&.to_set & FRAGMENTS)&.sort&.map { |link_key| "<%= render \"fragments/#{link_key}\" %>" }, + "", + "{", + " edition(base_path: \"#\{base_path\}\") {", + " ... on Edition {", + " ...DefaultTopLevelFields", + " #{build_fields(data.except(*DEFAULT_TOP_LEVEL_FIELDS))}", + " }", + " }", + "}", + ] + else + [ + "{", + " edition(base_path: \"\#\{base_path\}\") {", + " ... on Edition {", + " #{build_fields(data)}", + " }", + " }", + "}", + ] + end + + class_name = "Queries::Graphql::#{schema_name.camelize}" + indented_query = parts.join("\n").indent(12) + + <<~OUTPUT + class #{class_name} + def self.query(base_path:) + <<-QUERY + #{indented_query} + QUERY + end + end + OUTPUT + end + +private + + def build_fields(data, indent = 4) + fields = data.flat_map do |entry| + case entry + in [String, {}] + nil + in ["details", Hash => details] + [ + "details {", + details.map { |details_key, _| " #{details_key}" }, + "}", + ] + in ["links", Hash => links] + [ + "links {", + links.map { |link_key, link_value| " #{build_links_query(link_key, link_value, indent + 2)}" }, + "}", + ] + in [String => key, String | Numeric | true | false | nil] + key + end + end + fields.compact.join("\n").indent(indent) + end + + def build_links_query(key, array, indent) + fragment_name = FRAGMENT_NAME_OVERRIDES[key] + return "...#{fragment_name.camelize}" if @use_fragments && FRAGMENTS.include?(fragment_name) + + [ + "#{key} {", + " " * (indent + 2) + build_fields(array.first, indent + 2), + "#{' ' * indent}}", + ].join("\n") + end + + def fetch_content(base_path) + url = URI("https://www.gov.uk/api/content#{base_path}".chomp("/")) + http = Net::HTTP.new(url.host, url.port) + http.use_ssl = true + request = Net::HTTP::Get.new(url) + response = http.request(request) + + if response.is_a?(Net::HTTPSuccess) + JSON.parse(response.body) + else + raise "HTTP request failed with status #{response.code} #{response.message}" + end + end +end