Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for GraphQL API for Strapi #2269

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e36f52d
Basic setup of the new strapi graphql system
msquance-stem Jan 17, 2025
7eb10d8
Completion of all the different GraphQL queries for the components an…
msquance-stem Jan 22, 2025
296b290
Updating test suite to cover all the new graphql features and adding …
msquance-stem Jan 24, 2025
59b152f
Fixing query system
msquance-stem Jan 27, 2025
057cd13
Sonarcloud suggested fix
msquance-stem Jan 27, 2025
60e263c
Adding log to circle ci artifacts config to see if I can work out why…
msquance-stem Jan 27, 2025
b639c29
Testing default strapi_graphql_url to see if that fixes issues with l…
msquance-stem Jan 28, 2025
d20d6e5
Pointing it to the staging strapi by default to try and fix lighthous…
msquance-stem Jan 28, 2025
142e3d6
Wrong tld on temp fix
msquance-stem Jan 28, 2025
41cb1c5
Creating query for web_page_previews
msquance-stem Jan 28, 2025
5cf60a2
Attempting to confirm its just the home page is broken on lighthouse
msquance-stem Jan 29, 2025
9234cb1
Putting lighthouse config back
msquance-stem Jan 29, 2025
0a956d4
Fixing enrichment missing dynamic content, added default date filter …
msquance-stem Jan 29, 2025
d39a814
Confirming that blog post component is cause of lighthouse failure
msquance-stem Jan 29, 2025
9e54ee7
Temporarily removing rspec to speed up debugging
msquance-stem Jan 29, 2025
ce6bc88
More attempts to work out what is causing lighthouse issues
msquance-stem Jan 29, 2025
8675b4b
Catching generic error and getting it to print to work out what is go…
msquance-stem Jan 29, 2025
b43bc52
Adding logger to try and detect issue
msquance-stem Jan 29, 2025
ba026c1
Adding introspection query to the stubs, this means we no longer need…
msquance-stem Jan 29, 2025
818888d
Fixing schema error for bad merge
msquance-stem Feb 3, 2025
e97e90c
Fixing test issues caused by aside query stubs overlapping each other
msquance-stem Feb 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ CMS_PROVIDER="strapi"
STRAPI_API_KEY="fake-strapi-api-key-jsngnsemgsmeglkknslleng"
STRAPI_WRITE_API_KEY="fake-strapi-write-api-key-jsngnsemgsmeglkknslleng"
STRAPI_API_URL="https://strapi.teachcomputing.org/api"
STRAPI_IMAGE_URL="https://strapi.teachcomputing.org"
STRAPI_GRAPHQL_URL="https://strapi.teachcomputing.org/graphql"
STRAPI_IMAGE_URL="https://strapi.teachcomputing.org"
STRAPI_TEST_SCHEMA_PATH='spec/support/cms/providers/strapi/schema.json'
14 changes: 10 additions & 4 deletions app/jobs/searchable_page_indexing_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ class SearchablePageIndexingJob < ApplicationJob
def perform
now = DateTime.now
SearchablePages::CmsBlog.delete_all
blog_search_records = Cms::Collections::Blog.all(1, 1000)
if blog_search_records.resources.any?
SearchablePages::CmsBlog.insert_all(blog_search_records.resources.map { |blog| blog.to_search_record(now) })
page = 1
per_page = 100
loop do
blog_search_records = Cms::Collections::Blog.all(page, per_page) # Strapi Graphql has a max limit of 100
if blog_search_records.resources.any?
SearchablePages::CmsBlog.insert_all(blog_search_records.resources.map { |blog| blog.to_search_record(now) })
end
break if (page * per_page) > blog_search_records.total_records
page += 1
end

SearchablePages::CmsWebPage.delete_all
page_search_records = Cms::Collections::WebPage.all(1, 200)
page_search_records = Cms::Collections::WebPage.all(1, 100)
if page_search_records.resources.any?
SearchablePages::CmsWebPage.insert_all(page_search_records.resources.map { |page| page.to_search_record(now) })
end
Expand Down
6 changes: 3 additions & 3 deletions app/services/cms/collections/aside_section.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ def self.cache_expiry
4.hours
end

def self.resource_key
"aside-sections"
end
def self.resource_key = "aside-sections"

def self.graphql_key = "asideSections"
end
end
end
8 changes: 5 additions & 3 deletions app/services/cms/collections/blog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ def self.cache_expiry
15.minutes
end

def self.resource_key
"blogs"
end
def self.resource_key = "blogs"

def self.graphql_key = "blogs"

def self.sort = "publishDate:desc"

def self.query_keys
[:tag]
Expand Down
8 changes: 4 additions & 4 deletions app/services/cms/collections/enrichment_page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def self.resource_attribute_mappings
{model: Models::Slug, key: nil},
{model: Models::Seo, key: :seo},
{model: Models::PageTitle, key: :pageTitle},
{model: Models::DynamicZone, key: :content},
{model: Models::EnrichmentDynamicZone, key: :content},
{model: Models::EnrichmentList, key: :enrichments}
]
end
Expand All @@ -40,9 +40,9 @@ def self.cache_expiry
4.hours
end

