Skip to content

Commit

Permalink
feat(Field#lazy_resove) add hook for instrumenting lazy methods
Browse files Browse the repository at this point in the history
  • Loading branch information
rmosolgo committed Nov 30, 2016
1 parent 4b02c42 commit 2918e7c
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 10 deletions.
3 changes: 2 additions & 1 deletion guides/schema/instrumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,9 @@ end

It can be attached as shown above. You can use `redefine { ... }` to make a shallow copy of the {{ "GraphQL::Field" | api_doc }} and extend its definition.

## Query Instrumentation
{{ "GraphQL::Field#lazy_resolve_proc" | api_doc }} can also be instrumented. This is called for objects registered with [lazy execution]({{ site.baseurl }}/schema/lazy_execution).

## Query Instrumentation

Query instrumentation can be attached during schema definition:

Expand Down
42 changes: 42 additions & 0 deletions lib/graphql/compatibility/lazy_execution_specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,49 @@ def test_it_resolves_lazy_connections
]
assert_equal expected_edges, res["data"]["pushes"]["edges"]
assert_equal [[1, 2, 3], [4, 4, 4]], pushes
end

def test_it_calls_lazy_resolve_instrumentation
query_str = %|
{
p1: push(value: 1) {
value
}
p2: push(value: 2) {
push(value: 3) {
value
}
}
pushes(values: [1,2,3]) {
edges {
node {
value
push(value: 4) {
value
}
}
}
}
}
|

log = []
res = self.class.lazy_schema.execute(query_str, context: {lazy_instrumentation: log, pushes: []})
expected_log = [
"PUSH",
"Query.push: 1",
"Query.push: 2",
"PUSH",
"LazyPush.push: 3",
"LazyPushEdge.node: 1",
"LazyPushEdge.node: 2",
"LazyPushEdge.node: 3",
"PUSH",
"LazyPush.push: 4",
"LazyPush.push: 4",
"LazyPush.push: 4",
]
assert_equal expected_log, log
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def initialize(ctx, value)

def push
if @context[:lazy_pushes].include?(@value)
@context[:lazy_instrumentation] && @context[:lazy_instrumentation] << "PUSH"
@context[:pushes] << @context[:lazy_pushes]
@context[:lazy_pushes] = []
end
Expand All @@ -37,6 +38,19 @@ def push
end
end

module LazyInstrumentation
def self.instrument(type, field)
prev_lazy_resolve = field.lazy_resolve_proc
field.redefine {
lazy_resolve ->(o, a, c) {
result = prev_lazy_resolve.call(o, a, c)
c[:lazy_instrumentation] && c[:lazy_instrumentation].push("#{type.name}.#{field.name}: #{o.value}")
result
}
}
end
end

def self.build(execution_strategy)
lazy_push_type = GraphQL::ObjectType.define do
name "LazyPush"
Expand Down Expand Up @@ -73,6 +87,7 @@ def self.build(execution_strategy)
mutation_execution_strategy(execution_strategy)
lazy_resolve(LazyPush, :push)
lazy_resolve(LazyPushCollection, :push)
instrument(:field, LazyInstrumentation)
end
end
end
Expand Down
3 changes: 2 additions & 1 deletion lib/graphql/execution/execute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,11 @@ def resolve_field(owner, selection, parent_type, field, object, query_ctx)

