Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
include:
- gemfile: Gemfile
ruby: 3.3
steps:
- run: echo BUNDLE_GEMFILE=${{ matrix.gemfile }} > $GITHUB_ENV
- uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
- name: Run tests
run: |
gem install bundler -v 2.4.22
bundle install --jobs 4 --retry 3
bundle exec rake test
36 changes: 36 additions & 0 deletions lib/shopify_custom_data_graphql/response_transformer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
module ShopifyCustomDataGraphQL
class ResponseTransformer
EMPTY_HASH = {}.freeze
SCOPED_FIELD = /^#{Regexp.quote(RequestTransformer::RESERVED_PREFIX)}([^_]+)_(.+)/.freeze

def initialize(transform_map)
@transform_map = transform_map
end

def perform(result)
result["data"] = transform_object_scope!(result["data"], @transform_map) if result["data"]
result["errors"] = transform_errors!(result["errors"]) if result["errors"]
result
end

Expand Down Expand Up @@ -97,5 +99,39 @@ def transform_field_value(field_value, transform)
MetafieldTypeResolver.resolve(transform_type, field_value, transform["s"])
end
end

def transform_errors!(errors)
errors.each do |error|
error.delete("locations") if @transform_map.any?
error["path"] = transform_error_path(error["path"]) if error["path"]
end
end

def transform_error_path(path)
transformed_path = []
path.reduce([0, @transform_map]) do |(index, current_map)|
key = path[index]
if key.is_a?(String) && key.start_with?(RequestTransformer::RESERVED_PREFIX)
m = key.match(SCOPED_FIELD)
parent_name = m[1]
child_name = m[2]
transformed_path.push(parent_name, child_name)
next_map = current_map.dig("f", parent_name, "f", child_name)
return transformed_path unless next_map

if (next_transform = next_map.dig("fx", "t"))
next [index + 2, next_map] if MetafieldTypeResolver.reference?(next_transform)
end
[index + 1, next_map]
else
next_map = key.is_a?(String) ? current_map.dig("f", key) : current_map
return path unless next_map

transformed_path << key
[index + 1, next_map]
end
end
transformed_path
end
end
end
4 changes: 2 additions & 2 deletions shopify_custom_data_graphql.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Gem::Specification.new do |spec|
spec.name = "shopify_custom_data_graphql"
spec.version = ShopifyCustomDataGraphQL::VERSION
spec.authors = ["Greg MacWilliam"]
spec.summary = "A client for consuming Shopify metafields and metaobjects through schema projections."
spec.description = "Build a shop-specific GraphQL schema and use it to make requests."
spec.summary = "A statically-typed GraphQL API for Shopify Metafields and Metaobjects."
spec.description = "A statically-typed GraphQL API for Shopify Metafields and Metaobjects."
spec.homepage = "https://github.com/gmac/shopify_custom_data_graphql"
spec.license = "MIT"