def self.resource_key
"enrichment-pages"
end
def self.resource_key = "enrichment-pages"

def self.graphql_key = "enrichmentPages"
end
end
end
6 changes: 3 additions & 3 deletions app/services/cms/collections/programme.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ def self.cache_expiry
15.minutes
end

def self.resource_key
"programmes"
end
def self.resource_key = "programmes"

def self.graphql_key = "programmes"
end
end
end
6 changes: 3 additions & 3 deletions app/services/cms/collections/web_page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ def self.resource_attribute_mappings
]
end

def self.resource_key
"web-pages"
end
def self.resource_key = "web-pages"

def self.graphql_key = "webPages"
end
end
end
15 changes: 15 additions & 0 deletions app/services/cms/models/enrichment_dynamic_zone.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module Cms
module Models
class EnrichmentDynamicZone
attr_accessor :cms_models

def initialize(cms_models:)
@cms_models = cms_models
end

def render
CmsDynamicZoneComponent.new(cms_models:)
end
end
end
end
51 changes: 51 additions & 0 deletions app/services/cms/providers/strapi/base_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
module Cms
module Providers
module Strapi
class BaseClient
private

def to_paginated_response(collection_class, data)
{
resources: data[:data].map { map_collection(collection_class, _1) },
page: data[:meta][:pagination][:page],
page_size: data[:meta][:pagination][:pageSize],
page_number: data[:meta][:pagination][:pageCount],
total_records: data[:meta][:pagination][:total]
}
end

def map_collection(collection_class, data)
to_resource(data[:id], data[:attributes], collection_class.collection_attribute_mappings.map { process_model(_1, data[:attributes]) })
end

def map_resource(resource_class, data, has_preview = false, preview_key = nil)
attributes = if has_preview && data[:attributes].has_key?(:versions) # deal with the fact plugin doesnt return versions for some models
versions = data[:attributes][:versions][:data]
version_to_show = versions.find { _1[:attributes][:versionNumber].to_s == preview_key } || versions.last
version_to_show[:attributes]
else
data[:attributes]
end

raise ActiveRecord::RecordNotFound if !has_preview && attributes[:publishedAt].blank?

to_resource(data[:id], attributes, resource_class.resource_attribute_mappings.map { process_model(_1, attributes) }, preview: has_preview)
end

def process_model(mapping, attributes)
Factories::ModelFactory.process_model(mapping, attributes)
end

def to_resource(id, attributes, data_models, preview: false)
{
id:,
data_models: data_models.compact, # remove any nil data models
created_at: attributes[:createdAt],
updated_at: attributes[:updatedAt],
published_at: preview ? DateTime.now.to_s : attributes[:publishedAt]
}
end
end
end
end
end
43 changes: 2 additions & 41 deletions app/services/cms/providers/strapi/client.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module Cms
module Providers
module Strapi
class Client
class Client < BaseClient
def initialize
@connection = Connection.api
end
Expand All @@ -25,14 +25,7 @@ def all(collection_class, page, page_size, params)

raise ActiveRecord::RecordNotFound unless response.status == 200

body = JSON.parse(response.body, symbolize_names: true)
{
resources: body[:data].map { map_collection(collection_class, _1) },
page: body[:meta][:pagination][:page],
page_size: body[:meta][:pagination][:pageSize],
page_number: body[:meta][:pagination][:pageCount],
total_records: body[:meta][:pagination][:total]
}
to_paginated_response(collection_class, JSON.parse(response.body, symbolize_names: true))
end

def one(resource_class, resource_id = nil, preview: false, preview_key: nil)
Expand Down Expand Up @@ -75,38 +68,6 @@ def generate_populate_params(mappings, preview: false)
populate_params[0] = :versions if preview
populate_params
end

def map_collection(collection_class, data)
to_resource(data[:id], data[:attributes], collection_class.collection_attribute_mappings.map { process_model(_1, data[:attributes]) })
end

def map_resource(resource_class, data, has_preview = false, preview_key = nil)
attributes = if has_preview && data[:attributes].has_key?(:versions) # deal with the fact plugin doesnt return versions for some models
versions = data[:attributes][:versions][:data]
version_to_show = versions.find { _1[:attributes][:versionNumber].to_s == preview_key } || versions.last
version_to_show[:attributes]
else
data[:attributes]
end

raise ActiveRecord::RecordNotFound if !has_preview && attributes[:publishedAt].blank?

to_resource(data[:id], attributes, resource_class.resource_attribute_mappings.map { process_model(_1, attributes) }, preview: has_preview)
end

def process_model(mapping, attributes)
Factories::ModelFactory.process_model(mapping, attributes)
end