lazy_method_name = query.lazy_method(raw_value)
result = if lazy_method_name
GraphQL::Execution::Lazy.new(raw_value, lazy_method_name).then { |inner_value|
field.prepare_lazy(raw_value, arguments, field_ctx).then { |inner_value|
continue_resolve_field(selection, parent_type, field, inner_value, field_ctx)
}
elsif raw_value.is_a?(GraphQL::Execution::Lazy)
# It came from a connection resolve, assume it was already instrumented
raw_value.then { |inner_value|
continue_resolve_field(selection, parent_type, field, inner_value, field_ctx)
}
Expand Down
44 changes: 39 additions & 5 deletions lib/graphql/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,27 +121,30 @@ module GraphQL
class Field
include GraphQL::Define::InstanceDefinable
accepts_definitions :name, :description, :deprecation_reason,
:resolve, :type, :arguments,
:resolve, :lazy_resolve,
:type, :arguments,
:property, :hash_key, :complexity, :mutation,
:relay_node_field,
argument: GraphQL::Define::AssignArgument


attr_accessor :name, :deprecation_reason, :description, :property, :hash_key, :mutation, :arguments, :complexity

# @return [Boolean] True if this is the Relay find-by-id field
attr_accessor :relay_node_field

ensure_defined(
:name, :deprecation_reason, :description, :description=, :property, :hash_key, :mutation, :arguments, :complexity,
:resolve, :resolve=, :type, :type=, :name=, :property=, :hash_key=,
:resolve, :resolve=, :lazy_resolve, :lazy_resolve=, :lazy_resolve_proc,
:type, :type=, :name=, :property=, :hash_key=,
:relay_node_field,
)

# @!attribute [r] resolve_proc
# @return [<#call(obj, args,ctx)>] A proc-like object which can be called to return the field's value
# @return [<#call(obj, args, ctx)>] A proc-like object which can be called to return the field's value
attr_reader :resolve_proc

# @return [<#call(obj, args, ctx)>] A proc-like object which can be called trigger a lazy resolution
attr_reader :lazy_resolve_proc

# @!attribute name
# @return [String] The name of this field on its {GraphQL::ObjectType} (or {GraphQL::InterfaceType})

Expand All @@ -158,6 +161,7 @@ def initialize
@complexity = 1
@arguments = {}
@resolve_proc = build_default_resolver
@lazy_resolve_proc = DefaultLazyResolve
@relay_node_field = false
end

Expand Down Expand Up @@ -217,10 +221,40 @@ def to_s
"<Field name:#{name || "not-named"} desc:#{description} resolve:#{resolve_proc}>"
end

# If {#resolve} returned and object which should be handled lazily,
# this method will be called later force the object to return its value.
# @param obj [Object] The {#resolve}-provided object, registered with {Schema#lazy_resolve}
# @param args [GraphQL::Query::Arguments] Arguments to this field
# @param ctx [GraphQL::Query::Context] Context for this field
# @return [Object] The result of calling the registered method on `obj`
def lazy_resolve(obj, args, ctx)
@lazy_resolve_proc.call(obj, args, ctx)
end

# Assign a new resolve proc to this field. Used for {#lazy_resolve}
def lazy_resolve=(new_lazy_resolve_proc)
@lazy_resolve_proc = new_lazy_resolve_proc
end

# Prepare a lazy value for this field. It may be `then`-ed and resolved later.
# @return [GraphQL::Execution::Lazy] A lazy wrapper around `obj` and its registered method name
def prepare_lazy(obj, args, ctx)
GraphQL::Execution::Lazy.new {
lazy_resolve(obj, args, ctx)
}
end

private

def build_default_resolver
GraphQL::Field::Resolve.create_proc(self)
end

module DefaultLazyResolve
def self.call(obj, args, ctx)
method_name = ctx.query.lazy_method(obj)
obj.public_send(method_name)
end
end
end
end
5 changes: 2 additions & 3 deletions lib/graphql/relay/connection_resolve.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@ def call(obj, args, ctx)
nodes = @underlying_resolve.call(obj, args, ctx)
lazy_method = ctx.query.lazy_method(nodes)
if lazy_method
GraphQL::Execution::Lazy.new do
resolved_nodes = nodes.public_send(lazy_method)
@field.prepare_lazy(nodes, args, ctx).then { |resolved_nodes|
build_connection(resolved_nodes, args, obj)
end
}
else
build_connection(nodes, args, obj)
end
Expand Down

0 comments on commit 2918e7c

Please sign in to comment.