We have an admittedly exotic setup here and I need some advice.
We're merging two Sangria Schemas.
Schema A: Hand-coded Schema that uses the sangria.derive macros. Values are fetched from thrift services and marshalled into Scala objects.
Schema B: Dynamic schema that represents the values as nested Map[String, Any]. Field values are resolved by name from the Context.value Map.
The goal is to eventually remove the hand-coded Schema A and have Schema B deliver the same data.
We start with Schema A, walk down from the Query object, and merge ObjectTypes when they're compatible.
I've been using a MappedSequenceLeafAction to merge the results of the Actions from both schemas for an ObjectType.
Something like
resolve = ctx => MappedSequenceLeafAction(
Seq(objectTypeFromA.resolve(ctx), objectTypeFromB.resolve(ctx)),
{ case Seq(l, r) => MergedValue(l, r) }
)
So the data in the Context.value field for a merged ObjectType is a MergedValue(left: Any, right: Any)
The resolve function for the ObjectType in Schema A is wrapped with a short function that extracts the right side of the MergedValue and passes it to the macro-derived resolve function.
If the ObjectType contains fields that originated in Schema B, the resolve function for those fields already contains code to unpack the MergedValue.left field and use that.
Schema B fields use the last part of the Context.path to look up the value in the Map[String, Any] contained in the left field.
This all works well with one level of nesting. The problem comes up when fields from Schema B are two-levels deep inside an ObjectType that has been merged.
When we resolve the value for the field representing the merged ObjectType we hand the "left" side into the original resolve function.
When the field from Schema B is nested inside of this ObjectType we've lost the right side. All we have is the marshalled Scala object representing the ObjectType.
I can think of two ways to fix this:
1. Bake the ability to unpack the MergedValue into the macro-derived sangria schema. Then use the MergedValues *everywhere*.
I'm not sure how to modify the resolve functions though and it makes an already complicated code path even more hairy.
2. Put the results from any field fetched from Schema B into a Map that's stored in the QueryContext itself.
Then we don't need the MergedValue at all anymore. Schema B fields would traverse the nested maps (or a similarly convenient data-structure) based on the path in Context.path.
The problem with this is that I'm not sure if there's a hook available in Sangria that would allow me to do this.
I've looked into using a sangria.middleware.Middleware or a QueryReducer but there's no hook for "the data has been resolved, but the fields haven't yet".
All of the Schema B fields are resolved via a custom DerferredResolver so I'm not sure if an UpdateCtx action can be used.
I'd need to update the context after the deferred values were fetched but fields hadn't yet been assigned values.
Sorry for the long post. You're a star if you've gotten this far ;)
Erik
Example:
################
# Schema A
type User {
foo: String
}
# The logged-in user
type Viewer {
user: User
}
query {
viewer: Viewer!
}
################
# Schema B
type User {
foo: String
bar: String
}
type Viewer {
user: User
someOtherViewerData: String
}
query {
viewer: Viewer!
}
##############
# Merged Schema
type User {
foo: String # From both, prefer A for now, eventually B
bar: String # From schema B but called after User.resolve has stripped the MergedValue to satisfy the macro resolvers
}
# The logged-in user
type Viewer {
user: User # Fetch User from both Schema A and B providers. Resolvable when ctx.value = MergedValue(l: User, r: Map("viewer" -> Map("user" -> ...., "someOtherViewerData": "hello")))
someOtherViewerData: String # From schema B
}
query {
viewer: Viewer! # No real backing data. The logged in user is derived from data in the Context
}