-
Notifications
You must be signed in to change notification settings - Fork 89
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
Neo metaphysics #809
Neo metaphysics #809
Conversation
I think that, if we feel this works well, we should start removing types from MP’s schema that are expressed in service’s own GraphQL schemas and stitch them in. The end goal would be to have pretty much nothing left in MP but stitching code. |
Here’s a few issues I’ve written up previously about other (long term) things to consider in this new stitching world: |
index.js
Outdated
mergedSchema().then(merged => { | ||
schema = merged | ||
app.listen(port, () => info(`Listening on ${port}`)) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe all of the app
setup should just move inside this promise and then schema
can just be a const
.
mergedSchema.js
Outdated
fragment: `fragment SubmissionArtist on Submission { artist_id }`, | ||
resolve: (parent, args, context, info) => { | ||
const id = parent.artist_id | ||
return mergeInfo.delegate("query", "artist", { id }, context, info) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically at runtime generating a query like:
{
artist(id: "<result of artist_id>") {
# ...
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting note is that we may not want to have root fields for each type that we stitch into other types. Maybe we can use the node
root field for this? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mergeSchemas
author explained to me that this should be done in a few steps:
- Add the required root field to the original schema.
- Merge the schemas and setup the delegator to use the root field.
- Transform the merged schema to remove the root field (the delegator refers to the original schema).
package.json
Outdated
@@ -53,6 +54,7 @@ | |||
"graphql": "^0.11.7", | |||
"graphql-depth-limit": "^1.1.0", | |||
"graphql-relay": "0.5.3", | |||
"graphql-tools": "https://github.com/alloy/graphql-tools/releases/download/v2.7.3-alpha.1/graphql-tools-2.7.3-alpha.1.tgz", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This uses my fork ardatan/graphql-tools#484
mergedSchema.js
Outdated
schemas: [localSchema, convectionSchema, linkTypeDefs], | ||
// Prefer others over the local MP schema. | ||
onTypeConflict: (_leftType, rightType) => { | ||
console.warn(`[!] Type collision ${rightType}`) // eslint-disable-line no-console |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@orta There’s currently a bunch of consignments related type name conflicts, presumably because you’ve manually added these to MP’s schema before:
19:04:06 web.1 | [!] Type collision ID
19:04:06 web.1 | [!] Type collision Submission
19:04:06 web.1 | [!] Type collision String
19:04:06 web.1 | [!] Type collision Asset
19:04:06 web.1 | [!] Type collision JSON
19:04:06 web.1 | [!] Type collision Boolean
19:04:06 web.1 | [!] Type collision Int
The ID
, String
, Boolean
, and Int
ones are built-in though, it seems weird that graphql-tools
would trigger this callback for those 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's tricky, to remove Submission entirely in metaphysics - I'd have to recreate a bunch of mutations on convection, feasible, but time consuming
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, the submission type should be the exact same object - so maybe it's fine to return the convection one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WRT the JSON type - I would assume they have the same behavior in terms of representing an any
- I bet we get these primitives for every Ruby GraphQL
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The mutations already exist in convection-- might need some updating but the basic functionality is there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mergedSchema.js
Outdated
fetch, | ||
uri: process.env.CONVECTION_GRAPH_URL, | ||
headers: { | ||
Authorization: `Bearer ${process.env.CONVECTION_TOKEN}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sweir27 I guess this actually needs to be specified by the client at runtime, as it differs per user, right? I think that should be easy using the ‘link’ API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the convection loaders have the ability to convert a grav token into a convection token. We'll need something like this for at least convection / impulse
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooohhh, we can probably just access those data loaders (including the token loader) during stitching resolving. Will take a look 👍
mergedSchema.js
Outdated
return { | ||
...context.headers, | ||
headers: { | ||
// authorization: `XApp ${config.GRAVITY_XAPP_TOKEN}`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sweir27 Does this look like what you had in mind (to allow MP to fetch Convection’s schema on startup)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We chatted about this - it's a hard one, maybe looking into giving Metaphysics 2 client apps, one for the APIs we forward onwards and another for the server to server access. Then the server2server one can have privileged access for getting the schema.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just read the discussion in #platform-humans and that makes sense to me. My only question is how easy it is for MP to identify on each incoming request if the user has access to the more privileged app role? cc @mzikherman
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But once we have that we can just load 2 stitches schemas and execute against the correct one depending on the access the authenticated user has.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, one thing that comes to mind is that in the more privileged stitched schema we would probably have stitching code that refers to the more privileged fields and thus leak schema details through MP being OSS 🤔
One solution I can think of is to have another app that imports all of MP and then extends the schema with privileged details and make that closed source? Probably only slightly more cumbersome, mostly in having to remember to add fields to the right app.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed a new issue to discuss further #810
mergedSchema.js
Outdated
if (tokenLoader) { | ||
return tokenLoader().then(({ token }) => { | ||
return { | ||
...context.headers, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note to self, this is wrong, it needs to add all headers to the below object instead.
mergedSchema.js
Outdated
} | ||
` | ||
|
||
return mergeSchemas({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This basically happens once during start up of MP, so one thing to consider is, lets say Convection's GraphLQ schema got updated and there was a new field, we'd ONLY get that update if we restart MP, right?
For now we might be fine with manual restarts of MP, but eventually if this became more problematic (as we add more and more services) it might make sense to use event stream for reloading schema. Each time an app updated it schema we'd refresh this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct 👍 And that sounds like a good solution to keep in mind.
mergedSchema.js
Outdated
headers: { | ||
...context.headers, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we don’t even really want to pass on (all) headers that a client specifies? I can imagine that we do want e.g. the request ID that @sweir27 had been working on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we'd want the request ID - but that's probably it (at least in convection I've not seen any header handling outside of auth)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: with #818 there's more headers we want to pass through
index.js
Outdated
...loaders, | ||
}, | ||
formatError: graphqlErrorHandler(req.body), | ||
validationRules: [depthLimit(queryLimit)], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Wasn't aware of GraphQL depth limit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I… dislike it so much 😞 It’s such an arbitrary 🔨
Of course I understand why we’d want it at the moment, but in the long term I see only allowing persisted queries in production (or limiting depth query for not persisted queries) as a better solution.
mergedSchema.js
Outdated
function createConvectionLink() { | ||
const httpLink = createHttpLink({ | ||
fetch, | ||
uri: `${process.env.CONVECTION_API_BASE}/graphql`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like this uri
might be the main piece of convection-unique code here. I wonder if there's an opportunity to wrap this boilerplate in a way that'd make it easy to do just point to the urls we're stitching.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other Convection specific code is using the convectionTokenLoader
. I agree that this can probably be made more generic, but was punting on that until we add another service.
Very exciting stuff 👏 I think bootstrapping stitching inside MP with the intent to whittle it down to mostly just stitching code and having logic in downstream GraphQL APIs is a great strategy. This is a very exciting step towards a beautiful microservice story where MP is a single orchestrator of downstream GraphQL APIs and we can eliminate redundant fields/logic/caching layers. It'd also be cool to consider writing a HAL > GraphQL stitching library that could introspect the Hypermedia links (and JSON schemas?) and generate GraphQL schemas + resolvers for it 🤔 💥 . One general comment not to block this PR is that I'm noticing a lot of code that I struggle to follow in these glue code layers. e.g. There's a lot of factory/dependency-injection-like patterns being used in the loader/stitching layers and I wonder if there're simpler ways to organize these modules. It seems like the tools encourage this as well, so maybe they're partly responsible. In any case, just a general comment—not sure how to make it better myself. 👏 amazing work! |
artsy/convection#122 is pretty close to being ready |
Hey, just found this from the PR here: ardatan/graphql-tools#484 Can you help me figure out how that function existing is messing things up? I see the PR just deletes it, but wanted to learn more. |
@stubailo The test in that PR should illustrate it well if you comment out the deletion change. In short, the problem is that after selecting a subset of fields from the original schema the function in the merged schema may not return For example, a {
artist(id: “banksy”) {
name
}
} |
7057776
to
a3c3c13
Compare
return { | ||
"X-Request-Id": requestID, | ||
"x-datadog-trace-id": traceId, | ||
"x-datadog-parent-id": parentSpanId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@saolsen I noticed that these last two are not strings. I’m sure that all entries in response.headers
get coerced to strings by Express before the response is sent to the client, but it was slightly unexpected to see these objects in there. Do you think we can add toString()
calls here?
This is currently using my fork that has ardatan/graphql-tools#484
a3c3c13
to
ab06a51
Compare
I don’t know how well that would work when we want to design the schema without necessarily following the same shape in which these endpoints return data. Maybe an example of what you have in mind could help?
It’s true and I agree that it can be a bit much when reading for the first few times. It does work nicely with how graphql-js works and especially testing, though (no more need for rewiring). I’m on the fence, because thus far everybody ended up understanding how it works. |
return rightType | ||
}, | ||
resolvers: mergeInfo => ({ | ||
Submission: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alloy - can you explain how adding in additional schemas will work in relation to the above line? Is this a namespace?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No this is not a namespace, it’s the way in which schemas are created using graphql-tools
https://github.com/apollographql/graphql-tools/blob/master/README.md. In this case the code you’re referring to is the implementation for the schema extension just above (in IDL).
OK, it's time - let's give this a shot. |
This bootstraps schema stitching inside MP. In the below example you’ll see the use of MP’s and Convection’s schemas combined and used.
TODO