-
Notifications
You must be signed in to change notification settings - Fork 88
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
feat: structured output #676
feat: structured output #676
Conversation
56e9291
to
eaeba15
Compare
e96872e
to
2c91188
Compare
280b867
to
e21dca8
Compare
eb85fe2
to
7d03132
Compare
We updated instances of composition errors in the UI with build errors. I think we would want the json here to also be build errors? |
Agree with @jsegaran on making space for 'build' errors. For 2 is going to get more complex with some of the changes we're introducing to managed federation. In some cases, a customer's supergraph changes won't be But even short of that, I think I want a clear success boolean for the immediate goal of publishing the subgraph since that does NOT depend on build being successful NOR the built supergraph schema being published. This example, provided above, highlights the issue:
In this case the response says Example: I don't know how well that would work with CI/CD platforms though? |
What this boils down to is "how do we define success?" and I think the answer to that question will vary by customer, by graph, and even by variant. In some cases my goal is simply to publish my subgraph and I don't care whether the supergraph is successfully built or launched and published. But in others success hinges on the supergraph build succeeding, or on the supergraph being published. Separately, I'd also like to get back some of the Studio metadata for the publish (e.g. the build_id and the launch_id) and maybe even links I can follow to view the change in Studio or retrieve the updated supergraph schema. That's not necessary for structured output to ship but I'd consider it a nice-to-have. |
Few fly-by thoughts:
|
Makes sense to me!
How do we feel about adding a "type" field to those type of errors. I'm thinking it would look like this: {
"data": {
"success": false
},
"error": {
"message": "Encountered 3 build errors while trying to compose subgraph \"products\" into supergraph \"rover-supergraph-demo@test\".",
"build_errors": [
{
"message": "[inventory] Product.id -> is marked as @external but is not used by a @requires, @key, or @provides directive.",
"code": "EXTERNAL_UNUSED",
"type": "composition"
},
{
"message": "[products] Product -> A @key selects iddddd, but Product.iddddd could not be found",
"code": "KEY_FIELDS_SELECT_INVALID_TYPE"
"type": "composition"
},
{
"message": "[inventory] Product -> extends from products but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are:\n\t@key(fields: \"iddddd\")\n\t@key(fields: \"sku package\")\n\t@key(fields: \"sku variation{id}\")",
"code": "KEY_NOT_SPECIFIED",
"type": "composition"
}
],
"code": "E029"
}
}
This is something we've already decided for
Yeah... I actually just implemented this incorrectly!
As for making
Would you mind making a new issue for this? We'll definitely need changes to our queries for this and I'd rather track that work separately. It's pretty easy to add new fields to the JSON output without breaking people, and much harder to take them away.
Few thoughts here:
On the whole I don't think it hurts anything at all if we output a
I think
Interesting note! It's not really meant to be GraphQL output, but I can see how that could be confusing given this is a GraphQL tool! I have it as
The way Rover is structured, you can only ever return a single error type. This error type can be caused by any number of other errors, but all errors must be combined into a singular, top-level error type. This is why the top level is However, that is an implementation detail! We could definitely serialize errors any way we like, however I think that nesting them like we have them makes things a bit more flexible and predictable. Scripts just have to check for null, not for null and an empty array.
Not gonna happen pal. |
Thanks @EverlastingBugstopper !
That looks great to me. What do you think @jsegaran?
Okay great, let me know if Studio is actually returning an error in this case though. I think I remember some funkiness here.
Yeah keeping it as a simple boolean makes sense. But I think I want booleans for some of the other statuses too. This stuff is going to get more complex with time, particularly when we start to introduce non-blocking composition errors/warnings. I know this has been on @prasek 's mind. In that future we may have cases where there are errors/warnings but composition still succeeded. To prevent breaking changes and/or confusion it may be good to introduce a boolean for composition success now, but that's a bit beyond my wheelhouse and I'll defer to @prasek.
Is the key dependent on whether I'm trying to create vs update? If so it feels a little strange to me that I'd need to know whether my |
Currently we'll have one additional boolean, which is
|
1e8cec2
to
12f4261
Compare
@EverlastingBugstopper @abernix @jstjoe
Perhaps I've typed
Longer term we'll probably want to represent the states for a {
"data": {
"status": {
"subgraph": "published",
"supergraph": "composition_error | built | published"
}
}
} But perhaps it's best to keep things simple (a) until we have those defined in the public API and (b) have specific use cases where they're needed in the structured output, so we can deliver smaller incremental additions without breaking changes as the public API is finalized? Along those lines I'd ask:
If
{
"exit_code": 1
"data": null,
"error": {
"code": "E029"
"type": "composition"
"message": "Encountered 3 build errors while trying to compose subgraph \"products\" into supergraph \"rover-supergraph-demo@test\".",
"details": [
{
"message": "[inventory] Product.id -> is marked as @external but is not used by a @requires, @key, or @provides directive.",
"code": "EXTERNAL_UNUSED",
},
{
"message": "[products] Product -> A @key selects iddddd, but Product.iddddd could not be found",
"code": "KEY_FIELDS_SELECT_INVALID_TYPE"
},
{
"message": "[inventory] Product -> extends from products but specifies an invalid @key directive. Valid @key directives are specified by the originating type. Available @key directives for this type are:\n\t@key(fields: \"iddddd\")\n\t@key(fields: \"sku package\")\n\t@key(fields: \"sku variation{id}\")",
"code": "KEY_NOT_SPECIFIED",
}
]
}
} Circling back on 3. is it important for structured output to have a rich data/domain model vs. a simpler presentation model? If we think a simpler presentation model will work for the initial release of rover structured output, perhaps we start with that and then once we have a documented public API with a stable domain model we can expose the domain model. If we expose the domain model today, it's likely we'll be forced to introduce breaking changes as the public API gets finalized, so that argues for keeping the structured output in For example, a presentation model lets us encapsulate the domain model and provides a simpler consumption model: {
"exit_code": 0
"display": {
"message": "Subgraph inventory published successfully to supergraph-router@dev"
"details": [
{
"message": "Publishing SDL to supergraph-router:dev (subgraph: inventory) using credentials from the default profile."
},
{
"message": "The 'inventory' subgraph for the 'supergraph-router' graph was updated"
},
{
"message": "The gateway for the 'supergraph-router' graph was NOT updated with a new schema"
},
},
"error": null
} Then as the public API is released we could add a richer domain model: {
"exit_code": 0
"display": {
"message": "Graph published successfully to monolith@dev"
"details": [ ... ]
},
"data": {
"api_schema_hash": "123456",
"field_changes": {
"additions": 2,
"removals": 1,
"edits": 0
},
"type_changes": {
"additions": 4,
"removals": 0,
"edits": 7
},
}
"error": null
} Note that for federated graphs in particular we talk about two types of federated graph:
|
I definitely like the simpler presentation model @prasek. With the goal of minimizing breaking changes, and limited bandwidth on gravity to take on a big domain modeling exercise for our future features (both in Studio's data model and to the composition model) I think this is a winning approach. |
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.
So much good work in here! Really well done, Avery!
Agree 🙏 if we're comfortable exposing the domain model now and can maintain this interface for a while, so user scripts can stay stable regardless of rover minor version even as the api might change, we could do a |
This seems fine, but since we only exit with code
How about
Yeah the purpose for introducing the re: simpler presentation model - I'm pretty strongly opposed to the If we have a top level
I'm... not exactly sure how this is relevant here? Could you elaborate a bit on how this relates? Is it that the Studio API itself is currently a backend-focused graph and that affects our implementation here? |
12f4261
to
1797045
Compare
If were not adding a top-level
Just by analogy
If the goal of structured output is get an early version of the public API for tighter integration then the If the goal of structured output is to (a) determine basic pass/fail (b) capture errors in a simple/generic way (c) format the output in a slightly more formatted way vs. stdout, then As mentioned above, the end-state could include both so consumers could pick the simplest programming model for their use cases. |
This commit adds a global `--json` flag that structures the output of Rover like so: **success:** { "data": { "sdl": { "contents": "type Person {\n id: ID!\n name: String\n appearedIn: [Film]\n directed: [Film]\n}\n\ntype Film {\n id: ID!\n title: String\n actors: [Person]\n director: Person\n}\n\ntype Query {\n person(id: ID!): Person\n people: [Person]\n film(id: ID!): Film!\n films: [Film]\n}\n", "type": "graph" } }, "error": null } **errors:** { "data": null, "error": { "message": "Could not find subgraph \"products\".", "suggestion": "Try running this command with one of the following valid subgraphs: [people, films]", "code": "E009" } }
➕
This is a real risk and a perfectly validated reason give pause when contemplating including error strings in output. Particularly if they don't have error codes. In fact, I recall that prior to introducing error codes for every error message, the Node.js Foundation could not change any error message text because every change was considered a breaking change. Thankfully, they now have error codes for everything and they can instruct people to not parse error messages. Parsing error messages is a Bad Idea. However, we're not going to stop people from doing it. I think the important thing to do here is to make sure that the user doesn't resort to parsing error messages because they have no other choice and that they feel like it was their bad choice when choosing to do it, not a limitation of the system they're extracting data from.
A supporting use-case here is that we've heard from customers who actually just want to re-structure the output we're providing in our CLIs to ship its output from CI workflows into to other tools — e.g., Slack / ChatOps / Logging pipelines, etc. I think of this as: humans have familiarity with particular opinionated messaging that they see when running locally and they enjoy having some of that mirrored to automated workflows. |
Agree. |
We already have both error codes and error messages in structured output, so we should be OK there - I think Phil was talking about a top level I think it'd be fine to have both, but if we introduce just display and no structure, then I think we'd run into pretty much the same thing that happened to Node with people parsing those strings to get at the underlying structure (defeating the purpose of structured output).
Is this something that you think they'd need I'm not 100% opposed to adding the top level TODO:
|
Making good progress! I'll finish up the next TODOs on Monday. |
Agree this is perfect.
Agree. Think we're good on the error codes.
Agree. Supporting a simple and consistent UX to do this basic use case seems like the most important thing.
All
Agree we'll want both
For |
Yeah, sorry, didn't mean to imply that we weren't! We are good on this front!
Agree on both quotes.
I think providing structure for display elements is still better than necessitating someone parse out our own display contents.
I wouldn't over-index on which one is the simple/deeper approach. Many folks probably would never use the It sounds like we want both. We could introduce |
Consider me convinced! I don't think it should be too much extra work to get in the display structure and will probably lead to some cleaner code regardless. I'll try to get that in and we can cut a beta this week. I've also now mapped everything so that if |
After some investigation, I've uncovered some kinda gnarly design questions with the |
All TODOs have been addressed and the top level comment has been updated to reflect the changes to the JSON structure since the initial PR was opened. |
* chore: refactor subgraph check This commit does a lot of heavy lifting for the pending rebase. 1) Creates new input and output types in rover-client for subgraph check 2) Moves GitContext out of rover::utils to rover-client::utils 3) Creates error code E029 for composition errors 4) Styles cloud-composition errors like harmonizer * chore: refactor subgraph fetch (#575) * chore: refactor subgraph publish (#630) * chore: refactor config whoami (#633) * chore: refactor subgraph delete (#639) * chore: refactor subgraph list (#640) * chore: refactor subgraph introspect (#641) * chore: refactor graph introspect (#643) * chore: refactor release update checker (#646) * chore: begin adding shared types and consolidate check operations (#652) * chore: move GraphRef to rover-client (#664) * chore: refactor the rest of rover-client (#675) * chore: do not re-export queries * chore: finish wiring OperationCheck error * chore: adds graphql linter (#677) * fix: graph_ref -> graphref * feat: structured output (#676)
This fixes #285 by adding a global
--json
flag that structures the output of Rover (and should only be merged after #677)I think maybe we should cut a beta or a release candidate from this branch so that we can maybe gather some feedback on the structure of the output before we commit to never breaking the API?
Example Output
All errors look very similar, currently they only have the string description and the error code like so (unless there are build errors, which are special cased and outlined below):
the rest of the commands are outlined here with sample data:
rover supergraph fetch
rover supergraph fetch
(edge case where there has never been a successful build)rover supergraph compose
rover subgraph list
{
"json_version": "1.beta",
"data": {
"subgraphs": [
{
"name": "subgraph one",
"url": "http://localhost:4001",
"updated_at": {
"local": now_local,
"utc": now_utc
}
},
{
"name": "subgraph two",
"url": null,
"updated_at": {
"local": null,
"utc": null
}
}
],
"success": true
},
"error": null
}
rover graph check
androver subgraph check
(with some operation check failures)rover subgraph check
(with build errors)rover graph publish
rover subgraph publish
(no build errors)rover subgraph publish
(with build errors)IMPORTANT NOTE
If you run
rover subgraph delete ${graph_ref} --name ${subgraph} --json
without the--confirm
flag, it will still do a dry-run first and confirm with the user before continuing. That dry run response will not return build errors in JSON format as it's an interactive command, but the subsequent responses (or if they did pass--confirm
) will return the actual JSON.i.e. this will still happen, and build errors will be printed as normal strings if
--json
is passed and--confirm
is not. I considered adding another--dry-run
flag here, but thought it was probably out of scope for this change.rover subgraph delete
(no build errors)rover subgraph delete
(with build errors)rover config list
rover subgraph introspect
androver graph introspect
rover explain E001
rover docs list
Any command that does not have any structured output will return the following JSON if there are no errors:
The following commands do not have any structured output:
rover info
,rover config auth
,rover config clear
,rover config delete
.The following commands also do not have any structured output, but probably should:
rover config whoami
rover docs open
,rover install
, androver update check
. These need some refactoring in order to satisfy the requirements for structured output but for now I think the default success structure will be OK. Any additional keys will not break additional scripts (that aren't like, checking for exact string matches instead of properly parsing the JSON).