Skip to content

Schema Coordinates #794

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

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
38616cc
Add Schema Coordinates RFC
magicmark Sep 17, 2020
45fb46c
Update schema coordinates spec edit
magicmark Nov 18, 2020
eabdaff
implement PR suggestions
magicmark Nov 21, 2020
f994f99
Tweak example table wording
magicmark Nov 22, 2020
17002d4
Apply suggestions from code review
magicmark Nov 23, 2020
6456c5f
enumName -> enumValueName
magicmark Nov 28, 2020
4905977
- Add PR feedback
magicmark Jan 4, 2021
8be29ca
Update Section 3 -- Type System.md
magicmark Jan 7, 2021
568d26f
Editorial on grammar and semantics
leebyron Apr 13, 2021
b8f3f47
Move section
leebyron Apr 13, 2021
8580162
Simplify examples
leebyron Apr 14, 2021
87a38e2
standalone
leebyron Apr 15, 2021
75c48ba
update numbers
leebyron Apr 16, 2021
148d073
clarify element
leebyron Apr 16, 2021
c43f4aa
Update Punctuator grammar
leebyron Apr 16, 2021
bb896cf
specify schema element
leebyron Apr 16, 2021
e5a2092
fix example
leebyron Apr 16, 2021
b65eb31
clarify metafields
leebyron Apr 16, 2021
c16a28b
Better Punctator
leebyron Apr 16, 2021
93bd2c6
Minor algo variable name refinements
leebyron Apr 22, 2021
b2e42e8
whitespace fix
magicmark Dec 10, 2024
4ce2d48
add note about union members
magicmark Dec 10, 2024
2d60b33
prettier
magicmark Dec 10, 2024
d61cdc3
formatting
magicmark Dec 10, 2024
caed065
Update spec/Section 3 -- Type System.md
magicmark Jan 2, 2025
07b3bbd
Update spec/Section 3 -- Type System.md
magicmark Jan 2, 2025
a3383ee
Merge branch 'main' into schema_coordinates_spec_edit
benjie May 15, 2025
258d841
Run prettier
benjie May 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion spec/Appendix B -- Grammar Summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ Token ::
- FloatValue
- StringValue

Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | }
Punctuator ::

- DotPunctuator
- OtherPunctuator

DotPunctuator :: `.` [lookahead != {`.`, Digit}]

OtherPunctuator :: one of ! $ & ( ) ... : = @ [ ] { | }

Name ::

Expand Down Expand Up @@ -412,3 +419,11 @@ TypeSystemDirectiveLocation : one of
- `ENUM_VALUE`
- `INPUT_OBJECT`
- `INPUT_FIELD_DEFINITION`

SchemaCoordinate :

- Name
- Name . Name
- Name . Name ( Name : )
- @ Name
- @ Name ( Name : )
15 changes: 14 additions & 1 deletion spec/Section 2 -- Language.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,25 @@ and is {Ignored}.

### Punctuators

Punctuator :: one of ! $ & ( ) ... : = @ [ ] { | }
Punctuator ::

- DotPunctuator
- OtherPunctuator

DotPunctuator :: `.` [lookahead != {`.`, Digit}]

OtherPunctuator :: one of ! $ & ( ) ... : = @ [ ] { | }

GraphQL documents include punctuation in order to describe structure. GraphQL is
a data description language and not a programming language, therefore GraphQL
lacks the punctuation often used to describe mathematical expressions.

The {`.`} punctuator must not be followed by a {`.`} or {Digit}. This ensures
that the source {"..."} can only be interpreted as a single {`...`} and not
three {`.`}. It also avoids any potential ambiguity with {FloatValue}. As an
example the source {".123"} has no valid lexical representation (without this
restriction it would have been interpreted as {`.`} followed by {IntValue}).

### Names

Name ::
Expand Down
125 changes: 125 additions & 0 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -2168,3 +2168,128 @@ to the relevant IETF specification.
```graphql example
scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")
```

## Schema Coordinates

SchemaCoordinate :

- Name
- Name . Name
- Name . Name ( Name : )
- @ Name
- @ Name ( Name : )