Expand Down
39 changes: 39 additions & 0 deletions test/fixtures/casettes/errors_with_list_path.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"errors": [
{
"message": "Access denied for createdByStaff field.",
"locations": [
{
"line": 11,
"column": 13
}
],
"path": [
"products",
"nodes",
0,
"___extensions_widget",
"reference",
"___system_createdByStaff"
],
"extensions": {
"code": "ACCESS_DENIED"
}
}
],
"data": {
"products": {
"nodes": [
{
"extensions": {
"widget": {
"system": {
"createdByStaff": null
}
}
}
}
]
}
}
}
33 changes: 33 additions & 0 deletions test/fixtures/casettes/errors_with_object_path.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"errors": [
{
"message": "Access denied for createdByStaff field.",
"locations": [
{
"line": 8,
"column": 11
}
],
"path": [
"product",
"___extensions_widget",
"reference",
"___system_createdByStaff"
],
"extensions": {
"code": "ACCESS_DENIED"
}
}
],
"data": {
"product": {
"extensions": {
"widget": {
"system": {
"createdByStaff": null
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"data": {
"product": {
"id": "gid://shopify/Product/6885875646486",
"___extensions_fileReferenceList": {
"references": {
"nodes": [
{
"id": "gid://shopify/MediaImage/20354823356438",
"alt": "A scenic landscape"
}
]
}
},
"___extensions_productReferenceList": {
"references": {
"edges": [
{
"node": {
"id": "gid://shopify/Product/6561850556438",
"title": "Aquanauts Crystal Explorer Sub"
}
}
]
}
}
}
}
}
9 changes: 9 additions & 0 deletions test/fixtures/casettes/transforms_extensions_typename.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"data": {
"product": {
"__typename": "Product",
"extensions": "Product",
"___extensions___typename": "Product"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1181,7 +1181,7 @@ def transform_request(shop_query, variables: {}, operation_name: nil, schema: sh
)

errors = query.schema.static_validator.validate(query)[:errors]
refute errors.any?, "Invalid metafields query: #{errors.first.message}" if errors.any?
refute errors.any?, "Invalid custom data query: #{errors.first.message}" if errors.any?
result = ShopifyCustomDataGraphQL::RequestTransformer.new(query).perform

# validate transformed query against base admin schema
Expand Down
108 changes: 107 additions & 1 deletion test/shopify_custom_data_graphql/response_transformer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,65 @@ def test_transforms_extensions_reference_fields
assert_equal expected, result.dig("data")
end


def test_transforms_extensions_reference_list_fields
result = fetch("transforms_extensions_reference_list_fields", %|query {
product(id: "#{PRODUCT_ID}") {
id
extensions {
fileReferenceList(first: 10, after: "r2d2") {
nodes { id alt }
}
productReferenceList(last: 10, before: "c3p0") {
edges { node { id title } }
}
}
}
}|)

expected = {
"product" => {
"id" => PRODUCT_ID,
"extensions" => {
"fileReferenceList" => {
"nodes" => [{
"id" => "gid://shopify/MediaImage/20354823356438",
"alt" => "A scenic landscape",
}],
},
"productReferenceList" => {
"edges" => [{
"node" => {
"id" => "gid://shopify/Product/6561850556438",
"title" => "Aquanauts Crystal Explorer Sub",
},
}],
},
},
},
}

assert_equal expected, result.dig("data")
end

def test_transforms_extensions_typename
result = fetch("transforms_extensions_typename", %|query {
product(id: "#{PRODUCT_ID}") {
__typename
extensions { __typename }
}
}|)

expected = {
"product" => {
"__typename" => "Product",
"extensions" => { "__typename" => "ProductExtensions" },
},
}

assert_equal expected, result.dig("data")
end

def test_transforms_mixed_reference_with_matching_type_selection
result = fetch("mixed_reference_returning_taco", %|query {
product(id: "1") {
Expand Down Expand Up @@ -142,6 +201,52 @@ def test_transforms_mixed_reference_without_matching_type_selection
assert_equal expected, result.dig("data")
end

def test_transforms_errors_with_object_paths
result = fetch("errors_with_object_path", %|query {
product(id: "#{PRODUCT_ID}") {
extensions {
widget {
system {
createdByStaff { name }
}
}
}
}
}|)

expected_errors = [{
"message" => "Access denied for createdByStaff field.",
"path" => ["product", "extensions", "widget", "system", "createdByStaff"],
"extensions" => { "code" => "ACCESS_DENIED" },
}]

assert_equal expected_errors, result.dig("errors")
end

def test_transforms_errors_with_list_paths
result = fetch("errors_with_list_path", %|query {
products(first: 1) {
nodes {
extensions {
widget {
system {
createdByStaff { name }
}
}
}
}
}
}|)

expected_errors = [{
"message" => "Access denied for createdByStaff field.",
"path" => ["products", "nodes", 0, "extensions", "widget", "system", "createdByStaff"],
"extensions" => { "code" => "ACCESS_DENIED" },
}]

assert_equal expected_errors, result.dig("errors")
end

private

def fetch(fixture, document, variables: {}, operation_name: nil, schema: nil)
Expand All @@ -152,7 +257,8 @@ def fetch(fixture, document, variables: {}, operation_name: nil, schema: nil)
operation_name: operation_name,
)

assert query.schema.static_validator.validate(query)[:errors].none?, "Invalid shop query."
errors = query.schema.static_validator.validate(query)[:errors]
refute errors.any?, "Invalid custom data query: #{errors.first.message}" if errors.any?
shop_query = ShopifyCustomDataGraphQL::RequestTransformer.new(query).perform.to_prepared_query
shop_query.perform do |query_string|
fetch_response(fixture, query_string)
Expand Down
Loading