Subchapter 10.13
graphos-factory-core/references/mapping-language.mdMarkdown31 KBView on GitHub
JSONSelection: the syntax inside selection, body, queryParams, path,
isSuccess and errors, and inside the {…} expressions of a header value or
a URI template, which are otherwise string templates. It turns the JSON an API sends into the shape a GraphQL type
declares, with no resolver code.
The one idea everything else follows from: a selection is a shape, not a query. Composition has to determine the output shape by reading the string, without ever seeing a response. That is why a group selection needs a sub-selection it can name keys from, why methods are a fixed set rather than arbitrary code, and why an untypable payload has to become a documented scalar instead of a guess.
This file is written for the target toolchain: connect/v0.4, composed at
federation_version: =2.15.2, served by Apollo Router 2.17. Where
connect/v0.3 behaves differently, the difference is marked At v0.3,
because workspaces created before the move may still be pinned at
federation_version: =2.12.0 with connect/v0.3. A workspace can also link
an older federation/ spec than its pin, recorded as federation_spec_version.
Check .factory/workspace.yaml
before trusting any example here, and read
Moving a workspace from v0.3 to v0.4 before changing the pin.
The grammar is in the appendix, so this
file answers a syntax question without a network round trip. For the grammar
explained rule by rule, with railroad diagrams and the reasoning behind each
production, read
json_selection/README.md (opens in a new tab)
in the router repository, but take the method list from
Which methods run. Read
connectors-language.md for the directives that carry
these strings.
Three version numbers matter, and they are independent:
| Setting | Target | Where it lives |
|---|---|---|
federation_version | =2.15.2 | supergraph.yaml, pinned by workspace.yaml |
@link(url: ".../federation/vX") | v2.15 | the schema |
@link(url: ".../connect/vX") | v0.4 | the schema, pinned by workspace.yaml |
The router’s version (2.17) and federation_version are separate lines. 2.15.2
was the newest composition release when this file was measured; 2.15.3 and
2.15.4 have not been measured here.
Apollo documents connect/v0.4‘s floor as composition 2.14.1 and Apollo
Router 2.15.0; below either, use connect/v0.3 (composition 2.12, Router
2.8). The table records what composed here, which is not the supported
floor: the v0.4 link alone composes from 2.13.0. A federation_version
accepts a federation/ link no newer than itself:
federation_version | newest federation/ link | connect/v0.4 |
|---|---|---|
| 2.12.0 | v2.12 | no |
| 2.13.0 | v2.13 | yes |
| 2.14.0, 2.14.3 | v2.14 | yes |
| 2.15.0 | v2.14 | yes |
| 2.15.2 | v2.15 | yes |
yes means the link composes, not that all of this file’s grammar does:
composed at 2.14.0, comma-separated lists, bare numbers, unwrapped ?? and
?! chains and bare-brace bodies are parse errors. All of them compose at
2.15.1 and 2.15.2.
A federation/ link newer than the pin fails composition; see
connectors-language.md.
There is no array syntax. A sub-selection after an array-valued path applies to
every element, the way a GraphQL selection set does. Every row below is
executed output for one response whose author.articles is a two-element array:
| Selection | Result |
|---|---|
author.articles.title | ["T1", "T2"] |
author.articles { title } | [{title:"T1"}, {title:"T2"}] |
author.articles.byline.place | ["P1", "P2"] |
author.articles { title place: byline.place } | [{title, place}, …] |
author.articles { td: { title date } } | [{td:{title,date}}, …] |
If the value turns out not to be an array, the same selection yields one result instead of a list. The string does not change.
Paths inside the braces start from each element, so
author.articles { name: author.name } gives name: null for every article.
To reach the parent, bind it first:
author->as($a).articles { name: $a.name } gives name: "Ben" on each.
A selection list is separated by whitespace or by commas, not both:
a, b, c and a b c are fine, and a, b c fails with a message saying the
list is comma-separated. Method arguments always take commas. The examples in
this file use whitespace, which is the only form v0.3 accepts.
At v0.3: commas in a selection list are a parse error.
Most REST list endpoints wrap the payload. Select through the wrapper with $.
and keep siblings alongside it:
selection: """
$.data { id name }
nextCursor: meta.next_cursor
"""Given {"data":{"id":7,"name":"n"},"meta":{"next_cursor":"c1"}} this produces
{id: 7, name: "n", nextCursor: "c1"}: the data wrapper is gone and
nextCursor sits beside the fields it wrapped.
This is the one that silently produces the wrong shape.
author { name } # keeps the key: { author: { name } }
some.nested.path { a b c } # merges: { a, b, c }A single key with a sub-selection contributes that key to the output. A
path of more than one step does not, so its sub-selection merges into the
parent. Executed, both in one selection, the result is
{author: {name: "Ben"}, a: "A", b: "B", c: "C"}.
Two consequences worth holding on to:
author { name } keeps author;
author.details { name } does not.$.author { name }. To keep
a key on a longer path, alias it: abc: some.nested.path { a b c }.When the response has authorID and authorName but the schema wants a nested
author, alias a sub-selection. The alias is required, because the group would
otherwise be anonymous:
selection: """
postID
title
author: {
id: authorID
name: authorName
}
"""A very common REST shape, and three tokens:
selection: """
id
friends: friend_ids { id: $ }
"""{"id":123,"friend_ids":[234,345,456]} becomes
{id: 123, friends: [{id:234},{id:345},{id:456}]}. Inside the sub-selection
$ is each element in turn.
The target field has to be a typed object list. A { … } group cannot target a
<Prefix>_JSON scalar: composition rejects it with
GROUP_SELECTION_IS_NOT_OBJECT. The JSON escape hatch in
schema-authoring.md does not absorb a sub-selection.
->keysToCamelCase renames every key of an object, so one line replaces a
block of camelCase: snake_case aliases:
selection: """
$->keysToCamelCase { id loginName fullName }
"""turns {"id":1,"login_name":"ben","full_name":"Ben Newman"} into
{id: 1, loginName: "ben", fullName: "Ben Newman"}. The sub-selection is not
optional: without it composition cannot see which fields the method provides,
and every field of the type is CONNECTORS_UNRESOLVED_FIELD.
->keysToCamelCaseDeep recurses: {"outer_key":{"inner_key":1}} becomes
{outerKey: {innerKey: 1}}.
Aliases stay the right tool when the GraphQL name is not the camelCase of the wire name. naming.md has the rules.
In a factory workspace, keep the aliases for now: the tools do not yet
read through ->keysToCamelCase. graphos-factory-core reconcile reports every
field it renames as unmapped; source-coverage reports every field under a
root $->keysToCamelCase { … } as unaccounted, and every field under a
nested one as unresolved.
A oneOf whose members a wire value tells apart (usually a discriminator)
becomes a union, or an interface when the members share fields. One form
composes: a ... spread of ->match on the discriminator, with one arm per
member, each an object carrying a literal __typename:
type Query {
media: [Media] @connect(source: "api", http: { GET: "/media" }, selection: """
id
title
... kind->match(
["book", { __typename: "Book", pages: pages }],
["movie", { __typename: "Movie", minutes }],
[@, null]
)
""")
}This works for union Media = Book | Movie and for interface Media. pages
and minutes read from the element the spread sits in, and { minutes } is
the shorthand for minutes: minutes. An arm can also be written
$ { __typename: $("Book") pages }.
What composition enforces, at =2.15.2:
| Written | Result |
|---|---|
a flat list, id title pages minutes | SELECTED_FIELD_NOT_FOUND: minutes does not exist on Book |
__typename: kind->match(["book", "Book"], …) beside flat fields | the same. A __typename outside a spread narrows nothing |
the shared fields only, id title | CONNECTORS_UNRESOLVED_FIELD for Book.pages and Movie.minutes |
| a member with no arm | No matching shape found for selection. Attempted 1 different shape variations |
| an arm with a field its member lacks | the same message |
an arm with no __typename, one that names a non-member, or one computed (__typename: kind) | INTERNAL: An internal error has occurred |
A field outside the spread has to exist on every member.
Only at a connector’s own return type. A union or interface field inside
another type’s selection (payment { ... type->match(…) } on
Order.payment: PaymentMethod) fails with GROUP_SELECTION_IS_NOT_OBJECT.
Give that field its own @connect on Order, keyed by $this.id, and put the
spread in it: $.payment { ... type->match(…) }. That costs one request per
parent. When nothing can re-fetch the value, it stays a documented JSON scalar
(schema-authoring.md).
An unmatched discriminator is a silent null. Executed through Router
2.17.0 with a podcast element the arms do not list:
[@, null], the element is null and errors is empty;null too. An interface’s result
then depends on the query: { media { id title } } returns the podcast,
and adding __typename or a fragment turns it into null;[Media!] list, the whole list is null, reported only in
extensions.valueCompletion;[@, { __typename: "Book", … }], labels the
podcast a Book, with pages: null.Write [@, null], keep the element type nullable, and give e2e one case per
member plus one with a discriminator value the arms do not list.
Without a discriminator, match on a key only some members send, through a
fallback: ... $(radius ?? "none")->match(["none", { __typename: "Rect", width, height }], [@, { __typename: "Circle", radius }]). The method does not run on
a missing key, so radius->match([null, …], …) yields null for every
Rect.
At v0.3 a union or interface is CONNECTORS_UNSUPPORTED_ABSTRACT_TYPE,
and ... fails with “Spread syntax (…) is not supported in connect/v0.3
(use connect/v0.4)”. A
polymorphic payload is a documented JSON scalar there.
What the skill’s tools see. rover connector test asserts a
spread correctly, and an unmatched element fails the case with
Method ->match did not match any [candidate, value] pair. The factory
readers parse this form, with each arm reading from the object the spread
sits in:
source-coverage maps the discriminator and every arm’s fields;reconcile reports an excluded field an arm maps;wire-enum-drift and int-overflow check an arm’s leaves against
its __typename member. The source property comes from the oneOf variant
whose discriminator enum holds the arm’s candidate, so a @ arm is not
checked;scaffold asks for __typename and one ... on Member { … } fragment per
arm, with the fields beside the spread repeated in each.Any other spread (... $.x { … }, ... ->echo(…), a ->match arm that is
neither an object nor null) reads unresolved in source-coverage,
naming the spread, for every path under that object the selection does not
otherwise map.
At v0.4 the right-hand side of an alias is any expression, so a literal can be written bare:
kind: "Book" # "Book"
five: 5 # 5
flag: true # true
none: null # null
fb: $.missing ?? "x" # "x"$( … ) still works and still means “this is a literal expression”, and
$."…" reads a property whose name needs quoting. Both are unambiguous at
every version, so prefer them in anything that has to survive a move between
versions. The skill’s own tools need them too: graphos-factory-core reconcile
reads a bare true, false or null as a property path (“the connector
selects … true”), and $(true) as a literal.
At v0.3 the same text means something else, silently. A quoted string on
the right of an alias is a property name, and so are true, false and
null. Executed against {"Book": "…", "true": "…", "false": "…", "null": "…"}:
| Selection | v0.3 | v0.4 |
|---|---|---|
looksLiteral: "Book" | the Book property | "Book" |
bool: true | the true property | true |
no: false | the false property | false |
nul: null | the null property | null |
properLiteral: $("Book") | "Book" | "Book" |
propertyForm: $."Book" | the Book property | the Book property |
Neither version errors, so moving a schema from v0.3 to v0.4 can change what
it returns; connect-migrate finds such
sites in selection, and the other mapping arguments are checked by hand. At
v0.3 a bare number (a: 5) or an unwrapped operator chain (a: $.x ?? "d")
does not parse; wrap it in $( ).
| Variable | Bound to | Where |
|---|---|---|
$ | the value the enclosing { … } received, or the response root | selection, errors |
@ | the value a method is operating on, rebound per element inside ->map | method arguments |
$args | the field’s GraphQL arguments | root-field connectors |
$this | the parent object, so sibling fields are reachable | non-root types |
$batch | the collected keys | batch connectors |
$config | values from router config | anywhere |
$env | the router process environment | anywhere |
$context | the router’s request context, e.g. the caller’s own credentials | anywhere |
$request.headers | the client request’s headers; each value is a list | anywhere |
$status | the HTTP status | selection, errors |
$response.headers | the API response’s headers; each value is a list | selection, errors |
$ does not rebind inside a method argument, which is what makes
$.first->and($.second) work: $.second still reads from the enclosing
object, not from the result of $.first. Use @ when you do want the value
flowing through the method.
rover connector test injects $args, $this, $batch, $config and
$context, but not $env. scripts/unit.sh rewrites static
{$env.NAME} to {$config.NAME} in a temporary copy and the suites supply
config.common.variables.$config. Only a real router run proves $env.
Written value->method(args), chainable left to right, and always named here
with the leading ->. The set is closed.
The router that executes the connector decides which methods exist, not
the connect version. Routers 2.17 and 2.18 run the same 39 public
methods, whatever the composition pin. ->withError and ->withWarning
are in no 2.x release up to 2.18.0, so do not use either. The connect
version decides how an existing method behaves: a breaking change to a
method applies only from the connect version that introduced it, so a
schema opts in by moving its link. ->typeof, ->matchIf,
->has, ->keys and ->values are not exposed at any release, so never reach
for them however they are documented elsewhere. An unavailable method is
neither a build error nor a runtime error: the field comes back null.
| Method | Does | Example |
|---|---|---|
->first | first element | colors->first |
->last | last element | colors->last |
->get(i) | element at index; also a string char, or an object property by name | items->get(2), user->get("name") |
->slice(a, b) | sub-list, or substring | colors->slice(0, 2), $("abcdef")->slice(1, 3) is "bc" |
->size | list length, string length, or property count | colors->size |
->filter(cond) | every element matching, @ is the element | users->filter(@.active->eq(true)) |
->find(cond) | the first element matching, else nothing | users->find(@.active->eq(true)) |
->map(expr) | apply to each element; on a non-list, a one-element list (27 gives [54]) | numbers->map(@->mul(2)) |
->entries | object to [{key, value}, …]; ->entries.key for just the keys | obj->entries |
->joinNotNull(sep) | joins a list of scalars, skipping nulls | $(["a", null, "b"])->joinNotNull(",") is "a,b" |
->eq ->ne ->gt ->gte ->lt ->lte compare against their argument and
return a boolean. ->in([…]) is true when the value is one of the listed
values; ->contains(v) is true when the list it is applied to holds v.
Note the direction: $(123)->in([123, 456]) and $([123, 456])->contains(123)
are the same question asked from opposite ends. ->and ->or take further
values, ->not inverts.
Combining conditions inside ->filter or ->find needs ->as. @ rebinds in
every method, so in @.active->and(@.age->gt(30)) the inner @ is the boolean
@.active, and the router returns null for the whole field with no error.
If the first condition is itself a method call (@.a->eq("x")->and(…)),
composition fails instead, with a misleading CONNECTORS_UNRESOLVED_FIELD
on every field of the type. Bind the element first:
adults: users->filter(@->as($u)->echo($u.active)->and($u.age->gt(30)))Use the ->as form unless the router the service runs on is 2.18.0 or
later. Before 2.18.0, a ->filter or ->find after another list-producing
method, followed by a sub-selection
(users->filter(@.active)->filter(@.age->gt(30)) { id name },
users->filter(@.active)->find(@.age->gt(30)) { id name }), composes, and
then the router will not start (“… all of its members are @inaccessible”).
Without a sub-selection (…->filter(…)->map(@.name)) the chain works.
| Method | Does | Example |
|---|---|---|
->split(sep) | string to list | $("a,b,c")->split(",") is ["a","b","c"] |
->trim, ->trimStart, ->trimEnd | strip whitespace | $(" hi ")->trim is "hi" |
A comma-separated header or field becomes a clean list with
tags->split(",")->map(@->trim).
->add ->sub ->mul ->div ->mod work on integers and floats.
->toString renders a scalar (null becomes ""; a list or object is an
error). ->parseInt takes an optional base, so $("ff")->parseInt(16) is
255. ->jsonStringify serialises a value to a JSON string, and ->jsonParse
reverses it:
$('{"x":1}')->jsonParse is {x: 1}, which unpacks an API that embeds JSON in
a string field.
| Method | Does |
|---|---|
->echo(x) | returns its argument, ignoring the input |
->as($var[, expr]) | returns the input unchanged and binds it (or expr) to $var for later in the chain: person->as($n, @.name)->echo($n) is "Ben" |
->match([a, x], [b, y], [@, z]) | value translation; a final [@, …] pair is the catch-all |
->keysToCamelCase, ->keysToCamelCaseDeep | see Renaming snake_case keys in bulk |
->match is how an enum crosses the wire boundary. Executed, with
status: "open":
state: status->match(['open', 'OPEN'], ['closed', 'CLOSED'], [@, 'UNKNOWN'])gives "OPEN", and an unlisted input gives "UNKNOWN". Without the [@, …]
catch-all an unmatched value drops the output key, and the router returns
null with no error.
naming.md explains when to translate and when to keep the wire
casing instead; graphos-factory-core lint checks the two agree
(wire-enum-drift).
These read like JavaScript’s optional chaining and nullish coalescing, and
mostly behave like them. Executed through Router 2.17 against
{"person": {"name": "Ben"}, "size": null, "n": 27}:
| Selection | Result | JavaScript counterpart |
|---|---|---|
$.nobody?.absent | null | nobody?.absent |
size?->jsonStringify | null | size?.jsonStringify() |
size->jsonStringify | "null" | none: the method runs on null |
n?->jsonStringify | "27" | n?.jsonStringify() |
$.nobody.absent ?? "fb" | "fb" | nobody?.absent ?? "fb" |
$.size ?! "fb" | null | size === undefined ? "fb" : size |
Where they differ from JavaScript:
$.nobody.absent is null with or
without the ?, where JavaScript’s nobody.absent would throw. On a path,
? marks the step you expect may be missing; it does not change the result.?-> is where ? matters. A method runs on null unless the step before
it is guarded: size->jsonStringify turns a JSON null into the four-character
string "null", and size?->jsonStringify keeps it null.? can close a path. $.person.absent? has no JavaScript form; it marks
the last step as optional.undefined. ?? falls back when the value is
null or missing, as in JavaScript. ?! falls back only when it is missing,
so an explicit null from the API survives. Use ?? for an error-message
fallback, where anything beats empty, and ?! when the API distinguishes
“not set” from “set to null” and the schema should too.a ?? b ?? c is fine;
a ?? b ?! c does not compose.A key that the selection drops and a key the API sent as null look the same
to the client: a nullable field reads null, and a non-null field nulls its
parent object.
At v0.3 an operator chain has to be wrapped: nn: $($.nullField ?? 'fallback').
The wrapped form works at both versions.
queryParams and body use the same language as selection; a header value
is a string template, and only its {…} expressions do. An entry whose value
is null is omitted from the request, which is how an optional argument stays
optional:
http: {
GET: "/widgets"
queryParams: """
limit: $args.limit
cursor: $args.cursor
"""
}A request body is a literal object. Keys are the API’s wire names, not the GraphQL ones, and a literal value can sit beside the arguments:
body: """
{
channel: $args.channel,
thread_ts: $args.threadTs,
kind: "message"
}
"""Executed with channel: "C1", threadTs: "123.4", that sends
{"channel":"C1","thread_ts":"123.4","kind":"message"}. A body written as
plain mapping lines (channel: $args.channel on its own) also works.
Inside the braces, separate the entries with commas. The skill’s tools read a
braced body as comma-separated: on gitea’s issue-create body, dropping the
commas takes source-coverage from 8 request fields mapped to 0.
At v0.3 the braces must be wrapped, body: "$({ a: $args.a })"; the
wrapped form is valid at both versions.
Use connect-migrate (opens in a new tab). It
parses every @connect(selection: …) with the real Connectors parser at both
the schema’s linked version and v0.4, and lists the sites whose meaning would
change (Literals) with the rewrite that preserves each one.
curl -fsSL https://raw.githubusercontent.com/apollographql/connect-migrate/main/install.sh | sh
puts connect-migrate in ~/.local/bin. The binaries are unsigned; where
that is a problem, build it from the router repository instead:
cargo build --release --bin connect-migrate --features connect-migrate in
apollo-federation/.connect-migrate agent-guide prints the procedure.
In short: connect-migrate analyze <workspace> -o manifest.md; apply the
manifest’s rewrites; put only its questions to the user; run analyze
again while the schema still links v0.3 until it reports nothing to
change; bump the @link last, after step 3.connect-migrate reads only selection, but
body, queryParams, path, isSuccess and errors, on @connect and
on @source, are parsed at the linked version too. In each, find every
quoted string, true, false or null after an alias, and rewrite it as
$."…" to keep the v0.3 property read, or as $( … ) if the literal was
meant. Leave headers out: a header value is a string template, not a
mapping.connect_spec: v0.4 and
federation_version: "2.15.2" in .factory/workspace.yaml, and =2.15.2
in supergraph.yaml. The federation/ link can move or stay: lint’s
federation-drift checks it against federation_spec_version when that is
set, and against federation_version when it is not. To move it, link
federation/v2.15 and delete federation_spec_version, which is only for a
link that differs from the plugin pin (workspace-contract.md),
or set it to "2.15". To keep it, set federation_spec_version to the
version it links, as the pilots do (federation/v2.12,
"2.12"). graphos-factory-core lint and render fail until the pins and the
schema agree.graphos-factory-core decisions, then
compose and run the e2e layer.A field is null and the response has no error. The router reports
mapping problems this way: an unavailable method, a ->match with no [@, …]
catch-all, ->and inside ->filter, a missing property, a non-null field left
empty. Check the selection against the payload, and assert the values you care
about in the e2e suite, which serves the schema through a real router. Results
from rover connector test are not evidence of what the router does.
Every response-derived field is null, but literals and $args come
through. The API or stub is answering without a JSON content type
(application/octet-stream, text/plain). Check the Content-Type before
debugging the selection.
GROUP_SELECTION_IS_NOT_OBJECT. A { … } group, or a method whose result
composition can see is an object list (raw: $.obj->entries), targets a
JSON-typed field. Type the object. On a union or interface field, see
Abstract types.
INTERNAL or No matching shape found for selection on a union or
interface. An arm lacks a literal __typename, names a non-member, selects
a field its member lacks, or a member has no arm
(Abstract types).
Extended Backus-Naur Form, as implemented at federation_version: =2.15.2.
Whitespace and # comments are allowed between any two tokens except
where NO_SPACE forbids it, which is why $ args and a broken-up identifier
are invalid.
JSONSelection ::= LitExpr | NamedSelectionList
NamedSelectionList ::= NamedSelection ("," NamedSelection)* ","? | NamedSelection*
SubSelection ::= "{" NamedSelectionList "}"
NamedSelection ::= "..." LitExpr
| Alias LitExpr
| PathSelection
Alias ::= Key ":"
PathSelection ::= (VarPath | KeyPath | AtPath | ExprPath) SubSelection?
VarPath ::= "$" (NO_SPACE Identifier)? PathTail
KeyPath ::= Key PathTail
AtPath ::= "@" PathTail
ExprPath ::= "$(" LitExpr ")" PathTail
PathTail ::= "?"? (PathStep "?"?)*
NonEmptyPathTail ::= "?"? (PathStep "?"?)+ | "?"
PathStep ::= "." Key | "->" Identifier MethodArgs?
Key ::= Identifier | LitString
Identifier ::= [a-zA-Z_] NO_SPACE [0-9a-zA-Z_]*
MethodArgs ::= "(" (LitExpr ("," LitExpr)* ","?)? ")"
LitExpr ::= LitOpChain | LitPath | LitPrimitive | LitObject | LitArray | PathSelection
LitOpChain ::= LitExpr (LitOp LitExpr)+
LitOp ::= "??" | "?!"
LitPath ::= (LitPrimitive | LitObject | LitArray) NonEmptyPathTail
LitPrimitive ::= LitString | LitNumber | "true" | "false" | "null"
LitString ::= "'" ("\\'" | [^'])* "'" | '"' ('\\"' | [^"])* '"'
LitNumber ::= "-"? ([0-9]+ ("." [0-9]*)? | "." [0-9]+)
LitObject ::= SubSelection
LitArray ::= "[" (LitExpr ("," LitExpr)* ","?)? "]"
NO_SPACE ::= !SpacesOrComments
SpacesOrComments ::= (Spaces | Comment)+
Spaces ::= (" " | "\t" | "\r" | "\n")+
Comment ::= "#" [^\n]*What the prose above depends on:
NamedSelectionList is either comma-separated or whitespace-separated,
never mixed. That is Separators.Alias LitExpr is why a literal can follow an alias
(Literals).PathSelection contributes its key only when the path is one key long.
That is the single-key rule.The v0.3 grammar is this one with four differences:
JSONSelection ::= NamedSelection*
SubSelection ::= "{" NamedSelection* "}"
NamedSelection ::= (Alias | "...")? PathSelection | Alias SubSelection
LitObject ::= "{" (LitProperty ("," LitProperty)* ","?)? "}"
LitProperty ::= Key ":" LitExprNo commas in a selection list; an alias takes only a path or a sub-selection,
never a literal (which is why a: "Book" reads a property there); ... is
rejected outright despite the grammar’s "..."; and a literal object is its
own rule, reachable only inside $( ).
Everything version-dependent here is a measurement. Two instruments did the measuring:
rover supergraph compose at the pin) decides what parses
and what builds: the pin and link tables, the grammar, and the build error in
Troubleshooting.application/json, and an echo server for request bodies.To re-measure after changing federation_version, connect_spec or the router
version: write a schema that uses each form once, compose it, check that the
compose succeeded, and query it through the router, or run the workspace’s e2e
layer, which does the same with WireMock.
Composition was measured with rover 0.41.0 at federation 2.12.0, 2.13.0,
2.14.0, 2.14.3, 2.15.0 and 2.15.2, at both connect spec versions, and at
2.15.1 for the grammar note under the pin table. Runtime results come from
Router 2.17.0 on schemas composed at 2.15.2 (connect/v0.4) and 2.12.0
(connect/v0.3). Abstract types was measured the same way:
composition at 2.15.2 and at 2.12.0, and runtime on Router 2.17.0.
Every selection and every result in this file was produced that way. If you change one, re-run it.