:: A _schema coordinate_ is a human readable string that uniquely identifies a
_schema element_ within a GraphQL Schema.

:: A _schema element_ can be a named type, a field, an input field, an enum
value, a field argument, a directive, or a directive argument.

A _schema coordinate_ is always unique. Each _schema element_ may be referenced
by exactly one possible schema coordinate.

A _schema coordinate_ may refer to either a defined or built-in _schema
element_. For example, `String` and `@deprecated(reason:)` are both valid schema
coordinates which refer to built-in schema elements. However it must not refer
to a meta-field. For example, `Business.__typename` is _not_ a valid schema
coordinate.
Comment on lines +2193 to +2195
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a specific reason to forbid meta fields? I can definitely see myself using it. For an example to log introspection usages of my server.

Copy link
Contributor Author

@magicmark magicmark Jan 2, 2025

Choose a reason for hiding this comment

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

Good question! I don't actually remember if there was a technical reason, or if was around limiting the spec to the intended application use cases 🤔

This blames to @leebyron - Lee, any more context?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@leebyron any thoughts?


Note: A union member is not a valid _schema coordinate_ as they reference
existing types in the schema. This preserves the uniqueness property of a
_schema coordinate_ as stated above.

Note: A {SchemaCoordinate} is not a definition within a GraphQL {Document}, but
a separate standalone grammar, intended to be used by tools to reference types,
fields, and other *schema element*s. Examples include: as references within
documentation to refer to types and fields in a schema, a lookup key that can be
used in logging tools to track how often particular fields are queried in
production.

**Semantics**

To refer to a _schema element_, a _schema coordinate_ must be interpreted in the
context of a GraphQL {schema}.

SchemaCoordinate : Name

1. Let {typeName} be the value of the first {Name}.
2. Return the type in the {schema} named {typeName}.

Comment on lines +2215 to +2217
Copy link
Contributor

@martinbonnin martinbonnin Jan 2, 2025

Choose a reason for hiding this comment

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

Nit: should we assert that the type exists?

Suggested change
1. Let {typeName} be the value of the first {Name}.
2. Return the type in the {schema} named {typeName}.
1. Let {typeName} be the value of the first {Name}.
2. Let {type} be the type in the {schema} named {typeName}.
3. Assert {type} exists.
4. Return {type}.

This is mainly for symmetry with the "argument coordinate" case below which asserts field existence and is therefore allowed to "fail". Asserting that the type exists makes it explicit that a coordinate may "fail".

Copy link
Member

Choose a reason for hiding this comment

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

I don't think we should do this. For example, if you wanted to ask the question "Does Foo.bar(baz:) exist in the schema, you might first check the schema coord Foo and make sure it's an object or interface, and then check Foo.bar exists, and finally access Foo.bar(baz:). It should not be required that checking for Foo causes it to throw IMO - returning null seems not unreasonable. Throwing if you try to access Foo.bar(baz:) when Foo.bar doesn't exist does however seem reasonable.

Copy link
Contributor

Choose a reason for hiding this comment

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

Does it even make sense to have "throw" or return "null"? I mean this is all declarative, it should be possible to represent an invalid coordinate if people want (for counter examples or whatever...).

If we were to actually implement a function I would probably use a verb:

GetAstNode(SchemaCoordinate):
...

Do we want graphql-js to have such functions?

Copy link
Member

Choose a reason for hiding this comment

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

That's effectively what we're doing

To refer to a schema element, a schema coordinate must be interpreted in the context of a GraphQL {schema}.

I.e. a schema coordinate can exist, but if you want to refer to a schema element then you follow the algorithm.

Copy link
Contributor

@martinbonnin martinbonnin May 12, 2025

Choose a reason for hiding this comment

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

Alright so what we're saying is:

throws if any component leading to the last component doesn't exist or is not of the expected type.
returns null if the last component of the coordinate is not found.
else return the element.

If we're going that road, I would say we should probably need to be explicit about it:

1. Let {typeName} be the value of the first {Name}.
2. If there is not type in the schema named {typeName} return null.
2. Else let {type} be the type in the {schema} named {typeName}.
4. return {type}.