def to_resource(id, attributes, data_models, preview: false)
{
id:,
data_models: data_models.compact, # remove any nil data models
created_at: attributes[:createdAt],
updated_at: attributes[:updatedAt],
published_at: preview ? DateTime.now.to_s : attributes[:publishedAt]
}
end
end
end
end
Expand Down
12 changes: 7 additions & 5 deletions app/services/cms/providers/strapi/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ module Cms
module Providers
module Strapi
class Connection
def self.api
config = Rails.application.config
Faraday.new(url: config.strapi_api_url) do |connection|
connection.adapter :net_http
connection.authorization :Bearer, config.strapi_api_key
class << self
def api
config = Rails.application.config
Faraday.new(url: config.strapi_api_url) do |connection|
connection.adapter :net_http
connection.authorization :Bearer, config.strapi_api_key
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ module Strapi
module Factories
module ComponentFactory
def self.process_component(strapi_data)
component_name = strapi_data[:__component]
if strapi_data[:__typename]
component_name = strapi_data[:__typename]
.underscore
.tr("_", "-")
.gsub(/\Acomponent-(blocks|content-blocks|buttons)-/, '\1.')
return nil if strapi_data.keys.count == 1
else
component_name = strapi_data[:__component]
end
case component_name
when "content-blocks.text-block"
ModelFactory.to_content_block(strapi_data[:textContent], with_wrapper: false)
Expand Down
2 changes: 2 additions & 0 deletions app/services/cms/providers/strapi/factories/model_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ def self.process_model(mapping, all_data)
to_web_page_preview(strapi_data)
elsif model_class == Models::DynamicZone
model_class.new(cms_models: strapi_data.map { ComponentFactory.process_component(_1) }.compact)
elsif model_class == Models::EnrichmentDynamicZone
model_class.new(cms_models: strapi_data.map { ComponentFactory.process_component(_1) }.compact)
elsif model_class == Models::EnrichmentList
to_enrichment_list(all_data, strapi_data)
end
Expand Down
14 changes: 12 additions & 2 deletions app/services/cms/providers/strapi/factories/query_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ module Providers
module Strapi
module Factories
module QueryFactory
def self.key_type(function)
case Rails.application.config.strapi_connection_type
when "rest"
"$#{function}"
when "graphql"
function
end
end

def self.generate_parameters(collection_class, query)
filter = {}
if collection_class == Cms::Collections::Blog
filter[:featured] = {"$eq": query[:featured]} if query[:featured]
filter[:blog_tags] = {slug: {"$eq": query[:tag]}} if query[:tag]
filter[:publishDate] = {key_type("lt") => DateTime.now.strftime}
filter[:featured] = {key_type("eq") => query[:featured]} if query&.dig(:featured)
filter[:blog_tags] = {slug: {key_type("eq") => query[:tag]}} if query&.dig(:tag)
end
filter
end
Expand Down
59 changes: 59 additions & 0 deletions app/services/cms/providers/strapi/graphql_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module Cms
module Providers
module Strapi
class GraphqlClient < BaseClient
def initialize(schema_path: nil)
@connection = GraphqlConnection.api(schema_path:)
end

def all(collection_class, page, page_size, params)
query = Queries::BaseQuery.new(collection_class)
begin
response = @connection.execute(query.all_query(page, page_size, params))
rescue => e
Sentry.capture_exception(e)
raise ActiveRecord::RecordNotFound
end
raise ActiveRecord::RecordNotFound if response.errors.any?

data = clean_aliases(response.original_hash)
to_paginated_response(collection_class, data[:data][collection_class.graphql_key.to_sym])
end

def one(resource_class, resource_id = nil, preview: false, preview_key: nil)
query = Queries::BaseQuery.new(resource_class)
begin
response = @connection.execute(query.single_query(resource_id))
rescue => e
Sentry.capture_exception(e)
raise ActiveRecord::RecordNotFound
end

raise ActiveRecord::RecordNotFound if response.errors.any?

data = clean_aliases(response.original_hash)

results = data[:data][resource_class.graphql_key.to_sym][:data]

raise ActiveRecord::RecordNotFound if results.empty?

map_resource(resource_class, results.first, preview, preview_key)
end

# This has been created to allow for alias to be alias_name__field_name
# This means we can add alias that override the collision issues we found when we moved to graphql
# but without needing to rebuild the factory
def clean_aliases(data)
updated_data = data.deep_transform_keys do |key|
if key.include?("__") && key != "__typename"
key.split("__").last
else
key
end
end
updated_data.deep_symbolize_keys
end
end
end
end
end
27 changes: 27 additions & 0 deletions app/services/cms/providers/strapi/graphql_connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module Cms
module Providers
module Strapi
class GraphqlConnection
class << self
def api(schema_path: nil)
config = Rails.application.config
@client = Graphlient::Client.new(
config.strapi_graphql_url,
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: "Bearer #{config.strapi_api_key}"
},
http_options: {read_timeout: 20, write_timeout: 30},
schema_path: schema_path
)
end

def dump_schema
GraphQL::Client.dump_schema(@client.schema)&.to_json
end
end
end
end
end
end
Loading