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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,23 +153,22 @@ An App schema promotes an app-owned custom data namespace as base fields and typ
client = ShopifyCustomDataGraphQL::Client.new(
# ...
base_namespaces: ["$app"],
prefixed_namespaces: ["$app:*", "app--*", "custom", "other"],
prefixed_namespaces: ["$app:*", "app--*", "custom"],
app_context_id: 123,
)
```

Results in:

* **`custom.my_field`** → `custom_myField`
* **`other.my_field`** → `other_myField`
* **`app--123.my_field`**: → `myField`
* **`app--123--other.my_field`** → `other_myField`
* **`app--456.my_field`** → `app456_myField`
* **`my_type`** → `MyTypeShopMetaobject`
* **`app--123--my_type`** → `MyTypeMetaobject`
* **`app--456--my_type`** → `MyTypeApp456Metaobject`

Providing just `app_context_id` will automatically filter the schema down to just `$app` fields and types owned by the specified client.
Providing just `app_context_id` will automatically filter the schema down to just `$app` fields and types owned by the specified app id.

### Combined namespaces

Expand Down
1 change: 1 addition & 0 deletions example/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ gem 'rack'
gem 'rackup'
gem 'graphql'
gem 'activesupport'
gem 'rainbow'
38 changes: 29 additions & 9 deletions example/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'rackup'
require 'json'
require 'graphql'
require 'rainbow'
require_relative '../lib/shopify_custom_data_graphql'

class App
Expand Down Expand Up @@ -31,30 +32,49 @@ def initialize
@client.on_cache_read { |k| @mock_cache[k] }
@client.on_cache_write { |k, v| @mock_cache[k] = v }

puts "Loading custom data schema..."
puts Rainbow("Loading custom data schema...").cyan.bright
@client.eager_load!
puts "Done."
puts Rainbow("Done.").cyan
end

def call(env)
req = Rack::Request.new(env)
case req.path_info
when /graphql/
params = JSON.parse(req.body.read)
result = @client.execute(
query: params["query"],
variables: params["variables"],
operation_name: params["operationName"],
)
result = log_result do
@client.execute(
query: params["query"],
variables: params["variables"],
operation_name: params["operationName"],
)
end

[200, {"content-type" => "application/json"}, [JSON.generate(result)]]
[200, {"content-type" => "application/json"}, [JSON.generate(result.to_h)]]
when /refresh/
reload_shop_schema
@client.schema(reload_custom_schema: true)
[200, {"content-type" => "text/html"}, ["Shop schema refreshed!"]]
else
[200, {"content-type" => "text/html"}, [@graphiql]]
end
end

def log_result
timestamp = Time.current
result = yield
message = [Rainbow("[request #{timestamp.to_s}]").cyan.bright]
stats = ["validate", "introspection", "transform_request", "proxy", "transform_response"].filter_map do |stat|
time = result.tracer[stat]
next unless time

"#{Rainbow(stat).magenta}: #{(time * 100).round / 100.to_f}ms"
end

message << stats.join(", ")
message << "\n#{result.query}" if result.tracer["transform_request"]
puts message.join(" ")
result
end
end

Rackup::Handler.default.run(App.new, :Port => 3000)
22 changes: 13 additions & 9 deletions lib/shopify_custom_data_graphql/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,17 @@ def schema(reload_custom_schema: false, reload_admin_schema: false)

def execute(query: nil, variables: nil, operation_name: nil)
tracer = Tracer.new
result = tracer.span("execute") do
prepare_query(query, operation_name, tracer) do |admin_query|
tracer.span("execute") do
perform_query(query, operation_name, tracer) do |admin_query|
@admin.fetch(admin_query, variables: variables)
end
end

puts tracer.as_json
result
rescue ValidationError => e
{ "errors" => e.errors }
PreparedQuery::Result.new(
query: query,
tracer: tracer,
result: { "errors" => e.errors },
)
end

def on_cache_read(&block)
Expand All @@ -127,7 +128,7 @@ def on_cache_write(&block)

private

def prepare_query(query_str, operation_name, tracer, &block)
def perform_query(query_str, operation_name, tracer, &block)
digest = @digest_class.hexdigest([query_str, operation_name, VERSION].join(" "))
if @lru && (hot_query = @lru.get(digest))
return hot_query.perform(tracer, source_query: query_str, &block)
Expand All @@ -147,9 +148,12 @@ def prepare_query(query_str, operation_name, tracer, &block)
end
raise ValidationError.new(errors: errors.map(&:to_h)) if errors.any?

return query.result.to_h if introspection_query?(query)
if introspection_query?(query)
result = tracer.span("introspection") { query.result.to_h }
return PreparedQuery::Result.new(query: query_str, tracer: tracer, result: result)
end

prepared_query = tracer.span("request_transform") do
prepared_query = tracer.span("transform_request") do
RequestTransformer.new(query).perform.to_prepared_query
end
json = prepared_query.to_json
Expand Down
38 changes: 25 additions & 13 deletions lib/shopify_custom_data_graphql/prepared_query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ module ShopifyCustomDataGraphQL
class PreparedQuery
DEFAULT_TRACER = Tracer.new

class Result
attr_reader :query, :tracer, :result

def initialize(query:, tracer:, result:)
@query = query
@tracer = tracer
@result = result
end

def to_h
@result
end
end

attr_reader :query, :transforms

def initialize(params)
Expand All @@ -15,10 +29,6 @@ def initialize(params)
end
end

def has_transforms?
@transforms.any?
end

def as_json
{
"query" => @query,
Expand All @@ -31,16 +41,18 @@ def to_json
end

def perform(tracer = DEFAULT_TRACER, source_query: nil)
# pass through source query when it requires no transforms
# stops queries without transformations from taking up cache space
return yield(source_query) if source_query && !has_transforms?

response = tracer.span("proxy") do
yield(@query)
end
tracer.span("transform_response") do
ResponseTransformer.new(@transforms).perform(response)
query = source_query && @transforms.none? ? source_query : @query
raw_result = tracer.span("proxy") { yield(query) }

result = if @transforms.any?
tracer.span("transform_response") do
ResponseTransformer.new(@transforms).perform(raw_result)
end
else
raw_result
end

Result.new(query: query, tracer: tracer, result: result)
end
end
end
22 changes: 11 additions & 11 deletions lib/shopify_custom_data_graphql/request_transformer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def initialize(query, metafield_ns = "custom")
@query = query
@schema = query.schema
@app_context = directive_kwargs(@schema.schema_directives, "app")&.dig(:id)
@owner_types = @schema.possible_types(@schema.get_type("HasMetafields")).to_set
@owner_types = @schema.possible_types(@query.get_type("HasMetafields")).to_set
@root_ext_name = MetafieldTypeResolver.extensions_typename(@schema.query.graphql_name)
@transform_map = TransformationMap.new(@app_context)
@metafield_ns = metafield_ns
Expand Down Expand Up @@ -71,13 +71,13 @@ def transform_scope(parent_type, input_selections, scope_type: NATIVE_SCOPE, sco
if scope_type == NATIVE_SCOPE && node.name == "extensions" && (parent_type == @schema.query || @owner_types.include?(parent_type))
@transform_map.apply_field_transform(FieldTransform.new(NAMESPACE_TRANSFORM))
with_namespace_anchor_field(node) do
next_type = parent_type.get_field(node.name).type.unwrap
next_type = @query.get_field(parent_type, node.name).type.unwrap
transform_scope(next_type, node.selections, scope_type: EXTENSIONS_SCOPE, scope_ns: node.alias || node.name)
end
elsif scope_type == METAOBJECT_SCOPE && node.name == "system"
@transform_map.apply_field_transform(FieldTransform.new(NAMESPACE_TRANSFORM))
with_namespace_anchor_field(node) do
next_type = parent_type.get_field(node.name).type.unwrap
next_type = @query.get_field(parent_type, node.name).type.unwrap
transform_scope(next_type, node.selections, scope_ns: node.alias || node.name)
end
elsif scope_type == EXTENSIONS_SCOPE && parent_type.graphql_name == @root_ext_name
Expand All @@ -89,15 +89,15 @@ def transform_scope(parent_type, input_selections, scope_type: NATIVE_SCOPE, sco
node = node.merge(alias: "#{RESERVED_PREFIX}#{scope_ns}_#{node.alias || node.name}")
end
if node.selections&.any?
next_type = parent_type.get_field(node.name).type.unwrap
next_type = @query.get_field(parent_type, node.name).type.unwrap
node = node.merge(selections: transform_scope(next_type, node.selections))
end
node
end
end

when GraphQL::Language::Nodes::InlineFragment
fragment_type = node.type.nil? ? parent_type : @schema.get_type(node.type.name)
fragment_type = node.type.nil? ? parent_type : @query.get_type(node.type.name)
with_typed_condition(parent_type, fragment_type, scope_type) do
if MetafieldTypeResolver.extensions_type?(fragment_type.graphql_name)
transform_scope(fragment_type, node.selections, scope_type: EXTENSIONS_SCOPE, scope_ns: scope_ns)
Expand All @@ -116,7 +116,7 @@ def transform_scope(parent_type, input_selections, scope_type: NATIVE_SCOPE, sco

when GraphQL::Language::Nodes::FragmentSpread
fragment_def = @query.fragments[node.name]
fragment_type = @schema.get_type(fragment_def.type.name)
fragment_type = @query.get_type(fragment_def.type.name)
with_typed_condition(parent_type, fragment_type, scope_type) do
unless @new_fragments[node.name]
fragment_type_name = fragment_type.graphql_name
Expand Down Expand Up @@ -175,7 +175,7 @@ def with_typed_condition(parent_type, fragment_type, scope_type)

# connections must map transformations through possible `edges -> node` and `nodes` pathways
def build_connection_selections(conn_type, conn_node)
conn_node_type = conn_type.get_field("nodes").type.unwrap
conn_node_type = @query.get_field(conn_type, "nodes").type.unwrap
conn_node.selections.map do |node|
@transform_map.field_breadcrumb(node) do
case node.name
Expand All @@ -186,7 +186,7 @@ def build_connection_selections(conn_type, conn_node)
when "node"
n.merge(selections: yield(conn_node_type, n.selections))
when GQL_TYPENAME
edge_type = conn_type.get_field("edges").type.unwrap
edge_type = @query.get_field(conn_type, "edges").type.unwrap
@transform_map.apply_field_transform(FieldTransform.new(STATIC_TYPENAME_TRANSFORM, value: edge_type.graphql_name))
n
else
Expand All @@ -210,10 +210,10 @@ def build_connection_selections(conn_type, conn_node)
def build_metaobject_query(parent_type, node, scope_ns: nil)
return build_typename(parent_type, node, scope_type: EXTENSIONS_SCOPE, scope_ns: scope_ns) if node.name == GQL_TYPENAME

field_type = parent_type.get_field(node.name).type.unwrap
field_type = @query.get_field(parent_type, node.name).type.unwrap
return node unless MetafieldTypeResolver.connection_type?(field_type.graphql_name)

node_type = field_type.get_field("nodes").type.unwrap
node_type = @query.get_field(field_type, "nodes").type.unwrap
metaobject_type = directive_kwargs(node_type.directives, "metaobject")&.dig(:type)
return node unless metaobject_type

Expand All @@ -232,7 +232,7 @@ def build_metaobject_query(parent_type, node, scope_ns: nil)
def build_metafield(parent_type, node, scope_type:, scope_ns: nil)
return build_typename(parent_type, node, scope_type: scope_type, scope_ns: scope_ns) if node.name == GQL_TYPENAME

field = parent_type.get_field(node.name)
field = @query.get_field(parent_type, node.name)
metafield_attrs = directive_kwargs(field.directives, "metafield")
return node unless metafield_attrs

Expand Down
1 change: 1 addition & 0 deletions shopify_custom_data_graphql.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 12.0"
spec.add_development_dependency "minitest", "~> 5.12"
spec.add_development_dependency "graphql-response_validator", "~> 0.0.2"
end
14 changes: 14 additions & 0 deletions test/fixtures/responses/coalesces_value_object_selections.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": {
"product": {
"extensions": "Product",
"___extensions_rating": {
"jsonValue": {
"value": 4.5,
"scale_max": 5,
"scale_min": 0
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,5 @@
}
}
],
"data": {
"products": {
"nodes": [
{
"extensions": {
"widget": {
"system": {
"createdByStaff": null
}
}
}
}
]
}
}
"data": null
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,5 @@
}
}
],
"data": {
"product": {
"extensions": {
"widget": {
"system": {
"createdByStaff": null
}
}
}
}
}
"data": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"data": {
"product": {
"extensions": "Product",
"___extensions_rating1": {
"jsonValue": {
"value": 4.5,
"scale_max": 5,
"scale_min": 0
}
},
"___extensions_rating2": {
"jsonValue": {
"value": 4.5,
"scale_max": 5,
"scale_min": 0
}
}
}
}
}
14 changes: 14 additions & 0 deletions test/fixtures/responses/fragment_spreads_with_type_condition.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"data": {
"node": {
"id": "gid://shopify/Product/1",
"title": "Ethereal Dreams T-Shirt",
"productExt": "Product",
"___productExt_boolean": {
"jsonValue": true
},
"___typehint": "Product",
"__typename__": "Product"
}
}
}
Loading
Loading