Copy link
Contributor Author

@magicmark magicmark May 12, 2025

Choose a reason for hiding this comment

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

it should be possible to represent an invalid coordinate if people want (for counter examples or whatever...)

At this risk of going down a rabbit hole of over-explicitness -- the spec as written states a schema coordinate must be interpreted in the context of a GraphQL schema.

However, we could think of parsing and validating the schema coordinate as two seperate steps (similar to parsing/validating documents):

  1. The grammar definition is implicitly the definition of parse()
  2. And the written instuctions are the definition of validate()

Which could be how graphql-js exposes it?

Copy link
Contributor

Choose a reason for hiding this comment

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

@magicmark I like this 👍 . It's also more consistent with the rest of the spec.

Copy link
Contributor

@martinbonnin martinbonnin May 25, 2025

Choose a reason for hiding this comment

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

After reading the graphql-js PR, I think it'd make sense to have resolveTypeCoordinate(TypeCoordinate), resolveMemberCoordinate(MemberCoordinate), etc... And use algorithms for all of them.

I can look into this for the next working group if there is interest, just let me know.

SchemaCoordinate : Name . Name

1. Let {typeName} be the value of the first {Name}.
2. Let {type} be the type in the {schema} named {typeName}.
3. If {type} is an Enum type:
4. Let {enumValueName} be the value of the second {Name}.
5. Return the enum value of {type} named {enumValueName}.
6. Otherwise if {type} is an Input Object type:
7. Let {inputFieldName} be the value of the second {Name}.
8. Return the input field of {type} named {inputFieldName}.
9. Otherwise:
10. Assert {type} must be an Object or Interface type.
11. Let {fieldName} be the value of the second {Name}.
12. Return the field of {type} named {fieldName}.

SchemaCoordinate : Name . Name ( Name : )

1. Let {typeName} be the value of the first {Name}.
2. Let {type} be the type in the {schema} named {typeName}.
3. Assert {type} must be an Object or Interface type.
4. Let {fieldName} be the value of the second {Name}.
5. Let {field} be the field of {type} named {fieldName}.
6. Assert {field} must exist.
7. Let {fieldArgumentName} be the value of the third {Name}.
8. Return the argument of {field} named {fieldArgumentName}.

SchemaCoordinate : @ Name

1. Let {directiveName} be the value of the first {Name}.
2. Return the directive in the {schema} named {directiveName}.

SchemaCoordinate : @ Name ( Name : )

1. Let {directiveName} be the value of the first {Name}.
2. Let {directive} be the directive in the {schema} named {directiveName}.
3. Assert {directive} must exist.
4. Let {directiveArgumentName} be the value of the second {Name}.
5. Return the argument of {directive} named {directiveArgumentName}.

**Examples**

| Element Kind | _Schema Coordinate_ | _Schema Element_ |
| ------------------ | --------------------------------- | --------------------------------------------------------------------- |
| Named Type | `Business` | `Business` type |
| Field | `Business.name` | `name` field on the `Business` type |
| Input Field | `SearchCriteria.filter` | `filter` input field on the `SearchCriteria` input object type |
| Enum Value | `SearchFilter.OPEN_NOW` | `OPEN_NOW` value of the `SearchFilter` enum |
| Field Argument | `Query.searchBusiness(criteria:)` | `criteria` argument on the `searchBusiness` field on the `Query` type |
| Directive | `@private` | `@private` directive |
| Directive Argument | `@private(scope:)` | `scope` argument on the `@private` directive |

The table above shows an example of a _schema coordinate_ for every kind of
_schema element_ based on the schema below.

```graphql
type Query {
searchBusiness(criteria: SearchCriteria!): [Business]
}

input SearchCriteria {
name: String
filter: SearchFilter
}

enum SearchFilter {
OPEN_NOW
DELIVERS_TAKEOUT
VEGETARIAN_MENU
}

type Business {
id: ID
name: String
email: String @private(scope: "loggedIn")
}

directive @private(scope: String!) on FIELD_DEFINITION
```