Skip to content
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

Merged
merged 15 commits into from
Nov 23, 2017
Merged

Neo metaphysics #809

merged 15 commits into from
Nov 23, 2017

Conversation

alloy
Copy link
Contributor

@alloy alloy commented Nov 14, 2017

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.

{
  # This root field comes from MP’s schema.
  artist(id: "marina-abramovic-1") {
    name
  }
  # This root field comes from Convection’s schema.
  submission(id: 186) {
    # This field exists in Convection’s schema
    artist_id
    # This field is generated in the `mergedSchema` module by fetching
    # an `Artist` from MP’s schema using the above `artist_id` field.
    artist {
      name
    }
  }
}
{
  "data": {
    "artist": {
      "name": "Marina Abramović"
    },
    "submission": [
      {
        "artist_id": "marina-abramovic-1",
        "artist": {
          "name": "Marina Abramović"
        }
      }
    ]
  }
}

TODO


@alloy
Copy link
Contributor Author

alloy commented Nov 14, 2017

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.

@alloy
Copy link
Contributor Author

alloy commented Nov 14, 2017

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}`))
})
Copy link
Contributor Author

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)
Copy link
Contributor Author

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>") {
    # ...
  }
}

Copy link
Contributor Author

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? 🤔

Copy link
Contributor Author

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:

  1. Add the required root field to the original schema.
  2. Merge the schemas and setup the delegator to use the root field.
  3. 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",
Copy link
Contributor Author

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
Copy link
Contributor Author

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 🤔

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, and indeed the mutations are already automatically available in the stitched schema, I haven’t tried them yet, though.

screen shot 2017-11-14 at 19 16 18

@alloy alloy requested a review from saolsen November 14, 2017 18:07
mergedSchema.js Outdated
fetch,
uri: process.env.CONVECTION_GRAPH_URL,
headers: {
Authorization: `Bearer ${process.env.CONVECTION_TOKEN}`,
Copy link
Contributor Author

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.

Copy link
Contributor

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

Copy link
Contributor Author

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 👍

@alloy alloy requested review from dblock and ashkan18 November 14, 2017 18:45
mergedSchema.js Outdated
return {
...context.headers,
headers: {
// authorization: `XApp ${config.GRAVITY_XAPP_TOKEN}`,
Copy link
Contributor Author

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)?

Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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,
Copy link
Contributor Author

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({
Copy link
Contributor

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.

Copy link
Contributor Author

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,
Copy link
Contributor Author

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.

Copy link
Contributor

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)

Copy link
Contributor

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)],
Copy link
Contributor

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.

Copy link
Contributor Author

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`,
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@craigspaeth
Copy link
Contributor

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!

@orta
Copy link
Contributor

orta commented Nov 16, 2017

artsy/convection#122 is pretty close to being ready

@stubailo
Copy link

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.

@alloy
Copy link
Contributor Author

alloy commented Nov 18, 2017

@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 true if the selection doesn’t satisfy the data it’s looking for.

For example, a isTypeOf implementation for the Artist type in the merged schema that looks for the data to contain name and birthday keys will fail to pass data returned from the original schema with the following query:

{
  artist(id: “banksy”) {
    name
  }
}

@alloy
Copy link
Contributor Author

alloy commented Nov 22, 2017

@saolsen Did a bit of refactoring re tracer work ff4083f and I also pass these IDs along to Convection a3c3c13

return {
"X-Request-Id": requestID,
"x-datadog-trace-id": traceId,
"x-datadog-parent-id": parentSpanId,
Copy link
Contributor Author

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?

@alloy
Copy link
Contributor Author

alloy commented Nov 22, 2017

@craigspaeth

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 🤔 💥 .

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?

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.

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.

@alloy alloy assigned orta and unassigned mzikherman Nov 22, 2017
return rightType
},
resolvers: mergeInfo => ({
Submission: {
Copy link
Member

@damassi damassi Nov 23, 2017

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?

Copy link
Contributor Author

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).

@orta
Copy link
Contributor

orta commented Nov 23, 2017

OK, it's time - let's give this a shot.

@orta orta merged commit 5cd73c2 into master Nov 23, 2017
@orta orta deleted the neo-metaphysics branch November 23, 2017 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants