diff --git a/.gitignore b/.gitignore index e85ede9..3c3629e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ node_modules -core -join \ No newline at end of file diff --git a/build b/build index e57e384..e7be76f 100755 --- a/build +++ b/build @@ -8,3 +8,4 @@ spec-md inaccessible/v0.1/inaccessible-v0.1.md > inaccessible/v0.1/index.html spec-md link/v1.0/link-v1.0.md > link/v1.0/index.html spec-md join/v0.1/join-v0.1.md > join/v0.1/index.html spec-md federation/v2.0/federation-v2.0.md > federation/v2.0/index.html +spec-md index.md > index.html \ No newline at end of file diff --git a/core/v0.1/.gitignore b/core/v0.1/.gitignore new file mode 100644 index 0000000..b768a31 --- /dev/null +++ b/core/v0.1/.gitignore @@ -0,0 +1,3 @@ +node_modules +.dist +package-lock.json \ No newline at end of file diff --git a/core/v0.1/bad-non-unique-prefix-multi.graphql b/core/v0.1/bad-non-unique-prefix-multi.graphql new file mode 100644 index 0000000..221f3e4 --- /dev/null +++ b/core/v0.1/bad-non-unique-prefix-multi.graphql @@ -0,0 +1,9 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://specs.example.com/A/2.0") # name is A +{ + query: Query +} + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.1/bad-non-unique-prefix.graphql b/core/v0.1/bad-non-unique-prefix.graphql new file mode 100644 index 0000000..749db43 --- /dev/null +++ b/core/v0.1/bad-non-unique-prefix.graphql @@ -0,0 +1,9 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://www.specs.com/specA/1.1", as: "A") # name is A +{ + query: Query +} + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.1/basic.graphql b/core/v0.1/basic.graphql new file mode 100644 index 0000000..454191b --- /dev/null +++ b/core/v0.1/basic.graphql @@ -0,0 +1,14 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/example/v1.0") +{ + query: Query +} + +type Query { + field: Int @example +} + +directive @example on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.1/core-v0.1.md b/core/v0.1/core-v0.1.md new file mode 100644 index 0000000..e67f292 --- /dev/null +++ b/core/v0.1/core-v0.1.md @@ -0,0 +1,485 @@ +# Core Schemas + +

flexible metadata for GraphQL schemas

+ +```raw html + + + +
StatusRelease
Version0.1
+ + +``` + +[GraphQL](https://spec.graphql.org/) provides directives as a means of attaching user-defined metadata to a GraphQL document. Directives are highly flexible, and can be used to suggest behavior and define features of a graph which are not otherwise evident in the schema. + +Alas, *GraphQL does not provide a mechanism to globally identify or version directives*. Given a particular directive—e.g. `@join`—processors are expected to know how to interpret the directive based only on its name, definition within the document, and additional configuration from outside the document. This means that programs interpreting these directives have two options: + + 1. rely on a hardcoded interpretation for directives with certain signatures, or + 2. accept additional configuration about how to interpret directives in the schema. + +The first solution is fragile, particularly as GraphQL has no built-in namespacing mechanisms, so the possibility of name collisions always looms. + +The second is unfortunate: GraphQL schemas are generally intended to be self-describing, and requiring additional configuration subtly undermines this guarantee: given just a schema, programs do not necessarily know how to interpret it, and certainly not how to serve it. It also creates the possibility for the schema and configuration to fall out of sync, leading to issues which can manifest late in a deployment pipeline. + +Introducing **core schemas**. + +
+ +
+
core schema
+
+
+ +A basic core schema: + +:::[example](basic.graphql) -- A basic core schema + +**Core schemas** provide a concise mechanism for schema documents to specify the metadata they provide. Metadata is grouped into **features**, which typically define directives and associated types (e.g. scalars and inputs which serve as directive inputs). Additionally, core schemas provide: + - [**Flexible namespacing rules.**](#sec-Prefixing) It is always possible to represent any GraphQL schema within a core schema document. Additionally, documents can [choose the names](#@core/as) they use for the features they reference, guaranteeing that namespace collisions can always be resolved. + - [**Versioning.**](#sec-Versioning) Feature specifications follow [semver-like semantic versioning principles](#sec-Versioning), which helps schema processors determine if they are able to correctly interpret a document's metadata. + +**Core schemas are not a new language.** All core schema documents are valid GraphQL schema documents. However, this specification introduces new requirements, so not all valid GraphQL schemas are valid core schemas. + +The broad intention behind core schemas is to provide a *single document* which provides all the necessary configuration for programs that process and serve the schema to GraphQL clients, primarily by following directives in order to determine how to resolve queries made against that schema. + +# Parts of a Core Schema + +When talking about a core schema, we can broadly break it into two pieces: +- an **API** consisting of schema elements (objects, interfaces, enums, directives, etc.) which SHOULD be served to clients, and +- **machinery** containing document metadata. This typically consists of directives and associated input types (such as enums and input objects), but may include any schema element. Machinery MUST NOT be served to clients. Specifically, machinery MUST NOT be included in introspection responses or used to validate or execute queries. + +This reflects how core schemas are used: a core schema contains a GraphQL interface (the *API*) along with metadata about how to implement that interface (the *machinery*). Exposing the machinery to clients is unnecessary, and may in some cases constitute a security issue (for example, the machinery for a public-facing graph router will likely reference internal services, possibly exposing network internals which should not be visible to the general public). + +A key feature of core schemas is that it is always possible to derive a core schema's API without any knowledge of the features used by the document (with the exception of the `core` feature itself). Specifically, named elements are not included in the API schema if they are named `something__likeThis` or are a directive named `@something`, and `something` is the prefix of a feature declared with {@core}. + +A formal description is provided by the [IsInAPI](#sec-Is-In-API-) algorithm. + +# Actors + +```mermaid diagram -- Actors who may be interested in the core schemas +graph TB + classDef bg fill:none,color:#22262E; + author("👩🏽‍💻 🤖  Author"):::bg-->schema(["☉ Core Schema"]):::bg + schema-->proc1("🤖  Processor"):::bg + proc1-->output1(["☉ Core Schema[0]"]):::bg + output1-->proc2("🤖  Processor"):::bg + proc2-->output2(["☉ Core Schema[1]"]):::bg + output2-->etc("..."):::bg + etc-->final(["☉ Core Schema [final]"]):::bg + final-->core("🤖 Data Core"):::bg + schema-->reader("👩🏽‍💻 Reader"):::bg + output1-->reader + output2-->reader + final-->reader +``` + +- **Authors (either human or machine)** write an initial core schema as specified in this document, including versioned {@core} requests for all directives they use +- **Machine processors** can process core schemas and output new core schemas. The versioning of directives and associated schema elements provided by the {@core} allows processors to operate on directives they understand and pass through directives they do not. +- **Human readers** can examine the core schema at various stages of processing. At any stage, they can examine the {@core} directives and follow URLs to the specification, receiving an explanation of the requirements of the specification and what new directives, types, and other schema objects are available within the document. +- **Data cores** can then pick up the processed core schema and provide some data-layer service with it. Typically this means serving the schema's API as a GraphQL endpoint, using metadata defined by machinery to inform how it processes operations it receives. However, data cores may perform other tasks described in the core schema, such as routing to backend services, caching commonly-accessed fields and queries, and so on. The term "data core" is intended to capture this multiplicity of possible activities. + +# Basic Requirements + +Core schemas: + 1. MUST be valid GraphQL schema documents, + 2. MUST contain exactly one `SchemaDefinition`, and + 3. MUST use the {@core} directive on their schema definition to declare any features they reference by using {@core} to reference a [well-formed feature URL](#@core/feature). + +The first {@core} directive on the schema MUST reference the core spec itself, i.e. this document. + +:::[example](basic.graphql) -- Basic core schema using {@core} and `@example` + +## Unspecified directives are passed through by default + +Existing schemas likely contain definitions for directives which are not versioned, have no specification document, and are intended mainly to be passed through. This is the default behavior for core schema processors: + +```graphql example -- Unspecified directives are passed through +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") +{ + query: Query +} + +type SomeType { + field: Int @another +} + +# `@another` is unspecified. Core processors will not extract metadata from +# it, but its definition and all usages within the schema will be exposed +# in the API. +directive @another on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA +``` + +## Renaming core itself + +It is possible to rename the core feature itself with the same [`as:`](#@core/as) mechanism used for all features: + +```graphql example -- Renaming {@core} to {@coreSchema} +schema + @coreSchema(feature: "https://specs.apollo.dev/core/v0.1", as: "coreSchema") + @coreSchema(feature: "https://example.com/example/v1.0") +{ + query: Query +} + +type SomeType { + field: Int @example +} + +directive @coreSchema(feature: String!, as: String) + repeatable on SCHEMA +directive @example on FIELD_DEFINITION +``` + +# Directive Definitions + +All core schemas use the [{@core}](#@core) directive to declare their use of the `core` feature itself as well as any other core features they use. + +In order to use these directives in your schema, GraphQL requires you to include their definitions in your schema. + +Processors MUST validate that you have defined the directives with the same arguments, locations, and `repeatable` flag as given below. Specifically, the [bootstrapping](#sec-Bootstrapping) algorithm validates that the `@core` directive has a definition matching the definition given below. (The bootstrapping algorithm does not require processors to validate other aspects of the directive declaration such as description strings or argument ordering. The main purpose of this validation is to ensure that directive arguments have the type and default values expected by the specification.) + +The following declares the directive defined by this specification. You SHOULD define the directives in your core schema by including the following text in your schema document. + +```graphql +directive @core(feature: String!, as: String) repeatable on SCHEMA +``` + +When writing a specification for your own core feature, you SHOULD include a section like this one with sample definitions to copy into schemas, and you SHOULD require processors to validate that directive definitions in documents match your sample definitions. + +# Directives + +##! @core + +Declare a core feature present in this schema. + +```graphql definition +directive @core( + feature: String!, + as: String) + repeatable on SCHEMA +``` + +Documents MUST include a definition for the {@core} directive which includes all of the arguments defined above with the same types and default values. + +###! feature: String! + +A feature URL specifying the directive and associated schema elements. When viewed, the URL SHOULD provide the content of the appropriate version of the specification in some human-readable form. In short, a human reader should be able to click the link and go to the docs for the version in use. There are specific requirements on the format of the URL, but it is not required that the *content* be machine-readable in any particular way. + +Feature URLs contain information about the spec's [prefix](#sec-Prefixing) and [version](#sec-Versioning). + +Feature URLs serve two main purposes: + - Directing human readers to documentation about the feature + - Providing tools with information about the specs in use, along with enough information to select and invoke an implementation + +Feature URLs SHOULD be [RFC 3986 URLs](https://tools.ietf.org/html/rfc3986). When viewed, the URL SHOULD provide the specification of the selected version of the feature in some human-readable form; a human reader should be able to click the link and go to the correct version of the docs. + +Although they are not prohibited from doing so, it's assumed that processors will not load the content of feature URLs. Published specifications are not required to be machine-readable, and [this spec](.) places no requirements on the structure or syntax of the content to be found there. + +There are, however, requirements on the structure of the URL itself: + +```html diagram -- Basic anatomy of a feature URL + + https://spec.example.com/a/b/c/exampleFeature/v1.0 + +``` + +The final two segments of the URL's [path](https://tools.ietf.org/html/rfc3986#section-3.3) MUST contain the feature's name and a [version tag](#sec-Versioning). The content of the URL up to and including the name—but excluding the `/` after the name and the version tag—is the feature's *identity*. Trailing slashes at the end of the URL (ie, after the version tag) should be ignored. For the above example, +
+
`identity: "https://spec.example.com/a/b/c/exampleFeature"`
+
A global identifier for the feature. Processors can treat this as an opaque string identifying the feature (but not the version of the feature) for purposes of selecting an appropriate implementation. The identity never has a trailing `/`.
+
`name: "exampleFeature"`
+
The feature's name, for purposes of [prefixing](#sec-Prefixing) schema elements it defines.
+
`version: "v1.0"`
+
The tag for the [version](#sec-Versioning) of the feature used to author the document. Processors MUST select an implementation of the feature which can [satisfy](#sec-Satisfaction) the specified version.
+
+ +The version tag MUST be a valid {VersionTag}. The name MUST be a valid [GraphQL name](https://spec.graphql.org/draft/#Name) which does not include the namespace separator ({"__"}). + +#### Ignore meaningless URL components + +When extracting the URL's `name` and `version`, processors MUST ignore any url components which are not assigned a meaning. This spec assigns meaning to the final two segments of the [path](https://tools.ietf.org/html/rfc3986#section-3.3). Other URL components—particularly query strings and fragments, if present—MUST be ignored for the purposes of extracting the `name` and `version`. + +```html diagram -- Ignoring meaningless parts of a URL + + https://example.com/exampleSpec/v1.0/?key=val&k2=v2#frag + +``` + +#### Why is versioning in the URL, not a directive argument? + +The version is in the URL because when a human reader visits the URL, we would like them to be taken to the documentation for the *version of the feature used by this document*. Many text editors will turn URLs into hyperlinks, and it's highly desirable that clicking the link takes the user to the correct version of the docs. Putting the version information in a separate argument to the {@core} directive would prevent this. + +###! as: String + +Change the [names](#sec-Prefixing) of directives and schema elements from this specification. The specified string MUST be a valid [GraphQL name](https://spec.graphql.org/draft/#Name) and MUST NOT contain the namespace separator (two underscores, {"__"}) or end with an underscore. + +When [`as:`](#@core/as) is provided, processors looking for [prefixed schema elements](#sec-Elements-which-must-be-prefixed) MUST look for elements whose names are the specified name with the prefix replaced with the name provided to the `as:` argument. + +```graphql example -- Using {@core}(feature:, as:) to use a feature with a custom name +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://spec.example.com/example/v1.0", as: "eg") +{ + query: Query +} + +type User { + # Specifying `as: "eg"` transforms @example into @eg + name: String @eg(data: ITEM) +} + +# Additional specified schema elements must have their prefixes set +# to the new name. +# +# The spec at https://spec.example.com/example/v1.0 calls this enum +# `example__Data`, but because of the `as:` argument above, processors +# will use this `eg__Data` enum instead. +enum eg__Data { + ITEM +} + +# Name transformation must also be applied to definitions pulled in from +# specifications. +directive @eg(data: eg__Data) on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA +``` + +# Prefixing + +With the exception of a single root directive, core feature specifications MUST prefix all schema elements they introduce. The prefix: + 1. MUST match the name of the feature as derived from the feature's specification URL, + 2. MUST be a valid [GraphQL name](https://spec.graphql.org/draft/#Name), and + 3. MUST NOT contain the core namespace separator, which is two underscores ({"__"}), and + 4. MUST NOT end with an underscore (which would create ambiguity between whether {"x___y"} is prefix `x_` for element `y` or prefix `x` for element `_y`). + +Prefixed names consist of the name of the feature, followed by two underscores, followed by the name of the element, which can be any valid [GraphQL name](https://spec.graphql.org/draft/#Name). For instance, the `core` specification (which you are currently reading) introduces an element named [{@core}](#@core), and the `join` specification introduces an element named {@join__field} (among others). + +Note that both parts must be valid GraphQL names, and GraphQL names cannot start with digits, so core feature specifications cannot introduce names like `@feature__24hours`. + +A feature's *root directive* is an exception to the prefixing requirements. Feature specifications MAY introduce a single directive which carries only the name of the feature, with no prefix required. For example, the `core` specification introduces a {@core} directive. This directive has the same name as the feature ("`core`"), and so requires no prefix. + +```graphql example -- Using the @core directive without changing the prefix +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://spec.example.com/example/v1.0") { + query: Query +} + +type User { + name: String @example(data: ITEM) +} + +# An enum used to provide structured data to the example spec. +# It is prefixed with the name of the spec. +enum example__Data { + ITEM +} + +directive @example(data: example__Data) on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA +``` + +The prefix MUST NOT be elided within documentation; definitions of schema elements provided within the spec MUST include the feature's name as a prefix. + +## Elements which must be prefixed + +Feature specs MUST prefix the following schema elements: + - the names of any object types, interfaces, unions, enums, or input object types defined by the feature + - the names of any directives introduced in the spec, with the exception of the *root directive*, which must have the same name as the feature + +:::[example](prefixing.graphql) -- Prefixing + +# Versioning + +VersionTag : "v" Version + +Version : Major "." Minor + +Major : NumericIdentifier + +Minor : NumericIdentifier + +NumericIdentifier : "0" + | PositiveDigit Digit* + +Digit : "0" | PositiveDigit + +PositiveDigit : "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + +Specs are versioned with a **subset** of a [Semantic Version Number](https://semver.org/spec/v2.0.0.html) containing only the major and minor parts. Thus, specifications SHOULD provide a version of the form `v`{Major}`.`{Minor}, where both integers >= 0. + +```text example -- Valid version tags +v2.2 +v1.0 +v1.1 +v0.1 +``` + +As specified by semver, spec authors SHOULD increment the: + +{++ + +- MAJOR version when you make incompatible API changes, +- MINOR version when you add functionality in a backwards compatible manner + +++} + +Patch and pre-release qualifiers are judged to be not particularly meaningful in the context of core features, which are (by definition) interfaces rather than implementations. The patch component of a semver denotes a bug fix which is backwards compatible—that is, a change to the implementation which does not affect the interface. Patch-level changes in the version of a spec denote wording clarifications which do not require implementation changes. As such, it is not important to track them for the purposes of version resolution. + +As with [semver](https://semver.org/spec/v2.0.0.html), the `0.x` version series is special: there is no expectation of compatibility between versions `0.x` and `0.y`. For example, a processor must not activate implementation `0.4` to satisfy a requested version of `0.2`. + +## Satisfaction + +Given a version {requested} by a document and an {available} version of an implementation, the following algorithm will determine if the {available} version can satisfy the {requested} version: + +Satisfies(requested, available) : + 1. If {requested}.{Major} ≠ {available}.{Major}, return {false} + 2. If {requested}.{Major} = 0, return {requested}.{Minor} = {available}.{Minor} + 3. Return {requested}.{Minor} <= {available}.{Minor} + +## Referencing versions and activating implementations + +Schema documents MUST reference a feature version which supports all the schema elements and behaviors required by the document. As a practical matter, authors will generally prefer to reference a version they have reason to believe is supported by the most processors; depending on context, this might be an old stable version with a low major version, or a new less-deprecated version with a large major version. + +If a processor chooses to activate support for a feature, the processor MUST activate an implementation which can [satisfy](#sec-Satisfaction) the version required by the document. + + +# Processing Schemas + +```mermaid diagram +graph LR + schema(["📄 Input Schema"]):::file-->proc("🤖  Processor") + proc-->output(["📄 Output Schema"]):::file + classDef file fill:none,color:#22262E; + style proc fill:none,stroke:fuchsia,color:fuchsia; +``` + +A common use case is that of a processor which consumes a valid input schema and generates an output schema. + +The general guidance for processor behavior is: don't react to what you don't understand. + +Specifically, processors: + - SHOULD pass through {@core} directives which reference unknown feature URLs + - SHOULD pass through prefixed directives, types, and other schema elements + - SHOULD pass through directives which are not [associated with](#AssignFeatures) a {@core} feature + +Processors MAY accept configuration which overrides these default behaviors. + +Additionally, processors which prepare the schema for final public consumption MAY choose to eliminate all unknown directives and prefixed types in order to hide schema implementation details within the published schema. This will impair the operation of tooling which relies on these directives—such tools will not be able to run on the output schema, so the benefits and costs of this kind of information hiding should be weighed carefully on a case-by-case basis. + +# Validations & Algorithms + +This section lays out algorithms for processing core schemas. + +Algorithms described in this section may produce *validation failures* if a document does not conform to the requirements core schema document. Validation failures SHOULD halt processing. Some consumers, such as authoring tools, MAY attempt to continue processing in the presence of validation failures, but their behavior in such cases is unspecified. + +## Bootstrapping + +Determine the name of the core specification within the document. + +It is possible to [rename the core feature](#sec-Renaming-core-itself) within a document. This process determines the actual name for the core feature if one is present. + +- **Fails** the *Has Schema* validation if there are no SchemaDefinitions in the document +- **Fails** the *Has Core Feature* validation if the `core` feature itself is not referenced with a {@core} directive within the document +- **Fails** the *Bootstrap Core Feature Listed First* validation if the reference to the `core` feature is not the first {@core} directive on the document's SchemaDefinition +- **Fails** the *Core Directive Incorrect Definition* validation if the {@core} directive definition does not *match* the directive as defined by this specification. + +For the purposes of this algorithm, a directive's definition in a schema *matches* a definition provided in this specification if: +- Its arguments have the specified names, types, and default values (or lack thereof) +- It is defined as `repeatable` if and only if the specification's definition defines it as `repeatable` +- The set of locations it belongs to is the same set of locations in the specification's definition. + +The following aspects may differ between the definition in the schema and the definition in the specification without preventing the definitions from *matching*: +- The name of the directive (due to [prefixing](#sec-Prefixing)) +- The order of arguments +- The order of locations +- The directive's description string +- Argument description strings +- Directives applied to argument definitions + +Bootstrap(document) : +1. Let {schema} be the only SchemaDefinition in {document}. (Note that legal GraphQL documents [must include at most one SchemaDefinition](http://spec.graphql.org/draft/#sec-Root-Operation-Types).) + 1. ...if no SchemaDefinitions are present in {document}, the ***Has Schema* validation fails**. +1. For each directive {d} on {schema}, + 1. If {d} has a [`feature:`](#@core/feature) argument which [parses as a feature URL](#@core/feature), *and* whose identity is {"https://specs.apollo.dev/core/"} *and* whose version is {"v0.1"}, *and either* {d} has an [`as:`](#@core/as) argument whose value is equal to {d}'s name *or* {d} does not have an [`as:`](#@core/as) argument and {d}'s name is `core`: + - If any directive on {schema} listed before {d} has the same name as {d}, the ***Bootstrap Core Feature Listed First* validation fails**. + - If the definition of the directive {d} does not *match* the [definition of {@core} in this specification](#@core), the ***Core Directive Incorrect Definition* validation fails**. + - Otherwise, **Return** {d}'s name. +- If no matching directive was found, the ***Has Core Feature* validation fails**. + +## Feature Collection + +Collect a map of ({featureName}: `String`) -> `Directive`, where `Directive` is a {@core} Directive which introduces the feature named {featureName} into the document. + +- **Fails** the *Name Uniqueness* validation if feature names are not unique within the document. +- **Fails** *Invalid Feature URL* validation for any invalid feature URLs. + +CollectFeatures(document) : + - Let {coreName} be the name of the core feature found via {Bootstrap(document)} + - Let {features} be a map of {featureName}: `String` -> `Directive`, initially empty. + - For each directive {d} named {coreName} on the SchemaDefinition within {document}, + - Let {specifiedFeatureName} and {version} be the result of parsing {d}'s `feature:` argument according to the [specified rules for feature URLs](#@core/feature) + - If the `feature:` is not present or fails to parse: + - The ***Invalid Feature URL* validation fails** for {d}, + - Let {featureName} be the {d}'s [`as:`](#@core/as) argument or, if the argument is not present, {specifiedFeatureName} + - If {featureName} exists within {features}, the ***Name Uniqueness* validation fails**. + - Insert {featureName} => {d} into {features} + - **Return** {features} + + +Prefixes, whether implicit or explicit, must be unique within a document. Valid: + +:::[example](prefixing.graphql#schema[0]) -- Unique prefixes + +It is also valid to reference multiple versions of the same spec under different prefixes: + +:::[example](prefix-uniqueness.graphql#schema[0]) -- Explicit prefixes allow multiple versions of the same spec to coexist within a Document + +Without the explicit [`as:`](#@core/as), the above would be invalid: + +:::[counter-example](prefix-uniqueness.graphql#schema[1]) -- Non-unique prefixes with multiple versions of the same spec + +Different specs with the same prefix are also invalid: + +:::[counter-example](prefix-uniqueness.graphql#schema[2]) -- Different specs with non-unique prefixes + +## Assign Features + +Create a map of {element}: *Any Named Element* -> {feature}: `Directive` | {null}, associating every named schema element within the document with a feature directive, or {null} if it is not associated with a feature. + +AssignFeatures(document) : + - Let {features} be the result of collecting features via {CollectFeatures(document)} + - Let {assignments} be a map of ({element}: *Any Named Element*) -> {feature}: `Directive` | {null}, initally empty + - For each named schema element {e} within the {document} + - Let {name} be the name of the {e} + - If {e} is a Directive and {name} is a key within {features}, + - Insert {e} => {features}`[`{name}`]` into {assignments} + - **Continue** to next {e} + - If {name} begins with {"__"}, + - Insert {e} => {null} into {assignments} + - **Continue** to next {e} + - If {name} contains the substring {"__"}, + - Partition {name} into `[`{prefix}, {base}`]` at the first {"__"} (that is, find the shortest {prefix} and longest {base} such that {name} = {prefix} + {"__"} + {base}) + - If {prefix} exists within {features}, insert {e} => {features}`[`{prefix}`]` into {assignments} + - Else, insert {e} => {null} into {assignments} + - **Continue** to next {e} + - Insert {e} => {null} into {assignments} + - **Return** {assignments} + +## Is In API? + +Determine if any schema element is included in the [API](#sec-Parts-of-a-Core-Schema) described by the core schema. A schema element is any part of a GraphQL document using type system definitions that has a [name](https://spec.graphql.org/draft/#Name). + +IsInAPI(element) : + - Let {assignments} be the result of assigning features to elements via {AssignFeatures(document)} + - If {assignments}`[`{element}`]` is {null}, **Return** {true} + - Else, **Return** {false} + +Note: Later versions of this specification may add other ways to affect the behavior of this algorithm, but those mechanisms will only be enabled if you reference those hypothetical versions of this specification. + diff --git a/core/v0.1/good-unique-prefix-multi.graphql b/core/v0.1/good-unique-prefix-multi.graphql new file mode 100644 index 0000000..8eafe08 --- /dev/null +++ b/core/v0.1/good-unique-prefix-multi.graphql @@ -0,0 +1,9 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://specs.example.com/A/2.0", as: "A2") # name is A2 +{ + query: Query +} + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.1/index.html b/core/v0.1/index.html new file mode 100644 index 0000000..df41bb3 --- /dev/null +++ b/core/v0.1/index.html @@ -0,0 +1,1705 @@ + + + + +Core Schemas + + + + + +
+
+

Core Schemas

+
+

flexible metadata for GraphQL schemas

+ + + +
StatusRelease
Version0.1
+ + +

GraphQL provides directives as a means of attaching user‐defined metadata to a GraphQL document. Directives are highly flexible, and can be used to suggest behavior and define features of a graph which are not otherwise evident in the schema.

+

Alas, GraphQL does not provide a mechanism to globally identify or version directives. Given a particular directive—e.g. @join—processors are expected to know how to interpret the directive based only on its name, definition within the document, and additional configuration from outside the document. This means that programs interpreting these directives have two options:

+
    +
  1. rely on a hardcoded interpretation for directives with certain signatures, or
  2. +
  3. accept additional configuration about how to interpret directives in the schema.
  4. +
+

The first solution is fragile, particularly as GraphQL has no built‐in namespacing mechanisms, so the possibility of name collisions always looms.

+

The second is unfortunate: GraphQL schemas are generally intended to be self‐describing, and requiring additional configuration subtly undermines this guarantee: given just a schema, programs do not necessarily know how to interpret it, and certainly not how to serve it. It also creates the possibility for the schema and configuration to fall out of sync, leading to issues which can manifest late in a deployment pipeline.

+

Introducing core schemas.

+

A basic core schema:

+
Example № 1 A basic core schema
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/example/v1.0")
+{
+  query: Query
+}
+
+type Query {
+  field: Int @example
+}
+
+directive @example on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+

Core schemas provide a concise mechanism for schema documents to specify the metadata they provide. Metadata is grouped into features, which typically define directives and associated types (e.g. scalars and inputs which serve as directive inputs). Additionally, core schemas provide:

+ +

Core schemas are not a new language. All core schema documents are valid GraphQL schema documents. However, this specification introduces new requirements, so not all valid GraphQL schemas are valid core schemas.

+

The broad intention behind core schemas is to provide a single document which provides all the necessary configuration for programs that process and serve the schema to GraphQL clients, primarily by following directives in order to determine how to resolve queries made against that schema.

+
+ +
+
+

1Parts of a Core Schema

+

When talking about a core schema, we can broadly break it into two pieces:

+ +

This reflects how core schemas are used: a core schema contains a GraphQL interface (the API) along with metadata about how to implement that interface (the machinery). Exposing the machinery to clients is unnecessary, and may in some cases constitute a security issue (for example, the machinery for a public‐facing graph router will likely reference internal services, possibly exposing network internals which should not be visible to the general public).

+

A key feature of core schemas is that it is always possible to derive a core schema’s API without any knowledge of the features used by the document (with the exception of the core feature itself). Specifically, named elements are not included in the API schema if they are named something__likeThis or are a directive named @something, and something is the prefix of a feature declared with @core.

+

A formal description is provided by the IsInAPI algorithm.

+
+
+

2Actors

+
Actors who may be interested in the core schemas
graph TB + classDef bg fill:none,color:#22262E; + author("👩🏽‍💻 🤖  Author"):::bg-->schema(["☉ Core Schema"]):::bg + schema-->proc1("🤖  Processor"):::bg + proc1-->output1(["☉ Core Schema[0]"]):::bg + output1-->proc2("🤖  Processor"):::bg + proc2-->output2(["☉ Core Schema[1]"]):::bg + output2-->etc("..."):::bg + etc-->final(["☉ Core Schema [final]"]):::bg + final-->core("🤖 Data Core"):::bg + schema-->reader("👩🏽‍💻 Reader"):::bg + output1-->reader + output2-->reader + final-->reader +
+
+
+

3Basic Requirements

+

Core schemas:

+
    +
  1. MUST be valid GraphQL schema documents,
  2. +
  3. MUST contain exactly one SchemaDefinition, and
  4. +
  5. MUST use the @core directive on their schema definition to declare any features they reference by using @core to reference a well‐formed feature URL.
  6. +
+

The first @core directive on the schema MUST reference the core spec itself, i.e. this document.

+
Example № 2 Basic core schema using @core and @example
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/example/v1.0")
+{
+  query: Query
+}
+
+type Query {
+  field: Int @example
+}
+
+directive @example on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+
+

3.1Unspecified directives are passed through by default

+

Existing schemas likely contain definitions for directives which are not versioned, have no specification document, and are intended mainly to be passed through. This is the default behavior for core schema processors:

+
Example № 3 Unspecified directives are passed through
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+{
+  query: Query
+}
+
+type SomeType {
+  field: Int @another
+}
+
+# `@another` is unspecified. Core processors will not extract metadata from
+# it, but its definition and all usages within the schema will be exposed
+# in the API.
+directive @another on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+
+
+

3.2Renaming core itself

+

It is possible to rename the core feature itself with the same as: mechanism used for all features:

+
Example № 4 Renaming @core to @coreSchema
schema
+  @coreSchema(feature: "https://specs.apollo.dev/core/v0.1", as: "coreSchema")
+  @coreSchema(feature: "https://example.com/example/v1.0")
+{
+  query: Query
+}
+
+type SomeType {
+  field: Int @example
+}
+
+directive @coreSchema(feature: String!, as: String)
+  repeatable on SCHEMA
+directive @example on FIELD_DEFINITION
+
+
+
+
+

4Directive Definitions

+

All core schemas use the @core directive to declare their use of the core feature itself as well as any other core features they use.

+

In order to use these directives in your schema, GraphQL requires you to include their definitions in your schema.

+

Processors MUST validate that you have defined the directives with the same arguments, locations, and repeatable flag as given below. Specifically, the bootstrapping algorithm validates that the @core directive has a definition matching the definition given below. (The bootstrapping algorithm does not require processors to validate other aspects of the directive declaration such as description strings or argument ordering. The main purpose of this validation is to ensure that directive arguments have the type and default values expected by the specification.)

+

The following declares the directive defined by this specification. You SHOULD define the directives in your core schema by including the following text in your schema document.

+
directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+

When writing a specification for your own core feature, you SHOULD include a section like this one with sample definitions to copy into schemas, and you SHOULD require processors to validate that directive definitions in documents match your sample definitions.

+
+
+

5Directives

+
+

5.1@core

+

Declare a core feature present in this schema.

+
directive @core(
+  feature: String!,
+  as: String)
+  repeatable on SCHEMA
+
+

Documents MUST include a definition for the @core directive which includes all of the arguments defined above with the same types and default values.

+
+

5.1.1feature: String!

+

A feature URL specifying the directive and associated schema elements. When viewed, the URL SHOULD provide the content of the appropriate version of the specification in some human‐readable form. In short, a human reader should be able to click the link and go to the docs for the version in use. There are specific requirements on the format of the URL, but it is not required that the content be machine‐readable in any particular way.

+

Feature URLs contain information about the spec’s prefix and version.

+

Feature URLs serve two main purposes:

+
    +
  • Directing human readers to documentation about the feature
  • +
  • Providing tools with information about the specs in use, along with enough information to select and invoke an implementation
  • +
+

Feature URLs SHOULD be RFC 3986 URLs. When viewed, the URL SHOULD provide the specification of the selected version of the feature in some human‐readable form; a human reader should be able to click the link and go to the correct version of the docs.

+

Although they are not prohibited from doing so, it’s assumed that processors will not load the content of feature URLs. Published specifications are not required to be machine‐readable, and this spec places no requirements on the structure or syntax of the content to be found there.

+

There are, however, requirements on the structure of the URL itself:

+
Basic anatomy of a feature URL
+ https://spec.example.com/a/b/c/exampleFeature/v1.0 + +

The final two segments of the URL’s path MUST contain the feature’s name and a version tag. The content of the URL up to and including the name—but excluding the / after the name and the version tag—is the feature’s identity. Trailing slashes at the end of the URL (ie, after the version tag) should be ignored. For the above example,

identity: "https://spec.example.com/a/b/c/exampleFeature"
A global identifier for the feature. Processors can treat this as an opaque string identifying the feature (but not the version of the feature) for purposes of selecting an appropriate implementation. The identity never has a trailing /.
name: "exampleFeature"
The feature’s name, for purposes of prefixing schema elements it defines.
version: "v1.0"
The tag for the version of the feature used to author the document. Processors MUST select an implementation of the feature which can satisfy the specified version.

+

The version tag MUST be a valid VersionTag. The name MUST be a valid GraphQL name which does not include the namespace separator ("__").

+
+

5.1.1.1Ignore meaningless URL components

+

When extracting the URL’s name and version, processors MUST ignore any url components which are not assigned a meaning. This spec assigns meaning to the final two segments of the path. Other URL components—particularly query strings and fragments, if present—MUST be ignored for the purposes of extracting the name and version.

+
Ignoring meaningless parts of a URL
+ https://example.com/exampleSpec/v1.0/?key=val&k2=v2#frag + +
+
+

5.1.1.2Why is versioning in the URL, not a directive argument?

+

The version is in the URL because when a human reader visits the URL, we would like them to be taken to the documentation for the version of the feature used by this document. Many text editors will turn URLs into hyperlinks, and it’s highly desirable that clicking the link takes the user to the correct version of the docs. Putting the version information in a separate argument to the @core directive would prevent this.

+
+
+
+

5.1.2as: String

+

Change the names of directives and schema elements from this specification. The specified string MUST be a valid GraphQL name and MUST NOT contain the namespace separator (two underscores, "__") or end with an underscore.

+

When as: is provided, processors looking for prefixed schema elements MUST look for elements whose names are the specified name with the prefix replaced with the name provided to the as: argument.

+
Example № 5 Using @core(feature:, as:) to use a feature with a custom name
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://spec.example.com/example/v1.0", as: "eg")
+{
+  query: Query
+}
+
+type User {
+  # Specifying `as: "eg"` transforms @example into @eg
+  name: String @eg(data: ITEM)
+}
+
+# Additional specified schema elements must have their prefixes set
+# to the new name.
+#
+# The spec at https://spec.example.com/example/v1.0 calls this enum
+# `example__Data`, but because of the `as:` argument above, processors
+# will use this `eg__Data` enum instead.
+enum eg__Data {
+  ITEM
+}
+
+# Name transformation must also be applied to definitions pulled in from
+# specifications.
+directive @eg(data: eg__Data) on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+
+
+
+
+

6Prefixing

+

With the exception of a single root directive, core feature specifications MUST prefix all schema elements they introduce. The prefix:

+
    +
  1. MUST match the name of the feature as derived from the feature’s specification URL,
  2. +
  3. MUST be a valid GraphQL name, and
  4. +
  5. MUST NOT contain the core namespace separator, which is two underscores ("__"), and
  6. +
  7. MUST NOT end with an underscore (which would create ambiguity between whether "x___y" is prefix x_ for element y or prefix x for element _y).
  8. +
+

Prefixed names consist of the name of the feature, followed by two underscores, followed by the name of the element, which can be any valid GraphQL name. For instance, the core specification (which you are currently reading) introduces an element named @core, and the join specification introduces an element named @join__field (among others).

+
+Note +that both parts must be valid GraphQL names, and GraphQL names cannot start with digits, so core feature specifications cannot introduce names like @feature__24hours.
+

A feature’s root directive is an exception to the prefixing requirements. Feature specifications MAY introduce a single directive which carries only the name of the feature, with no prefix required. For example, the core specification introduces a @core directive. This directive has the same name as the feature (”core”), and so requires no prefix.

+
Example № 6 Using the @core directive without changing the prefix
schema
+ @core(feature: "https://specs.apollo.dev/core/v0.1")
+ @core(feature: "https://spec.example.com/example/v1.0") {
+  query: Query
+}
+
+type User {
+  name: String @example(data: ITEM)
+}
+
+# An enum used to provide structured data to the example spec.
+# It is prefixed with the name of the spec.
+enum example__Data {
+  ITEM
+}
+
+directive @example(data: example__Data) on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+

The prefix MUST NOT be elided within documentation; definitions of schema elements provided within the spec MUST include the feature’s name as a prefix.

+
+

6.1Elements which must be prefixed

+

Feature specs MUST prefix the following schema elements:

+
    +
  • the names of any object types, interfaces, unions, enums, or input object types defined by the feature
  • +
  • the names of any directives introduced in the spec, with the exception of the root directive, which must have the same name as the feature
  • +
+
Example № 7 Prefixing
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://spec.example.com/featureA/v1.0")
+  @core(feature: "https://spec.example.com/featureB/v2.0", as: "B") {
+  query: Query
+}
+
+"""
+featureA__SomeType is a type defined by feature A.
+"""
+type featureA__SomeType {
+  """
+  nativeField is a field defined by featureA on a type also defined
+  by featureA (namely featureA__SomeType)
+  """
+  nativeField: Int @featureA__fieldDirective
+}
+
+"""
+featureA__SomeInput is an input specified by feature A
+"""
+input featureA__SomeInput {
+  """
+  nativeInputField is defined by featureA
+  """
+  nativeInputField: Int
+}
+
+"""
+featureA__Items is specified by feature A
+"""
+enum featureA__Items { ONE, TWO, THREE @B }
+
+"""
+@B is the root directive defined by featureB
+
+Root directives are named after their feature
+"""
+directive @B on ENUM_VALUE
+
+"""
+@featureA__fieldDirective is a non-root (prefixed) directive defined by featureA
+"""
+directive @featureA__fieldDirective on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+
+
+
+

7Versioning

+ + + + + + +
+PositiveDigit
1|2|3|4|5|6|7|8|9
+
+

Specs are versioned with a subset of a Semantic Version Number containing only the major and minor parts. Thus, specifications SHOULD provide a version of the form vMajor.Minor, where both integers ≥ 0.

+
Example № 8 Valid version tags
v2.2
+v1.0
+v1.1
+v0.1
+
+

As specified by semver, spec authors SHOULD increment the:

+
    +
  • MAJOR version when you make incompatible API changes,
  • +
  • MINOR version when you add functionality in a backwards compatible manner
  • +
+
+

Patch and pre‐release qualifiers are judged to be not particularly meaningful in the context of core features, which are (by definition) interfaces rather than implementations. The patch component of a semver denotes a bug fix which is backwards compatible—that is, a change to the implementation which does not affect the interface. Patch‐level changes in the version of a spec denote wording clarifications which do not require implementation changes. As such, it is not important to track them for the purposes of version resolution.

+

As with semver, the 0.x version series is special: there is no expectation of compatibility between versions 0.x and 0.y. For example, a processor must not activate implementation 0.4 to satisfy a requested version of 0.2.

+
+

7.1Satisfaction

+

Given a version requested by a document and an available version of an implementation, the following algorithm will determine if the available version can satisfy the requested version:

+
+Satisfies(requested, available)
    +
  1. If requested.Majoravailable.Major, return false
  2. +
  3. If requested.Major = 0, return requested.Minor = available.Minor
  4. +
  5. Return requested.Minoravailable.Minor
  6. +
+
+
+
+

7.2Referencing versions and activating implementations

+

Schema documents MUST reference a feature version which supports all the schema elements and behaviors required by the document. As a practical matter, authors will generally prefer to reference a version they have reason to believe is supported by the most processors; depending on context, this might be an old stable version with a low major version, or a new less‐deprecated version with a large major version.

+

If a processor chooses to activate support for a feature, the processor MUST activate an implementation which can satisfy the version required by the document.

+
+
+
+

8Processing Schemas

+
graph LR + schema(["📄 Input Schema"]):::file-->proc("🤖  Processor") + proc-->output(["📄 Output Schema"]):::file + classDef file fill:none,color:#22262E; + style proc fill:none,stroke:fuchsia,color:fuchsia; +

A common use case is that of a processor which consumes a valid input schema and generates an output schema.

+

The general guidance for processor behavior is: don’t react to what you don’t understand.

+

Specifically, processors:

+ +

Processors MAY accept configuration which overrides these default behaviors.

+

Additionally, processors which prepare the schema for final public consumption MAY choose to eliminate all unknown directives and prefixed types in order to hide schema implementation details within the published schema. This will impair the operation of tooling which relies on these directives—such tools will not be able to run on the output schema, so the benefits and costs of this kind of information hiding should be weighed carefully on a case‐by‐case basis.

+
+
+

9Validations & Algorithms

+

This section lays out algorithms for processing core schemas.

+

Algorithms described in this section may produce validation failures if a document does not conform to the requirements core schema document. Validation failures SHOULD halt processing. Some consumers, such as authoring tools, MAY attempt to continue processing in the presence of validation failures, but their behavior in such cases is unspecified.

+
+

9.1Bootstrapping

+

Determine the name of the core specification within the document.

+

It is possible to rename the core feature within a document. This process determines the actual name for the core feature if one is present.

+
    +
  • Fails the Has Schema validation if there are no SchemaDefinitions in the document
  • +
  • Fails the Has Core Feature validation if the core feature itself is not referenced with a @core directive within the document
  • +
  • Fails the Bootstrap Core Feature Listed First validation if the reference to the core feature is not the first @core directive on the document’s SchemaDefinition
  • +
  • Fails the Core Directive Incorrect Definition validation if the @core directive definition does not match the directive as defined by this specification.
  • +
+

For the purposes of this algorithm, a directive’s definition in a schema matches a definition provided in this specification if:

+
    +
  • Its arguments have the specified names, types, and default values (or lack thereof)
  • +
  • It is defined as repeatable if and only if the specification’s definition defines it as repeatable
  • +
  • The set of locations it belongs to is the same set of locations in the specification’s definition.
  • +
+

The following aspects may differ between the definition in the schema and the definition in the specification without preventing the definitions from matching:

+
    +
  • The name of the directive (due to prefixing)
  • +
  • The order of arguments
  • +
  • The order of locations
  • +
  • The directive’s description string
  • +
  • Argument description strings
  • +
  • Directives applied to argument definitions
  • +
+
+Bootstrap(document)
    +
  1. Let schema be the only SchemaDefinition in document. (Note that legal GraphQL documents must include at most one SchemaDefinition.)
      +
    1. ...if no SchemaDefinitions are present in document, the Has Schema validation fails.
    2. +
    +
  2. +
  3. For each directive d on schema,
      +
    1. If d has a feature: argument which parses as a feature URL, and whose identity is "https://specs.apollo.dev/core/" and whose version is "v0.1", and either d has an as: argument whose value is equal to d‘s name or d does not have an as: argument and d‘s name is core:
        +
      1. If any directive on schema listed before d has the same name as d, the Bootstrap Core Feature Listed First validation fails.
      2. +
      3. If the definition of the directive d does not match the definition of @core in this specification, the Core Directive Incorrect Definition validation fails.
      4. +
      5. Otherwise, Return d‘s name.
      6. +
      +
    2. +
    +
  4. +
  5. If no matching directive was found, the Has Core Feature validation fails.
  6. +
+
+
+
+

9.2Feature Collection

+

Collect a map of (featureName: String) → Directive, where Directive is a @core Directive which introduces the feature named featureName into the document.

+
    +
  • Fails the Name Uniqueness validation if feature names are not unique within the document.
  • +
  • Fails Invalid Feature URL validation for any invalid feature URLs.
  • +
+
+CollectFeatures(document)
    +
  1. Let coreName be the name of the core feature found via Bootstrap(document)
  2. +
  3. Let features be a map of featureName: StringDirective, initially empty.
  4. +
  5. For each directive d named coreName on the SchemaDefinition within document,
      +
    1. Let specifiedFeatureName and version be the result of parsing d‘s feature: argument according to the specified rules for feature URLs
    2. +
    3. If the feature: is not present or fails to parse:
        +
      1. The Invalid Feature URL validation fails for d,
      2. +
      +
    4. +
    5. Let featureName be the d‘s as: argument or, if the argument is not present, specifiedFeatureName
    6. +
    7. If featureName exists within features, the Name Uniqueness validation fails.
    8. +
    9. Insert featureNamed into features
    10. +
    +
  6. +
  7. Return features
  8. +
+
+

Prefixes, whether implicit or explicit, must be unique within a document. Valid:

+
Example № 9 Unique prefixes
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://spec.example.com/featureA/v1.0")
+  @core(feature: "https://spec.example.com/featureB/v2.0", as: "B") {
+  query: Query
+}
+

It is also valid to reference multiple versions of the same spec under different prefixes:

+
Example № 10 Explicit prefixes allow multiple versions of the same spec to coexist within a Document
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/A/1.0")               # name is A
+  @core(feature: "https://specs.example.com/A/2.0", as: "A2")     # name is A2
+{
+  query: Query
+}
+

Without the explicit as:, the above would be invalid:

+
Counter Example № 11 Non‐unique prefixes with multiple versions of the same spec
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/A/1.0") # name is A
+  @core(feature: "https://specs.example.com/A/2.0") # name is A
+{
+  query: Query
+}
+

Different specs with the same prefix are also invalid:

+
Counter Example № 12 Different specs with non‐unique prefixes
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/A/1.0")              # name is A
+  @core(feature: "https://www.specs.com/specA/1.1", as: "A")     # name is A
+{
+  query: Query
+}
+
+
+

9.3Assign Features

+

Create a map of element: Any Named Elementfeature: Directive | null, associating every named schema element within the document with a feature directive, or null if it is not associated with a feature.

+
+AssignFeatures(document)
    +
  1. Let features be the result of collecting features via CollectFeatures(document)
  2. +
  3. Let assignments be a map of (element: Any Named Element) → feature: Directive | null, initally empty
  4. +
  5. For each named schema element e within the document
      +
    1. Let name be the name of the e
    2. +
    3. If e is a Directive and name is a key within features,
        +
      1. Insert efeatures[name] into assignments
      2. +
      3. Continue to next e
      4. +
      +
    4. +
    5. If name begins with "__",
        +
      1. Insert enull into assignments
      2. +
      3. Continue to next e
      4. +
      +
    6. +
    7. If name contains the substring "__",
        +
      1. Partition name into [prefix, base] at the first "__" (that is, find the shortest prefix and longest base such that name = prefix + "__" + base)
      2. +
      3. If prefix exists within features, insert efeatures[prefix] into assignments
          +
        1. Else, insert enull into assignments
        2. +
        +
      4. +
      5. Continue to next e
      6. +
      +
    8. +
    9. Insert enull into assignments
    10. +
    +
  6. +
  7. Return assignments
  8. +
+
+
+
+

9.4Is In API?

+

Determine if any schema element is included in the API described by the core schema. A schema element is any part of a GraphQL document using type system definitions that has a name.

+
+IsInAPI(element)
    +
  1. Let assignments be the result of assigning features to elements via AssignFeatures(document)
  2. +
  3. If assignments[element] is null, Return true
  4. +
  5. Else, Return false
  6. +
+
+
+Note +Later versions of this specification may add other ways to affect the behavior of this algorithm, but those mechanisms will only be enabled if you reference those hypothetical versions of this specification.
+
+
+

§Index

  1. AssignFeatures
  2. Bootstrap
  3. CollectFeatures
  4. Digit
  5. IsInAPI
  6. Major
  7. Minor
  8. NumericIdentifier
  9. PositiveDigit
  10. Satisfies
  11. Version
  12. VersionTag
+ + +
+
+ +
  1. 1Parts of a Core Schema
  2. +
  3. 2Actors
  4. +
  5. 3Basic Requirements + +
      +
    1. 3.1Unspecified directives are passed through by default
    2. +
    3. 3.2Renaming core itself
    4. +
    +
  6. +
  7. 4Directive Definitions
  8. +
  9. 5Directives + +
      +
    1. 5.1@core + +
        +
      1. 5.1.1feature: String! + +
          +
        1. 5.1.1.1Ignore meaningless URL components
        2. +
        3. 5.1.1.2Why is versioning in the URL, not a directive argument?
        4. +
        +
      2. +
      3. 5.1.2as: String
      4. +
      +
    2. +
    +
  10. +
  11. 6Prefixing + +
      +
    1. 6.1Elements which must be prefixed
    2. +
    +
  12. +
  13. 7Versioning + +
      +
    1. 7.1Satisfaction
    2. +
    3. 7.2Referencing versions and activating implementations
    4. +
    +
  14. +
  15. 8Processing Schemas
  16. +
  17. 9Validations & Algorithms + +
      +
    1. 9.1Bootstrapping
    2. +
    3. 9.2Feature Collection
    4. +
    5. 9.3Assign Features
    6. +
    7. 9.4Is In API?
    8. +
    +
  18. +
  19. §Index
  20. +
+
+ +
+ + + diff --git a/core/v0.1/netlify.toml b/core/v0.1/netlify.toml new file mode 100644 index 0000000..f1a19b0 --- /dev/null +++ b/core/v0.1/netlify.toml @@ -0,0 +1,6 @@ +# Netlify Admin: https://app.netlify.com/sites/apollo-specs-core/ +# Docs: https://docs.netlify.com/configure-builds/file-based-configuration/ + +[build] + command = "npm run build" + publish = ".dist/" diff --git a/core/v0.1/package.json b/core/v0.1/package.json new file mode 100644 index 0000000..03a1ce4 --- /dev/null +++ b/core/v0.1/package.json @@ -0,0 +1,18 @@ +{ + "name": "@apollo/core", + "version": "1.0.0", + "description": "for GraphQL schemas with extensible metadata", + "repository": "https://github.com/apollo-specs/core", + "main": "index.js", + "scripts": { + "build": "rsync -avz --exclude .dist . .dist && spec-md core.spec.md > .dist/index.html" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@queerviolet/speck": "git://github.com/queerviolet/speck.git#main", + "chokidar-cli": "^2.1.0", + "watch": "^1.0.2" + } +} diff --git a/core/v0.1/prefix-uniqueness.graphql b/core/v0.1/prefix-uniqueness.graphql new file mode 100644 index 0000000..a26a309 --- /dev/null +++ b/core/v0.1/prefix-uniqueness.graphql @@ -0,0 +1,25 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://specs.example.com/A/2.0", as: "A2") # name is A2 +{ + query: Query +} + +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://specs.example.com/A/2.0") # name is A +{ + query: Query +} + +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://www.specs.com/specA/1.1", as: "A") # name is A +{ + query: Query +} + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.1/prefixing.graphql b/core/v0.1/prefixing.graphql new file mode 100644 index 0000000..12c5da7 --- /dev/null +++ b/core/v0.1/prefixing.graphql @@ -0,0 +1,46 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://spec.example.com/featureA/v1.0") + @core(feature: "https://spec.example.com/featureB/v2.0", as: "B") { + query: Query +} + +""" +featureA__SomeType is a type defined by feature A. +""" +type featureA__SomeType { + """ + nativeField is a field defined by featureA on a type also defined + by featureA (namely featureA__SomeType) + """ + nativeField: Int @featureA__fieldDirective +} + +""" +featureA__SomeInput is an input specified by feature A +""" +input featureA__SomeInput { + """ + nativeInputField is defined by featureA + """ + nativeInputField: Int +} + +""" +featureA__Items is specified by feature A +""" +enum featureA__Items { ONE, TWO, THREE @B } + +""" +@B is the root directive defined by featureB + +Root directives are named after their feature +""" +directive @B on ENUM_VALUE + +""" +@featureA__fieldDirective is a non-root (prefixed) directive defined by featureA +""" +directive @featureA__fieldDirective on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.2/.gitignore b/core/v0.2/.gitignore new file mode 100644 index 0000000..b768a31 --- /dev/null +++ b/core/v0.2/.gitignore @@ -0,0 +1,3 @@ +node_modules +.dist +package-lock.json \ No newline at end of file diff --git a/core/v0.2/bad-non-unique-prefix-multi.graphql b/core/v0.2/bad-non-unique-prefix-multi.graphql new file mode 100644 index 0000000..221f3e4 --- /dev/null +++ b/core/v0.2/bad-non-unique-prefix-multi.graphql @@ -0,0 +1,9 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://specs.example.com/A/2.0") # name is A +{ + query: Query +} + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.2/bad-non-unique-prefix.graphql b/core/v0.2/bad-non-unique-prefix.graphql new file mode 100644 index 0000000..749db43 --- /dev/null +++ b/core/v0.2/bad-non-unique-prefix.graphql @@ -0,0 +1,9 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://www.specs.com/specA/1.1", as: "A") # name is A +{ + query: Query +} + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.2/basic.graphql b/core/v0.2/basic.graphql new file mode 100644 index 0000000..454191b --- /dev/null +++ b/core/v0.2/basic.graphql @@ -0,0 +1,14 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/example/v1.0") +{ + query: Query +} + +type Query { + field: Int @example +} + +directive @example on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.2/core-v0.2.md b/core/v0.2/core-v0.2.md new file mode 100644 index 0000000..ff1aa04 --- /dev/null +++ b/core/v0.2/core-v0.2.md @@ -0,0 +1,577 @@ +# Core Schemas + +

flexible metadata for GraphQL schemas

+ +```raw html + + + +
StatusRelease
Version0.2
+ + +``` + +[GraphQL](https://spec.graphql.org/) provides directives as a means of attaching user-defined metadata to a GraphQL document. Directives are highly flexible, and can be used to suggest behavior and define features of a graph which are not otherwise evident in the schema. + +Alas, *GraphQL does not provide a mechanism to globally identify or version directives*. Given a particular directive—e.g. `@join`—processors are expected to know how to interpret the directive based only on its name, definition within the document, and additional configuration from outside the document. This means that programs interpreting these directives have two options: + + 1. rely on a hardcoded interpretation for directives with certain signatures, or + 2. accept additional configuration about how to interpret directives in the schema. + +The first solution is fragile, particularly as GraphQL has no built-in namespacing mechanisms, so the possibility of name collisions always looms. + +The second is unfortunate: GraphQL schemas are generally intended to be self-describing, and requiring additional configuration subtly undermines this guarantee: given just a schema, programs do not necessarily know how to interpret it, and certainly not how to serve it. It also creates the possibility for the schema and configuration to fall out of sync, leading to issues which can manifest late in a deployment pipeline. + +Introducing **core schemas**. + +```html diagram + + + + + core schema + +``` + +A basic core schema: + +:::[example](basic.graphql) -- A basic core schema + +**Core schemas** provide a concise mechanism for schema documents to specify the metadata they provide. Metadata is grouped into **features**, which typically define directives and associated types (e.g. scalars and inputs which serve as directive inputs). Additionally, core schemas provide: + - [**Flexible namespacing rules.**](#sec-Prefixing) It is always possible to represent any GraphQL schema within a core schema document. Additionally, documents can [choose the names](#@core/as) they use for the features they reference, guaranteeing that namespace collisions can always be resolved. + - [**Versioning.**](#sec-Versioning) Feature specifications follow [semver-like semantic versioning principles](#sec-Versioning), which helps schema processors determine if they are able to correctly interpret a document's metadata. + +**Core schemas are not a new language.** All core schema documents are valid GraphQL schema documents. However, this specification introduces new requirements, so not all valid GraphQL schemas are valid core schemas. + +The broad intention behind core schemas is to provide a *single document* which provides all the necessary configuration for programs that process and serve the schema to GraphQL clients, primarily by following directives in order to determine how to resolve queries made against that schema. + +# Parts of a Core Schema + +When talking about a core schema, we can broadly break it into two pieces: +- an **API** consisting of schema elements (objects, interfaces, enums, directives, etc.) which SHOULD be served to clients, and +- **machinery** containing document metadata. This typically consists of directives and associated input types (such as enums and input objects), but may include any schema element. Machinery MUST NOT be served to clients. Specifically, machinery MUST NOT be included in introspection responses or used to validate or execute queries. + +This reflects how core schemas are used: a core schema contains a GraphQL interface (the *API*) along with metadata about how to implement that interface (the *machinery*). Exposing the machinery to clients is unnecessary, and may in some cases constitute a security issue (for example, the machinery for a public-facing graph router will likely reference internal services, possibly exposing network internals which should not be visible to the general public). + +A key feature of core schemas is that it is always possible to derive a core schema's API without any knowledge of the features used by the document (with the exception of the `core` feature itself). Specifically, named elements are not included in the API schema if they are named `something__likeThis` or are a directive named `@something`, and `something` is the prefix of a feature declared with {@core}. + +A formal description is provided by the [IsInAPI](#sec-Is-In-API-) algorithm. + +# Actors + +```mermaid diagram -- Actors who may be interested in the core schemas +graph TB + classDef bg fill:none,color:#22262E; + author("👩🏽‍💻 🤖  Author"):::bg-->schema(["☉ Core Schema"]):::bg + schema-->proc1("🤖  Processor"):::bg + proc1-->output1(["☉ Core Schema[0]"]):::bg + output1-->proc2("🤖  Processor"):::bg + proc2-->output2(["☉ Core Schema[1]"]):::bg + output2-->etc("..."):::bg + etc-->final(["☉ Core Schema [final]"]):::bg + final-->core("🤖 Data Core"):::bg + schema-->reader("👩🏽‍💻 Reader"):::bg + output1-->reader + output2-->reader + final-->reader +``` + +- **Authors (either human or machine)** write an initial core schema as specified in this document, including versioned {@core} requests for all directives they use +- **Machine processors** can process core schemas and output new core schemas. The versioning of directives and associated schema elements provided by the {@core} allows processors to operate on directives they understand and pass through directives they do not. +- **Human readers** can examine the core schema at various stages of processing. At any stage, they can examine the {@core} directives and follow URLs to the specification, receiving an explanation of the requirements of the specification and what new directives, types, and other schema objects are available within the document. +- **Data cores** can then pick up the processed core schema and provide some data-layer service with it. Typically this means serving the schema's API as a GraphQL endpoint, using metadata defined by machinery to inform how it processes operations it receives. However, data cores may perform other tasks described in the core schema, such as routing to backend services, caching commonly-accessed fields and queries, and so on. The term "data core" is intended to capture this multiplicity of possible activities. + +# Basic Requirements + +Core schemas: + 1. MUST be valid GraphQL schema documents, + 2. MUST contain exactly one `SchemaDefinition`, and + 3. MUST use the {@core} directive on their schema definition to declare any features they reference by using {@core} to reference a [well-formed feature URL](#@core/feature). + +The first {@core} directive on the schema MUST reference the core spec itself, i.e. this document. + +:::[example](basic.graphql) -- Basic core schema using {@core} and `@example` + +## Unspecified directives are passed through by default + +Existing schemas likely contain definitions for directives which are not versioned, have no specification document, and are intended mainly to be passed through. This is the default behavior for core schema processors: + +```graphql example -- Unspecified directives are passed through +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") +{ + query: Query +} + +type SomeType { + field: Int @another +} + +# `@another` is unspecified. Core processors will not extract metadata from +# it, but its definition and all usages within the schema will be exposed +# in the API. +directive @another on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA +``` + +## Renaming core itself + +It is possible to rename the core feature itself with the same [`as:`](#@core/as) mechanism used for all features: + +```graphql example -- Renaming {@core} to {@coreSchema} +schema + @coreSchema(feature: "https://specs.apollo.dev/core/v0.1", as: "coreSchema") + @coreSchema(feature: "https://example.com/example/v1.0") +{ + query: Query +} + +type SomeType { + field: Int @example +} + +directive @coreSchema(feature: String!, as: String) + repeatable on SCHEMA +directive @example on FIELD_DEFINITION +``` + +# Directive Definitions + +All core schemas use the [{@core}](#@core) directive to declare their use of the `core` feature itself as well as any other core features they use. + +In order to use these directives in your schema, GraphQL requires you to include their definitions in your schema. + +Processors MUST validate that you have defined the directives with the same arguments, locations, and `repeatable` flag as given below. Specifically, the [bootstrapping](#sec-Bootstrapping) algorithm validates that the `@core` directive has a definition matching the definition given below. (The bootstrapping algorithm does not require processors to validate other aspects of the directive declaration such as description strings or argument ordering. The main purpose of this validation is to ensure that directive arguments have the type and default values expected by the specification.) + +The following declares the directive defined by this specification. You SHOULD define the directives in your core schema by including the following text in your schema document. + +```graphql +directive @core(feature: String!, as: String) repeatable on SCHEMA +``` + +When writing a specification for your own core feature, you SHOULD include a section like this one with sample definitions to copy into schemas, and you SHOULD require processors to validate that directive definitions in documents match your sample definitions. + +# Directives + +##! @core + +Declare a core feature present in this schema. + +```graphql definition +directive @core( + feature: String!, + as: String, + for: core__Purpose) + repeatable on SCHEMA +``` + +Documents MUST include a definition for the {@core} directive which includes all of the arguments defined above with the same types and default values. + +###! feature: String! + +A feature URL specifying the directive and associated schema elements. When viewed, the URL SHOULD provide the content of the appropriate version of the specification in some human-readable form. In short, a human reader should be able to click the link and go to the docs for the version in use. There are specific requirements on the format of the URL, but it is not required that the *content* be machine-readable in any particular way. + +Feature URLs contain information about the spec's [prefix](#sec-Prefixing) and [version](#sec-Versioning). + +Feature URLs serve two main purposes: + - Directing human readers to documentation about the feature + - Providing tools with information about the specs in use, along with enough information to select and invoke an implementation + +Feature URLs SHOULD be [RFC 3986 URLs](https://tools.ietf.org/html/rfc3986). When viewed, the URL SHOULD provide the specification of the selected version of the feature in some human-readable form; a human reader should be able to click the link and go to the correct version of the docs. + +Although they are not prohibited from doing so, it's assumed that processors will not load the content of feature URLs. Published specifications are not required to be machine-readable, and [this spec](.) places no requirements on the structure or syntax of the content to be found there. + +There are, however, requirements on the structure of the URL itself: + +```html diagram -- Basic anatomy of a feature URL + + https://spec.example.com/a/b/c/exampleFeature/v1.0 + +``` + +The final two segments of the URL's [path](https://tools.ietf.org/html/rfc3986#section-3.3) MUST contain the feature's name and a [version tag](#sec-Versioning). The content of the URL up to and including the name—but excluding the `/` after the name and the version tag—is the feature's *identity*. Trailing slashes at the end of the URL (ie, after the version tag) should be ignored. For the above example, +
+
`identity: "https://spec.example.com/a/b/c/exampleFeature"`
+
A global identifier for the feature. Processors can treat this as an opaque string identifying the feature (but not the version of the feature) for purposes of selecting an appropriate implementation. The identity never has a trailing `/`.
+
`name: "exampleFeature"`
+
The feature's name, for purposes of [prefixing](#sec-Prefixing) schema elements it defines.
+
`version: "v1.0"`
+
The tag for the [version](#sec-Versioning) of the feature used to author the document. Processors MUST select an implementation of the feature which can [satisfy](#sec-Satisfaction) the specified version.
+
+ +The version tag MUST be a valid {VersionTag}. The name MUST be a valid [GraphQL name](https://spec.graphql.org/draft/#Name) which does not include the namespace separator ({"__"}). + +#### Ignore meaningless URL components + +When extracting the URL's `name` and `version`, processors MUST ignore any url components which are not assigned a meaning. This spec assigns meaning to the final two segments of the [path](https://tools.ietf.org/html/rfc3986#section-3.3). Other URL components—particularly query strings and fragments, if present—MUST be ignored for the purposes of extracting the `name` and `version`. + +```html diagram -- Ignoring meaningless parts of a URL + + https://example.com/exampleSpec/v1.0/?key=val&k2=v2#frag + +``` + +#### Why is versioning in the URL, not a directive argument? + +The version is in the URL because when a human reader visits the URL, we would like them to be taken to the documentation for the *version of the feature used by this document*. Many text editors will turn URLs into hyperlinks, and it's highly desirable that clicking the link takes the user to the correct version of the docs. Putting the version information in a separate argument to the {@core} directive would prevent this. + +###! as: String + +Change the [names](#sec-Prefixing) of directives and schema elements from this specification. The specified string MUST be a valid [GraphQL name](https://spec.graphql.org/draft/#Name) and MUST NOT contain the namespace separator (two underscores, {"__"}) or end with an underscore. + +When [`as:`](#@core/as) is provided, processors looking for [prefixed schema elements](#sec-Elements-which-must-be-prefixed) MUST look for elements whose names are the specified name with the prefix replaced with the name provided to the `as:` argument. + +```graphql example -- Using {@core}(feature:, as:) to use a feature with a custom name +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://spec.example.com/example/v1.0", as: "eg") +{ + query: Query +} + +type User { + # Specifying `as: "eg"` transforms @example into @eg + name: String @eg(data: ITEM) +} + +# Additional specified schema elements must have their prefixes set +# to the new name. +# +# The spec at https://spec.example.com/example/v1.0 calls this enum +# `example__Data`, but because of the `as:` argument above, processors +# will use this `eg__Data` enum instead. +enum eg__Data { + ITEM +} + +# Name transformation must also be applied to definitions pulled in from +# specifications. +directive @eg(data: eg__Data) on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA +``` + +###! for: core__Purpose + +An optional [purpose](#core__Purpose) for this feature. This hints to consumers as to whether they can safely ignore metadata from a given feature. + +By default, core features SHOULD fail open. This means that an unknown feature SHOULD NOT prevent a schema from being served or processed. Instead, consumers SHOULD ignore unknown feature metadata and serve or process the rest of the schema normally. + +This behavior is different for features with a specified purpose: + - [`SECURITY`](#core__Purpose/SECURITY) features convey metadata necessary to securely resolve fields within the schema + - [`EXECUTION`](#core__Purpose/EXECUTION) features convey metadata necessary to correctly resolve fields within the schema + +# Enums + +##! core__Purpose + +```graphql definition +enum core__Purpose { + SECURITY + EXECUTION +} +``` + +The role of a feature referenced with {@core}. + +This is not intended to be an exhaustive list of all the purposes a feature might serve. Rather, it is intended to capture cases where the default fail-open behavior of core schema consumers is undesirable. + +Note we'll refer to directives from features which are `for: SECURITY` or `for: EXECUTION` as "`SECURITY` directives" and "`EXECUTION` directives", respectively. + +###! SECURITY + +`SECURITY` features provide metadata necessary to securely resolve fields. For instance, a hypothetical {auth} feature may provide an {@auth} directive to flag fields which require authorization. If a data core does not support the {auth} feature and serves those fields anyway, these fields will be accessible without authorization, compromising security. + +Security-conscious consumers MUST NOT serve a field if: + - the schema definition has **any** unsupported SECURITY directives, + - the field's parent type definition has **any** unsupported SECURITY directives, + - the field's return type definition has **any** unsupported SECURITY directives, or + - the field definition has **any** unsupported SECURITY directives + +Such fields are *not securely resolvable*. Security-conscious consumers MAY serve schemas with fields which are not securely resolvable. However, they MUST remove such fields from the schema before serving it. + +Less security-conscious consumers MAY choose to relax these requirements. For instance, servers may provide a development mode in which unknown SECURITY directives are ignored, perhaps with a warning. Such software may also provide a way to explicitly disable some or all SECURITY features during development. + +More security-conscious consumers MAY choose to enhance these requirements. For instance, production servers MAY adopt a policy of entirely rejecting any schema which contains ANY unsupported SECURITY features, even if those features are never used to annotate the schema. + +###! EXECUTION + +`EXECUTION` features provide metadata necessary to correctly resolve fields. For instance, a hypothetical {ts} feature may provide a `@ts__resolvers` annotation which references a TypeScript module of field resolvers. A consumer which does not support the {ts} feature will be unable to correctly resolve such fields. + +Consumers MUST NOT serve a field if: + - the schema's definition has **any** unsupported EXECUTION directives, + - the field's parent type definition has **any** unsupported EXECUTION directives, + - the field's return type definition has **any** unsupported EXECUTION directives, or + - the field definition has **any** unsupported EXECUTION directives + +Such fields are *unresolvable*. Consumers MAY attempt to serve schemas with unresolvable fields. Depending on the needs of the consumer, unresolvable fields MAY be removed from the schema prior to serving, or they MAY produce runtime errors if a query attempts to resolve them. Consumers MAY implement stricter policies, wholly refusing to serve schemas with unresolvable fields, or even refusing to serve schemas with any unsupported EXECUTION features, even if those features are never used in the schema. + +# Prefixing + +With the exception of a single root directive, core feature specifications MUST prefix all schema elements they introduce. The prefix: + 1. MUST match the name of the feature as derived from the feature's specification URL, + 2. MUST be a valid [GraphQL name](https://spec.graphql.org/draft/#Name), and + 3. MUST NOT contain the core namespace separator, which is two underscores ({"__"}), and + 4. MUST NOT end with an underscore (which would create ambiguity between whether {"x___y"} is prefix `x_` for element `y` or prefix `x` for element `_y`). + +Prefixed names consist of the name of the feature, followed by two underscores, followed by the name of the element, which can be any valid [GraphQL name](https://spec.graphql.org/draft/#Name). For instance, the `core` specification (which you are currently reading) introduces an element named [{@core}](#@core), and the `join` specification introduces an element named {@join__field} (among others). + +Note that both parts must be valid GraphQL names, and GraphQL names cannot start with digits, so core feature specifications cannot introduce names like `@feature__24hours`. + +A feature's *root directive* is an exception to the prefixing requirements. Feature specifications MAY introduce a single directive which carries only the name of the feature, with no prefix required. For example, the `core` specification introduces a {@core} directive. This directive has the same name as the feature ("`core`"), and so requires no prefix. + +```graphql example -- Using the @core directive without changing the prefix +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://spec.example.com/example/v1.0") { + query: Query +} + +type User { + name: String @example(data: ITEM) +} + +# An enum used to provide structured data to the example spec. +# It is prefixed with the name of the spec. +enum example__Data { + ITEM +} + +directive @example(data: example__Data) on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA +``` + +The prefix MUST NOT be elided within documentation; definitions of schema elements provided within the spec MUST include the feature's name as a prefix. + +## Elements which must be prefixed + +Feature specs MUST prefix the following schema elements: + - the names of any object types, interfaces, unions, enums, or input object types defined by the feature + - the names of any directives introduced in the spec, with the exception of the *root directive*, which must have the same name as the feature + +:::[example](prefixing.graphql) -- Prefixing + +# Versioning + +VersionTag : "v" Version + +Version : Major "." Minor + +Major : NumericIdentifier + +Minor : NumericIdentifier + +NumericIdentifier : "0" + | PositiveDigit Digit* + +Digit : "0" | PositiveDigit + +PositiveDigit : "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + +Specs are versioned with a **subset** of a [Semantic Version Number](https://semver.org/spec/v2.0.0.html) containing only the major and minor parts. Thus, specifications SHOULD provide a version of the form `v`{Major}`.`{Minor}, where both integers >= 0. + +```text example -- Valid version tags +v2.2 +v1.0 +v1.1 +v0.1 +``` + +As specified by semver, spec authors SHOULD increment the: + +{++ + +- MAJOR version when you make incompatible API changes, +- MINOR version when you add functionality in a backwards compatible manner + +++} + +Patch and pre-release qualifiers are judged to be not particularly meaningful in the context of core features, which are (by definition) interfaces rather than implementations. The patch component of a semver denotes a bug fix which is backwards compatible—that is, a change to the implementation which does not affect the interface. Patch-level changes in the version of a spec denote wording clarifications which do not require implementation changes. As such, it is not important to track them for the purposes of version resolution. + +As with [semver](https://semver.org/spec/v2.0.0.html), the `0.x` version series is special: there is no expectation of compatibility between versions `0.x` and `0.y`. For example, a processor must not activate implementation `0.4` to satisfy a requested version of `0.2`. + +## Satisfaction + +Given a version {requested} by a document and an {available} version of an implementation, the following algorithm will determine if the {available} version can satisfy the {requested} version: + +Satisfies(requested, available) : + 1. If {requested}.{Major} ≠ {available}.{Major}, return {false} + 2. If {requested}.{Major} = 0, return {requested}.{Minor} = {available}.{Minor} + 3. Return {requested}.{Minor} <= {available}.{Minor} + +## Referencing versions and activating implementations + +Schema documents MUST reference a feature version which supports all the schema elements and behaviors required by the document. As a practical matter, authors will generally prefer to reference a version they have reason to believe is supported by the most processors; depending on context, this might be an old stable version with a low major version, or a new less-deprecated version with a large major version. + +If a processor chooses to activate support for a feature, the processor MUST activate an implementation which can [satisfy](#sec-Satisfaction) the version required by the document. + + +# Processing Schemas + +```mermaid diagram +graph LR + schema(["📄 Input Schema"]):::file-->proc("🤖  Processor") + proc-->output(["📄 Output Schema"]):::file + classDef file fill:none,color:#22262E; + style proc fill:none,stroke:fuchsia,color:fuchsia; +``` + +A common use case is that of a processor which consumes a valid input schema and generates an output schema. + +The general guidance for processor behavior is: don't react to what you don't understand. + +Specifically, processors: + - SHOULD pass through {@core} directives which reference unknown feature URLs + - SHOULD pass through prefixed directives, types, and other schema elements + - SHOULD pass through directives which are not [associated with](#AssignFeatures) a {@core} feature + +Processors MAY accept configuration which overrides these default behaviors. + +Additionally, processors which prepare the schema for final public consumption MAY choose to eliminate all unknown directives and prefixed types in order to hide schema implementation details within the published schema. This will impair the operation of tooling which relies on these directives—such tools will not be able to run on the output schema, so the benefits and costs of this kind of information hiding should be weighed carefully on a case-by-case basis. + +# Validations & Algorithms + +This section lays out algorithms for processing core schemas. + +Algorithms described in this section may produce *validation failures* if a document does not conform to the requirements core schema document. Validation failures SHOULD halt processing. Some consumers, such as authoring tools, MAY attempt to continue processing in the presence of validation failures, but their behavior in such cases is unspecified. + +## Bootstrapping + +Determine the name of the core specification within the document. + +It is possible to [rename the core feature](#sec-Renaming-core-itself) within a document. This process determines the actual name for the core feature if one is present. + +- **Fails** the *Has Schema* validation if there are no SchemaDefinitions in the document +- **Fails** the *Has Core Feature* validation if the `core` feature itself is not referenced with a {@core} directive within the document +- **Fails** the *Bootstrap Core Feature Listed First* validation if the reference to the `core` feature is not the first {@core} directive on the document's SchemaDefinition +- **Fails** the *Core Directive Incorrect Definition* validation if the {@core} directive definition does not *match* the directive as defined by this specification. + +For the purposes of this algorithm, a directive's definition in a schema *matches* a definition provided in this specification if: +- Its arguments have the specified names, types, and default values (or lack thereof) +- It is defined as `repeatable` if and only if the specification's definition defines it as `repeatable` +- The set of locations it belongs to is the same set of locations in the specification's definition. + +The following aspects may differ between the definition in the schema and the definition in the specification without preventing the definitions from *matching*: +- The name of the directive (due to [prefixing](#sec-Prefixing)) +- The order of arguments +- The order of locations +- The directive's description string +- Argument description strings +- Directives applied to argument definitions + +Bootstrap(document) : +1. Let {schema} be the only SchemaDefinition in {document}. (Note that legal GraphQL documents [must include at most one SchemaDefinition](http://spec.graphql.org/draft/#sec-Root-Operation-Types).) + 1. ...if no SchemaDefinitions are present in {document}, the ***Has Schema* validation fails**. +1. For each directive {d} on {schema}, + 1. If {d} has a [`feature:`](#@core/feature) argument which [parses as a feature URL](#@core/feature), *and* whose identity is {"https://specs.apollo.dev/core/"} *and* whose version is {"v0.1"}, *and either* {d} has an [`as:`](#@core/as) argument whose value is equal to {d}'s name *or* {d} does not have an [`as:`](#@core/as) argument and {d}'s name is `core`: + - If any directive on {schema} listed before {d} has the same name as {d}, the ***Bootstrap Core Feature Listed First* validation fails**. + - If the definition of the directive {d} does not *match* the [definition of {@core} in this specification](#@core), the ***Core Directive Incorrect Definition* validation fails**. + - Otherwise, **Return** {d}'s name. +- If no matching directive was found, the ***Has Core Feature* validation fails**. + +## Feature Collection + +Collect a map of ({featureName}: `String`) -> `Directive`, where `Directive` is a {@core} Directive which introduces the feature named {featureName} into the document. + +- **Fails** the *Name Uniqueness* validation if feature names are not unique within the document. +- **Fails** *Invalid Feature URL* validation for any invalid feature URLs. + +CollectFeatures(document) : + - Let {coreName} be the name of the core feature found via {Bootstrap(document)} + - Let {features} be a map of {featureName}: `String` -> `Directive`, initially empty. + - For each directive {d} named {coreName} on the SchemaDefinition within {document}, + - Let {specifiedFeatureName} and {version} be the result of parsing {d}'s `feature:` argument according to the [specified rules for feature URLs](#@core/feature) + - If the `feature:` is not present or fails to parse: + - The ***Invalid Feature URL* validation fails** for {d}, + - Let {featureName} be the {d}'s [`as:`](#@core/as) argument or, if the argument is not present, {specifiedFeatureName} + - If {featureName} exists within {features}, the ***Name Uniqueness* validation fails**. + - Insert {featureName} => {d} into {features} + - **Return** {features} + + +Prefixes, whether implicit or explicit, must be unique within a document. Valid: + +:::[example](prefixing.graphql#schema[0]) -- Unique prefixes + +It is also valid to reference multiple versions of the same spec under different prefixes: + +:::[example](prefix-uniqueness.graphql#schema[0]) -- Explicit prefixes allow multiple versions of the same spec to coexist within a Document + +Without the explicit [`as:`](#@core/as), the above would be invalid: + +:::[counter-example](prefix-uniqueness.graphql#schema[1]) -- Non-unique prefixes with multiple versions of the same spec + +Different specs with the same prefix are also invalid: + +:::[counter-example](prefix-uniqueness.graphql#schema[2]) -- Different specs with non-unique prefixes + +## Assign Features + +Create a map of {element}: *Any Named Element* -> {feature}: `Directive` | {null}, associating every named schema element within the document with a feature directive, or {null} if it is not associated with a feature. + +AssignFeatures(document) : + - Let {features} be the result of collecting features via {CollectFeatures(document)} + - Let {assignments} be a map of ({element}: *Any Named Element*) -> {feature}: `Directive` | {null}, initally empty + - For each named schema element {e} within the {document} + - Let {name} be the name of the {e} + - If {e} is a Directive and {name} is a key within {features}, + - Insert {e} => {features}`[`{name}`]` into {assignments} + - **Continue** to next {e} + - If {name} begins with {"__"}, + - Insert {e} => {null} into {assignments} + - **Continue** to next {e} + - If {name} contains the substring {"__"}, + - Partition {name} into `[`{prefix}, {base}`]` at the first {"__"} (that is, find the shortest {prefix} and longest {base} such that {name} = {prefix} + {"__"} + {base}) + - If {prefix} exists within {features}, insert {e} => {features}`[`{prefix}`]` into {assignments} + - Else, insert {e} => {null} into {assignments} + - **Continue** to next {e} + - Insert {e} => {null} into {assignments} + - **Return** {assignments} + +## Is In API? + +Determine if any schema element is included in the [API](#sec-Parts-of-a-Core-Schema) described by the core schema. A schema element is any part of a GraphQL document using type system definitions that has a [name](https://spec.graphql.org/draft/#Name). + +IsInAPI(element) : + - Let {assignments} be the result of assigning features to elements via {AssignFeatures(document)} + - If {assignments}`[`{element}`]` is {null}, **Return** {true} + - Else, **Return** {false} + +Note: Later versions of this specification may add other ways to affect the behavior of this algorithm, but those mechanisms will only be enabled if you reference those hypothetical versions of this specification. + +## Is Affected By Feature? + +Determine if a schema element is *affected* by a given feature. + +IsAffected(element, feature): + - Let {assignments} be the result of assigning features to elements via {AssignFeatures(document)} + - For each directive {d} on {element}, If {assignments}`[`{d}`]` is {feature}, **Return** {true} + - If {element} is a FieldDefinition, + - Let {parent} be the parent ObjectDefinition or InterfaceDefinition for {element} + - If {IsAffected(parent, feature)}, **Return** {true} + - For each argument type {a} declared on {element}, + - Let {t} be the InputDefinition, EnumDefinition, or ScalarDefinition for argument {a} + - If {IsAffected(t, feature)}, **Return** {true} + - Let {return} be the ObjectDefinition, InterfaceDefinition, or UnionDefinition for {element}'s return type + - If {IsAffected(return, feature)}, **Return** {true} + - If {element} is an InputDefinition, + - For each InputFieldDefinition {field} within {element}, + - Let {t} be the InputDefinition, EnumDefinition, or ScalarDefinition for the type of {field} + - If {IsAffected(t, feature)}, **Return** {true} + - If {element} is an EnumDefinition, + - For each EnumValueDefinition {value} in {element}, + - If {IsAffected(value, feature)}, **Return** {true} + + + diff --git a/core/v0.2/good-unique-prefix-multi.graphql b/core/v0.2/good-unique-prefix-multi.graphql new file mode 100644 index 0000000..8eafe08 --- /dev/null +++ b/core/v0.2/good-unique-prefix-multi.graphql @@ -0,0 +1,9 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://specs.example.com/A/2.0", as: "A2") # name is A2 +{ + query: Query +} + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.2/index.html b/core/v0.2/index.html new file mode 100644 index 0000000..b981165 --- /dev/null +++ b/core/v0.2/index.html @@ -0,0 +1,1829 @@ + + + + +Core Schemas + + + + + +
+
+

Core Schemas

+
+

flexible metadata for GraphQL schemas

+ + + +
StatusRelease
Version0.2
+ + +

GraphQL provides directives as a means of attaching user‐defined metadata to a GraphQL document. Directives are highly flexible, and can be used to suggest behavior and define features of a graph which are not otherwise evident in the schema.

+

Alas, GraphQL does not provide a mechanism to globally identify or version directives. Given a particular directive—e.g. @join—processors are expected to know how to interpret the directive based only on its name, definition within the document, and additional configuration from outside the document. This means that programs interpreting these directives have two options:

+
    +
  1. rely on a hardcoded interpretation for directives with certain signatures, or
  2. +
  3. accept additional configuration about how to interpret directives in the schema.
  4. +
+

The first solution is fragile, particularly as GraphQL has no built‐in namespacing mechanisms, so the possibility of name collisions always looms.

+

The second is unfortunate: GraphQL schemas are generally intended to be self‐describing, and requiring additional configuration subtly undermines this guarantee: given just a schema, programs do not necessarily know how to interpret it, and certainly not how to serve it. It also creates the possibility for the schema and configuration to fall out of sync, leading to issues which can manifest late in a deployment pipeline.

+

Introducing core schemas.

+
+ + + + core schema + +

A basic core schema:

+
Example № 1 A basic core schema
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/example/v1.0")
+{
+  query: Query
+}
+
+type Query {
+  field: Int @example
+}
+
+directive @example on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+

Core schemas provide a concise mechanism for schema documents to specify the metadata they provide. Metadata is grouped into features, which typically define directives and associated types (e.g. scalars and inputs which serve as directive inputs). Additionally, core schemas provide:

+ +

Core schemas are not a new language. All core schema documents are valid GraphQL schema documents. However, this specification introduces new requirements, so not all valid GraphQL schemas are valid core schemas.

+

The broad intention behind core schemas is to provide a single document which provides all the necessary configuration for programs that process and serve the schema to GraphQL clients, primarily by following directives in order to determine how to resolve queries made against that schema.

+
+ +
+
+

1Parts of a Core Schema

+

When talking about a core schema, we can broadly break it into two pieces:

+ +

This reflects how core schemas are used: a core schema contains a GraphQL interface (the API) along with metadata about how to implement that interface (the machinery). Exposing the machinery to clients is unnecessary, and may in some cases constitute a security issue (for example, the machinery for a public‐facing graph router will likely reference internal services, possibly exposing network internals which should not be visible to the general public).

+

A key feature of core schemas is that it is always possible to derive a core schema’s API without any knowledge of the features used by the document (with the exception of the core feature itself). Specifically, named elements are not included in the API schema if they are named something__likeThis or are a directive named @something, and something is the prefix of a feature declared with @core.

+

A formal description is provided by the IsInAPI algorithm.

+
+
+

2Actors

+
Actors who may be interested in the core schemas
graph TB + classDef bg fill:none,color:#22262E; + author("👩🏽‍💻 🤖  Author"):::bg-->schema(["☉ Core Schema"]):::bg + schema-->proc1("🤖  Processor"):::bg + proc1-->output1(["☉ Core Schema[0]"]):::bg + output1-->proc2("🤖  Processor"):::bg + proc2-->output2(["☉ Core Schema[1]"]):::bg + output2-->etc("..."):::bg + etc-->final(["☉ Core Schema [final]"]):::bg + final-->core("🤖 Data Core"):::bg + schema-->reader("👩🏽‍💻 Reader"):::bg + output1-->reader + output2-->reader + final-->reader +
+
+
+

3Basic Requirements

+

Core schemas:

+
    +
  1. MUST be valid GraphQL schema documents,
  2. +
  3. MUST contain exactly one SchemaDefinition, and
  4. +
  5. MUST use the @core directive on their schema definition to declare any features they reference by using @core to reference a well‐formed feature URL.
  6. +
+

The first @core directive on the schema MUST reference the core spec itself, i.e. this document.

+
Example № 2 Basic core schema using @core and @example
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/example/v1.0")
+{
+  query: Query
+}
+
+type Query {
+  field: Int @example
+}
+
+directive @example on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+
+

3.1Unspecified directives are passed through by default

+

Existing schemas likely contain definitions for directives which are not versioned, have no specification document, and are intended mainly to be passed through. This is the default behavior for core schema processors:

+
Example № 3 Unspecified directives are passed through
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+{
+  query: Query
+}
+
+type SomeType {
+  field: Int @another
+}
+
+# `@another` is unspecified. Core processors will not extract metadata from
+# it, but its definition and all usages within the schema will be exposed
+# in the API.
+directive @another on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+
+
+

3.2Renaming core itself

+

It is possible to rename the core feature itself with the same as: mechanism used for all features:

+
Example № 4 Renaming @core to @coreSchema
schema
+  @coreSchema(feature: "https://specs.apollo.dev/core/v0.1", as: "coreSchema")
+  @coreSchema(feature: "https://example.com/example/v1.0")
+{
+  query: Query
+}
+
+type SomeType {
+  field: Int @example
+}
+
+directive @coreSchema(feature: String!, as: String)
+  repeatable on SCHEMA
+directive @example on FIELD_DEFINITION
+
+
+
+
+

4Directive Definitions

+

All core schemas use the @core directive to declare their use of the core feature itself as well as any other core features they use.

+

In order to use these directives in your schema, GraphQL requires you to include their definitions in your schema.

+

Processors MUST validate that you have defined the directives with the same arguments, locations, and repeatable flag as given below. Specifically, the bootstrapping algorithm validates that the @core directive has a definition matching the definition given below. (The bootstrapping algorithm does not require processors to validate other aspects of the directive declaration such as description strings or argument ordering. The main purpose of this validation is to ensure that directive arguments have the type and default values expected by the specification.)

+

The following declares the directive defined by this specification. You SHOULD define the directives in your core schema by including the following text in your schema document.

+
directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+

When writing a specification for your own core feature, you SHOULD include a section like this one with sample definitions to copy into schemas, and you SHOULD require processors to validate that directive definitions in documents match your sample definitions.

+
+
+

5Directives

+
+

5.1@core

+

Declare a core feature present in this schema.

+
directive @core(
+  feature: String!,
+  as: String,
+  for: core__Purpose)
+  repeatable on SCHEMA
+
+

Documents MUST include a definition for the @core directive which includes all of the arguments defined above with the same types and default values.

+
+

5.1.1feature: String!

+

A feature URL specifying the directive and associated schema elements. When viewed, the URL SHOULD provide the content of the appropriate version of the specification in some human‐readable form. In short, a human reader should be able to click the link and go to the docs for the version in use. There are specific requirements on the format of the URL, but it is not required that the content be machine‐readable in any particular way.

+

Feature URLs contain information about the spec’s prefix and version.

+

Feature URLs serve two main purposes:

+
    +
  • Directing human readers to documentation about the feature
  • +
  • Providing tools with information about the specs in use, along with enough information to select and invoke an implementation
  • +
+

Feature URLs SHOULD be RFC 3986 URLs. When viewed, the URL SHOULD provide the specification of the selected version of the feature in some human‐readable form; a human reader should be able to click the link and go to the correct version of the docs.

+

Although they are not prohibited from doing so, it’s assumed that processors will not load the content of feature URLs. Published specifications are not required to be machine‐readable, and this spec places no requirements on the structure or syntax of the content to be found there.

+

There are, however, requirements on the structure of the URL itself:

+
Basic anatomy of a feature URL
+ https://spec.example.com/a/b/c/exampleFeature/v1.0 + +

The final two segments of the URL’s path MUST contain the feature’s name and a version tag. The content of the URL up to and including the name—but excluding the / after the name and the version tag—is the feature’s identity. Trailing slashes at the end of the URL (ie, after the version tag) should be ignored. For the above example,

identity: "https://spec.example.com/a/b/c/exampleFeature"
A global identifier for the feature. Processors can treat this as an opaque string identifying the feature (but not the version of the feature) for purposes of selecting an appropriate implementation. The identity never has a trailing /.
name: "exampleFeature"
The feature’s name, for purposes of prefixing schema elements it defines.
version: "v1.0"
The tag for the version of the feature used to author the document. Processors MUST select an implementation of the feature which can satisfy the specified version.

+

The version tag MUST be a valid VersionTag. The name MUST be a valid GraphQL name which does not include the namespace separator ("__").

+
+

5.1.1.1Ignore meaningless URL components

+

When extracting the URL’s name and version, processors MUST ignore any url components which are not assigned a meaning. This spec assigns meaning to the final two segments of the path. Other URL components—particularly query strings and fragments, if present—MUST be ignored for the purposes of extracting the name and version.

+
Ignoring meaningless parts of a URL
+ https://example.com/exampleSpec/v1.0/?key=val&k2=v2#frag + +
+
+

5.1.1.2Why is versioning in the URL, not a directive argument?

+

The version is in the URL because when a human reader visits the URL, we would like them to be taken to the documentation for the version of the feature used by this document. Many text editors will turn URLs into hyperlinks, and it’s highly desirable that clicking the link takes the user to the correct version of the docs. Putting the version information in a separate argument to the @core directive would prevent this.

+
+
+
+

5.1.2as: String

+

Change the names of directives and schema elements from this specification. The specified string MUST be a valid GraphQL name and MUST NOT contain the namespace separator (two underscores, "__") or end with an underscore.

+

When as: is provided, processors looking for prefixed schema elements MUST look for elements whose names are the specified name with the prefix replaced with the name provided to the as: argument.

+
Example № 5 Using @core(feature:, as:) to use a feature with a custom name
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://spec.example.com/example/v1.0", as: "eg")
+{
+  query: Query
+}
+
+type User {
+  # Specifying `as: "eg"` transforms @example into @eg
+  name: String @eg(data: ITEM)
+}
+
+# Additional specified schema elements must have their prefixes set
+# to the new name.
+#
+# The spec at https://spec.example.com/example/v1.0 calls this enum
+# `example__Data`, but because of the `as:` argument above, processors
+# will use this `eg__Data` enum instead.
+enum eg__Data {
+  ITEM
+}
+
+# Name transformation must also be applied to definitions pulled in from
+# specifications.
+directive @eg(data: eg__Data) on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+
+
+

5.1.3for: core__Purpose

+

An optional purpose for this feature. This hints to consumers as to whether they can safely ignore metadata from a given feature.

+

By default, core features SHOULD fail open. This means that an unknown feature SHOULD NOT prevent a schema from being served or processed. Instead, consumers SHOULD ignore unknown feature metadata and serve or process the rest of the schema normally.

+

This behavior is different for features with a specified purpose:

+
    +
  • SECURITY features convey metadata necessary to securely resolve fields within the schema
  • +
  • EXECUTION features convey metadata necessary to correctly resolve fields within the schema
  • +
+
+
+
+
+

6Enums

+
+

6.1core__Purpose

+
enum core__Purpose {
+  SECURITY
+  EXECUTION
+}
+
+

The role of a feature referenced with @core.

+

This is not intended to be an exhaustive list of all the purposes a feature might serve. Rather, it is intended to capture cases where the default fail‐open behavior of core schema consumers is undesirable.

+
+Note +we’ll refer to directives from features which are for: SECURITY or for: EXECUTION as “SECURITY directives” and “EXECUTION directives”, respectively.
+
+

6.1.1SECURITY

+

SECURITY features provide metadata necessary to securely resolve fields. For instance, a hypothetical auth feature may provide an @auth directive to flag fields which require authorization. If a data core does not support the auth feature and serves those fields anyway, these fields will be accessible without authorization, compromising security.

+

Security‐conscious consumers MUST NOT serve a field if:

+
    +
  • the schema definition has any unsupported SECURITY directives,
  • +
  • the field’s parent type definition has any unsupported SECURITY directives,
  • +
  • the field’s return type definition has any unsupported SECURITY directives, or
  • +
  • the field definition has any unsupported SECURITY directives
  • +
+

Such fields are not securely resolvable. Security‐conscious consumers MAY serve schemas with fields which are not securely resolvable. However, they MUST remove such fields from the schema before serving it.

+

Less security‐conscious consumers MAY choose to relax these requirements. For instance, servers may provide a development mode in which unknown SECURITY directives are ignored, perhaps with a warning. Such software may also provide a way to explicitly disable some or all SECURITY features during development.

+

More security‐conscious consumers MAY choose to enhance these requirements. For instance, production servers MAY adopt a policy of entirely rejecting any schema which contains ANY unsupported SECURITY features, even if those features are never used to annotate the schema.

+
+
+

6.1.2EXECUTION

+

EXECUTION features provide metadata necessary to correctly resolve fields. For instance, a hypothetical ts feature may provide a @ts__resolvers annotation which references a TypeScript module of field resolvers. A consumer which does not support the ts feature will be unable to correctly resolve such fields.

+

Consumers MUST NOT serve a field if:

+
    +
  • the schema’s definition has any unsupported EXECUTION directives,
  • +
  • the field’s parent type definition has any unsupported EXECUTION directives,
  • +
  • the field’s return type definition has any unsupported EXECUTION directives, or
  • +
  • the field definition has any unsupported EXECUTION directives
  • +
+

Such fields are unresolvable. Consumers MAY attempt to serve schemas with unresolvable fields. Depending on the needs of the consumer, unresolvable fields MAY be removed from the schema prior to serving, or they MAY produce runtime errors if a query attempts to resolve them. Consumers MAY implement stricter policies, wholly refusing to serve schemas with unresolvable fields, or even refusing to serve schemas with any unsupported EXECUTION features, even if those features are never used in the schema.

+
+
+
+
+

7Prefixing

+

With the exception of a single root directive, core feature specifications MUST prefix all schema elements they introduce. The prefix:

+
    +
  1. MUST match the name of the feature as derived from the feature’s specification URL,
  2. +
  3. MUST be a valid GraphQL name, and
  4. +
  5. MUST NOT contain the core namespace separator, which is two underscores ("__"), and
  6. +
  7. MUST NOT end with an underscore (which would create ambiguity between whether "x___y" is prefix x_ for element y or prefix x for element _y).
  8. +
+

Prefixed names consist of the name of the feature, followed by two underscores, followed by the name of the element, which can be any valid GraphQL name. For instance, the core specification (which you are currently reading) introduces an element named @core, and the join specification introduces an element named @join__field (among others).

+
+Note +that both parts must be valid GraphQL names, and GraphQL names cannot start with digits, so core feature specifications cannot introduce names like @feature__24hours.
+

A feature’s root directive is an exception to the prefixing requirements. Feature specifications MAY introduce a single directive which carries only the name of the feature, with no prefix required. For example, the core specification introduces a @core directive. This directive has the same name as the feature (”core”), and so requires no prefix.

+
Example № 6 Using the @core directive without changing the prefix
schema
+ @core(feature: "https://specs.apollo.dev/core/v0.1")
+ @core(feature: "https://spec.example.com/example/v1.0") {
+  query: Query
+}
+
+type User {
+  name: String @example(data: ITEM)
+}
+
+# An enum used to provide structured data to the example spec.
+# It is prefixed with the name of the spec.
+enum example__Data {
+  ITEM
+}
+
+directive @example(data: example__Data) on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+

The prefix MUST NOT be elided within documentation; definitions of schema elements provided within the spec MUST include the feature’s name as a prefix.

+
+

7.1Elements which must be prefixed

+

Feature specs MUST prefix the following schema elements:

+
    +
  • the names of any object types, interfaces, unions, enums, or input object types defined by the feature
  • +
  • the names of any directives introduced in the spec, with the exception of the root directive, which must have the same name as the feature
  • +
+
Example № 7 Prefixing
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://spec.example.com/featureA/v1.0")
+  @core(feature: "https://spec.example.com/featureB/v2.0", as: "B") {
+  query: Query
+}
+
+"""
+featureA__SomeType is a type defined by feature A.
+"""
+type featureA__SomeType {
+  """
+  nativeField is a field defined by featureA on a type also defined
+  by featureA (namely featureA__SomeType)
+  """
+  nativeField: Int @featureA__fieldDirective
+}
+
+"""
+featureA__SomeInput is an input specified by feature A
+"""
+input featureA__SomeInput {
+  """
+  nativeInputField is defined by featureA
+  """
+  nativeInputField: Int
+}
+
+"""
+featureA__Items is specified by feature A
+"""
+enum featureA__Items { ONE, TWO, THREE @B }
+
+"""
+@B is the root directive defined by featureB
+
+Root directives are named after their feature
+"""
+directive @B on ENUM_VALUE
+
+"""
+@featureA__fieldDirective is a non-root (prefixed) directive defined by featureA
+"""
+directive @featureA__fieldDirective on FIELD_DEFINITION
+
+directive @core(feature: String!, as: String) repeatable on SCHEMA
+
+
+
+
+

8Versioning

+ + + + + + +
+PositiveDigit
1|2|3|4|5|6|7|8|9
+
+

Specs are versioned with a subset of a Semantic Version Number containing only the major and minor parts. Thus, specifications SHOULD provide a version of the form vMajor.Minor, where both integers ≥ 0.

+
Example № 8 Valid version tags
v2.2
+v1.0
+v1.1
+v0.1
+
+

As specified by semver, spec authors SHOULD increment the:

+
    +
  • MAJOR version when you make incompatible API changes,
  • +
  • MINOR version when you add functionality in a backwards compatible manner
  • +
+
+

Patch and pre‐release qualifiers are judged to be not particularly meaningful in the context of core features, which are (by definition) interfaces rather than implementations. The patch component of a semver denotes a bug fix which is backwards compatible—that is, a change to the implementation which does not affect the interface. Patch‐level changes in the version of a spec denote wording clarifications which do not require implementation changes. As such, it is not important to track them for the purposes of version resolution.

+

As with semver, the 0.x version series is special: there is no expectation of compatibility between versions 0.x and 0.y. For example, a processor must not activate implementation 0.4 to satisfy a requested version of 0.2.

+
+

8.1Satisfaction

+

Given a version requested by a document and an available version of an implementation, the following algorithm will determine if the available version can satisfy the requested version:

+
+Satisfies(requested, available)
    +
  1. If requested.Majoravailable.Major, return false
  2. +
  3. If requested.Major = 0, return requested.Minor = available.Minor
  4. +
  5. Return requested.Minoravailable.Minor
  6. +
+
+
+
+

8.2Referencing versions and activating implementations

+

Schema documents MUST reference a feature version which supports all the schema elements and behaviors required by the document. As a practical matter, authors will generally prefer to reference a version they have reason to believe is supported by the most processors; depending on context, this might be an old stable version with a low major version, or a new less‐deprecated version with a large major version.

+

If a processor chooses to activate support for a feature, the processor MUST activate an implementation which can satisfy the version required by the document.

+
+
+
+

9Processing Schemas

+
graph LR + schema(["📄 Input Schema"]):::file-->proc("🤖  Processor") + proc-->output(["📄 Output Schema"]):::file + classDef file fill:none,color:#22262E; + style proc fill:none,stroke:fuchsia,color:fuchsia; +

A common use case is that of a processor which consumes a valid input schema and generates an output schema.

+

The general guidance for processor behavior is: don’t react to what you don’t understand.

+

Specifically, processors:

+ +

Processors MAY accept configuration which overrides these default behaviors.

+

Additionally, processors which prepare the schema for final public consumption MAY choose to eliminate all unknown directives and prefixed types in order to hide schema implementation details within the published schema. This will impair the operation of tooling which relies on these directives—such tools will not be able to run on the output schema, so the benefits and costs of this kind of information hiding should be weighed carefully on a case‐by‐case basis.

+
+
+

10Validations & Algorithms

+

This section lays out algorithms for processing core schemas.

+

Algorithms described in this section may produce validation failures if a document does not conform to the requirements core schema document. Validation failures SHOULD halt processing. Some consumers, such as authoring tools, MAY attempt to continue processing in the presence of validation failures, but their behavior in such cases is unspecified.

+
+

10.1Bootstrapping

+

Determine the name of the core specification within the document.

+

It is possible to rename the core feature within a document. This process determines the actual name for the core feature if one is present.

+
    +
  • Fails the Has Schema validation if there are no SchemaDefinitions in the document
  • +
  • Fails the Has Core Feature validation if the core feature itself is not referenced with a @core directive within the document
  • +
  • Fails the Bootstrap Core Feature Listed First validation if the reference to the core feature is not the first @core directive on the document’s SchemaDefinition
  • +
  • Fails the Core Directive Incorrect Definition validation if the @core directive definition does not match the directive as defined by this specification.
  • +
+

For the purposes of this algorithm, a directive’s definition in a schema matches a definition provided in this specification if:

+
    +
  • Its arguments have the specified names, types, and default values (or lack thereof)
  • +
  • It is defined as repeatable if and only if the specification’s definition defines it as repeatable
  • +
  • The set of locations it belongs to is the same set of locations in the specification’s definition.
  • +
+

The following aspects may differ between the definition in the schema and the definition in the specification without preventing the definitions from matching:

+
    +
  • The name of the directive (due to prefixing)
  • +
  • The order of arguments
  • +
  • The order of locations
  • +
  • The directive’s description string
  • +
  • Argument description strings
  • +
  • Directives applied to argument definitions
  • +
+
+Bootstrap(document)
    +
  1. Let schema be the only SchemaDefinition in document. (Note that legal GraphQL documents must include at most one SchemaDefinition.)
      +
    1. ...if no SchemaDefinitions are present in document, the Has Schema validation fails.
    2. +
    +
  2. +
  3. For each directive d on schema,
      +
    1. If d has a feature: argument which parses as a feature URL, and whose identity is "https://specs.apollo.dev/core/" and whose version is "v0.1", and either d has an as: argument whose value is equal to d‘s name or d does not have an as: argument and d‘s name is core:
        +
      1. If any directive on schema listed before d has the same name as d, the Bootstrap Core Feature Listed First validation fails.
      2. +
      3. If the definition of the directive d does not match the definition of @core in this specification, the Core Directive Incorrect Definition validation fails.
      4. +
      5. Otherwise, Return d‘s name.
      6. +
      +
    2. +
    +
  4. +
  5. If no matching directive was found, the Has Core Feature validation fails.
  6. +
+
+
+
+

10.2Feature Collection

+

Collect a map of (featureName: String) → Directive, where Directive is a @core Directive which introduces the feature named featureName into the document.

+
    +
  • Fails the Name Uniqueness validation if feature names are not unique within the document.
  • +
  • Fails Invalid Feature URL validation for any invalid feature URLs.
  • +
+
+CollectFeatures(document)
    +
  1. Let coreName be the name of the core feature found via Bootstrap(document)
  2. +
  3. Let features be a map of featureName: StringDirective, initially empty.
  4. +
  5. For each directive d named coreName on the SchemaDefinition within document,
      +
    1. Let specifiedFeatureName and version be the result of parsing d‘s feature: argument according to the specified rules for feature URLs
    2. +
    3. If the feature: is not present or fails to parse:
        +
      1. The Invalid Feature URL validation fails for d,
      2. +
      +
    4. +
    5. Let featureName be the d‘s as: argument or, if the argument is not present, specifiedFeatureName
    6. +
    7. If featureName exists within features, the Name Uniqueness validation fails.
    8. +
    9. Insert featureNamed into features
    10. +
    +
  6. +
  7. Return features
  8. +
+
+

Prefixes, whether implicit or explicit, must be unique within a document. Valid:

+
Example № 9 Unique prefixes
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://spec.example.com/featureA/v1.0")
+  @core(feature: "https://spec.example.com/featureB/v2.0", as: "B") {
+  query: Query
+}
+

It is also valid to reference multiple versions of the same spec under different prefixes:

+
Example № 10 Explicit prefixes allow multiple versions of the same spec to coexist within a Document
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/A/1.0")               # name is A
+  @core(feature: "https://specs.example.com/A/2.0", as: "A2")     # name is A2
+{
+  query: Query
+}
+

Without the explicit as:, the above would be invalid:

+
Counter Example № 11 Non‐unique prefixes with multiple versions of the same spec
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/A/1.0") # name is A
+  @core(feature: "https://specs.example.com/A/2.0") # name is A
+{
+  query: Query
+}
+

Different specs with the same prefix are also invalid:

+
Counter Example № 12 Different specs with non‐unique prefixes
schema
+  @core(feature: "https://specs.apollo.dev/core/v0.1")
+  @core(feature: "https://specs.example.com/A/1.0")              # name is A
+  @core(feature: "https://www.specs.com/specA/1.1", as: "A")     # name is A
+{
+  query: Query
+}
+
+
+

10.3Assign Features

+

Create a map of element: Any Named Elementfeature: Directive | null, associating every named schema element within the document with a feature directive, or null if it is not associated with a feature.

+
+AssignFeatures(document)
    +
  1. Let features be the result of collecting features via CollectFeatures(document)
  2. +
  3. Let assignments be a map of (element: Any Named Element) → feature: Directive | null, initally empty
  4. +
  5. For each named schema element e within the document
      +
    1. Let name be the name of the e
    2. +
    3. If e is a Directive and name is a key within features,
        +
      1. Insert efeatures[name] into assignments
      2. +
      3. Continue to next e
      4. +
      +
    4. +
    5. If name begins with "__",
        +
      1. Insert enull into assignments
      2. +
      3. Continue to next e
      4. +
      +
    6. +
    7. If name contains the substring "__",
        +
      1. Partition name into [prefix, base] at the first "__" (that is, find the shortest prefix and longest base such that name = prefix + "__" + base)
      2. +
      3. If prefix exists within features, insert efeatures[prefix] into assignments
          +
        1. Else, insert enull into assignments
        2. +
        +
      4. +
      5. Continue to next e
      6. +
      +
    8. +
    9. Insert enull into assignments
    10. +
    +
  6. +
  7. Return assignments
  8. +
+
+
+
+

10.4Is In API?

+

Determine if any schema element is included in the API described by the core schema. A schema element is any part of a GraphQL document using type system definitions that has a name.

+
+IsInAPI(element)
    +
  1. Let assignments be the result of assigning features to elements via AssignFeatures(document)
  2. +
  3. If assignments[element] is null, Return true
  4. +
  5. Else, Return false
  6. +
+
+
+Note +Later versions of this specification may add other ways to affect the behavior of this algorithm, but those mechanisms will only be enabled if you reference those hypothetical versions of this specification.
+
+
+

10.5Is Affected By Feature?

+

Determine if a schema element is affected by a given feature.

+
+IsAffected(element, feature)
    +
  1. Let assignments be the result of assigning features to elements via AssignFeatures(document)
  2. +
  3. For each directive d on element, If assignments[d] is feature, Return true
  4. +
  5. If element is a FieldDefinition,
      +
    1. Let parent be the parent ObjectDefinition or InterfaceDefinition for element
    2. +
    3. If IsAffected(parent, feature), Return true
    4. +
    5. For each argument type a declared on element,
        +
      1. Let t be the InputDefinition, EnumDefinition, or ScalarDefinition for argument a
      2. +
      3. If IsAffected(t, feature), Return true
      4. +
      +
    6. +
    7. Let return be the ObjectDefinition, InterfaceDefinition, or UnionDefinition for element‘s return type
    8. +
    9. If IsAffected(return, feature), Return true
    10. +
    +
  6. +
  7. If element is an InputDefinition,
      +
    1. For each InputFieldDefinition field within element,
        +
      1. Let t be the InputDefinition, EnumDefinition, or ScalarDefinition for the type of field
      2. +
      3. If IsAffected(t, feature), Return true
      4. +
      +
    2. +
    +
  8. +
  9. If element is an EnumDefinition,
      +
    1. For each EnumValueDefinition value in element,
        +
      1. If IsAffected(value, feature), Return true
      2. +
      +
    2. +
    +
  10. +
+
+
+
+

§Index

  1. AssignFeatures
  2. Bootstrap
  3. CollectFeatures
  4. Digit
  5. IsAffected
  6. IsInAPI
  7. Major
  8. Minor
  9. NumericIdentifier
  10. PositiveDigit
  11. Satisfies
  12. Version
  13. VersionTag
+ + +
+
+ +
  1. 1Parts of a Core Schema
  2. +
  3. 2Actors
  4. +
  5. 3Basic Requirements + +
      +
    1. 3.1Unspecified directives are passed through by default
    2. +
    3. 3.2Renaming core itself
    4. +
    +
  6. +
  7. 4Directive Definitions
  8. +
  9. 5Directives + +
      +
    1. 5.1@core + +
        +
      1. 5.1.1feature: String! + +
          +
        1. 5.1.1.1Ignore meaningless URL components
        2. +
        3. 5.1.1.2Why is versioning in the URL, not a directive argument?
        4. +
        +
      2. +
      3. 5.1.2as: String
      4. +
      5. 5.1.3for: core__Purpose
      6. +
      +
    2. +
    +
  10. +
  11. 6Enums + +
      +
    1. 6.1core__Purpose + +
        +
      1. 6.1.1SECURITY
      2. +
      3. 6.1.2EXECUTION
      4. +
      +
    2. +
    +
  12. +
  13. 7Prefixing + +
      +
    1. 7.1Elements which must be prefixed
    2. +
    +
  14. +
  15. 8Versioning + +
      +
    1. 8.1Satisfaction
    2. +
    3. 8.2Referencing versions and activating implementations
    4. +
    +
  16. +
  17. 9Processing Schemas
  18. +
  19. 10Validations & Algorithms + +
      +
    1. 10.1Bootstrapping
    2. +
    3. 10.2Feature Collection
    4. +
    5. 10.3Assign Features
    6. +
    7. 10.4Is In API?
    8. +
    9. 10.5Is Affected By Feature?
    10. +
    +
  20. +
  21. §Index
  22. +
+
+ +
+ + + diff --git a/core/v0.2/netlify.toml b/core/v0.2/netlify.toml new file mode 100644 index 0000000..f1a19b0 --- /dev/null +++ b/core/v0.2/netlify.toml @@ -0,0 +1,6 @@ +# Netlify Admin: https://app.netlify.com/sites/apollo-specs-core/ +# Docs: https://docs.netlify.com/configure-builds/file-based-configuration/ + +[build] + command = "npm run build" + publish = ".dist/" diff --git a/core/v0.2/package.json b/core/v0.2/package.json new file mode 100644 index 0000000..63529c1 --- /dev/null +++ b/core/v0.2/package.json @@ -0,0 +1,20 @@ +{ + "name": "specs-site", + "private": true, + "version": "1.0.0", + "description": "for GraphQL schemas with extensible metadata", + "repository": "https://github.com/apollo-specs/core", + "main": "index.js", + "scripts": { + "build": "rsync -avz --exclude .dist . .dist && spec-md core.spec.md > .dist/index.html", + "dev": "npm run build && chokidar '**/*' -i '.dist' -c 'npm run build'" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@queerviolet/speck": "git://github.com/queerviolet/speck.git#main", + "chokidar-cli": "^2.1.0", + "watch": "^1.0.2" + } +} diff --git a/core/v0.2/prefix-uniqueness.graphql b/core/v0.2/prefix-uniqueness.graphql new file mode 100644 index 0000000..a26a309 --- /dev/null +++ b/core/v0.2/prefix-uniqueness.graphql @@ -0,0 +1,25 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://specs.example.com/A/2.0", as: "A2") # name is A2 +{ + query: Query +} + +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://specs.example.com/A/2.0") # name is A +{ + query: Query +} + +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://specs.example.com/A/1.0") # name is A + @core(feature: "https://www.specs.com/specA/1.1", as: "A") # name is A +{ + query: Query +} + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/core/v0.2/prefixing.graphql b/core/v0.2/prefixing.graphql new file mode 100644 index 0000000..12c5da7 --- /dev/null +++ b/core/v0.2/prefixing.graphql @@ -0,0 +1,46 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v0.1") + @core(feature: "https://spec.example.com/featureA/v1.0") + @core(feature: "https://spec.example.com/featureB/v2.0", as: "B") { + query: Query +} + +""" +featureA__SomeType is a type defined by feature A. +""" +type featureA__SomeType { + """ + nativeField is a field defined by featureA on a type also defined + by featureA (namely featureA__SomeType) + """ + nativeField: Int @featureA__fieldDirective +} + +""" +featureA__SomeInput is an input specified by feature A +""" +input featureA__SomeInput { + """ + nativeInputField is defined by featureA + """ + nativeInputField: Int +} + +""" +featureA__Items is specified by feature A +""" +enum featureA__Items { ONE, TWO, THREE @B } + +""" +@B is the root directive defined by featureB + +Root directives are named after their feature +""" +directive @B on ENUM_VALUE + +""" +@featureA__fieldDirective is a non-root (prefixed) directive defined by featureA +""" +directive @featureA__fieldDirective on FIELD_DEFINITION + +directive @core(feature: String!, as: String) repeatable on SCHEMA diff --git a/index.html b/index.html new file mode 100644 index 0000000..d9f81c0 --- /dev/null +++ b/index.html @@ -0,0 +1,1087 @@ + + + + +Apollo Library of Technical Specifications + + + + + +
+
+

Apollo Library of Technical Specifications

+
+
+
+ +
+
+ + +
+ +
+
+

1Current Specs

+ +
+
+ + +
+
+ +
  1. 1Current Specs
  2. +
+
+ +
+ + + diff --git a/index.md b/index.md new file mode 100644 index 0000000..cf832e0 --- /dev/null +++ b/index.md @@ -0,0 +1,20 @@ +# Apollo Library of Technical Specifications + +```raw html +
+
+ +
+
+ + +``` + +# Current Specs + +- [link v1.0](link/v1.0) provides the foundation of core schemas—GraphQL schemas for the global graph. Core schemas use [specified conventions](link/v1.0) to link definitions from other schemas. All the other specs in this library rely on core schema conventions. +- [join v0.1](join/v0.1) describes supergraphs which join types from one or more subgraphs +- [tag v0.1](tag/v0.1) attaches a single piece of string metadata to various GraphQL definitions (it is mainly used for contracts) +- [inaccessible v0.1](inaccessible/v0.1) masks fields and types from a graph's public API + + diff --git a/join/v0.1/albums.graphql b/join/v0.1/albums.graphql new file mode 100644 index 0000000..e9ff385 --- /dev/null +++ b/join/v0.1/albums.graphql @@ -0,0 +1,14 @@ +type Album @key(fields: "id") { + id: ID! + user: User + photos: [Image!] +} + +extend type Image { + albums: [Album!] +} + +extend type User { + albums: [Album!] + favorite: Album +} \ No newline at end of file diff --git a/join/v0.1/auth.graphql b/join/v0.1/auth.graphql new file mode 100644 index 0000000..7a85d3a --- /dev/null +++ b/join/v0.1/auth.graphql @@ -0,0 +1,8 @@ +type User @key(fields: "id") { + id: ID! + name: String +} + +type Query { + me: User +} \ No newline at end of file diff --git a/join/v0.1/images.graphql b/join/v0.1/images.graphql new file mode 100644 index 0000000..741ec0f --- /dev/null +++ b/join/v0.1/images.graphql @@ -0,0 +1,15 @@ +type Image @key(fields: "url") { + url: Url + type: MimeType +} + +type Query { + images: [Image] +} + +extend type User { + favorite: Image +} + +scalar Url +scalar MimeType \ No newline at end of file diff --git a/join/v0.1/index.html b/join/v0.1/index.html new file mode 100644 index 0000000..478720c --- /dev/null +++ b/join/v0.1/index.html @@ -0,0 +1,1674 @@ + + + + +Join + + + + + +
+
+

Join

+
+

for defining supergraphs which join multiple subgraphs

+ + + +
StatusRelease
Version0.1
+ + +
Schema joining multiple subgraphs
graph LR + classDef bg fill:none,color:#22262E; + s1(auth.graphql):::bg-->core(composed schema: photos.graphql) + s2(images.graphql):::bg-->core + s3(albums.graphql):::bg-->core + style core fill:none,stroke:fuchsia,color:fuchsia; +

This document defines a core schema named join for describing core schemas which join multiple subgraph schemas into a single supergraph schema.

+

This specification provides machinery to:

+
    +
  • define subgraphs with the join__Graph enum and the @join__graph directive
  • +
  • assign fields to subgraphs with the @join__field directive
  • +
  • declare additional data required and provided by subgraph field resolvers with the requires and provides arguments to @join__field
  • +
  • assign keys and ownership to types with the @join__type and @join__owner directives
  • +
+
+ +
+
+

1How to read this document

+

This document uses RFC 2119 guidance regarding normative terms: MUST / MUST NOT / REQUIRED / SHALL / SHALL NOT / SHOULD / SHOULD NOT / RECOMMENDED / MAY / OPTIONAL.

+
+

1.1What this document isn't

+

This document specifies only the structure and semantics of supergraphs. It’s expected that a supergraph will generally be the output of a compilation process which composes subgraphs. The mechanics of that process are not specified normatively here. Conforming implementations may choose any approach they like, so long as the result conforms to the requirements of this document.

+
+
+
+

2Example: Photo Library

+

This section is non‐normative.

+

We’ll refer to this example of a photo library throughout the document:

+
Example № 1 Photos library composed schema
schema
+  @core(feature: "https://specs.apollo.dev/core/v1.0")
+  @core(feature: "https://specs.apollo.dev/join/v1.0") {
+  query: Query
+}
+
+directive @core(feature: String!) repeatable on SCHEMA
+
+directive @join__owner(graph: join__Graph!) on OBJECT
+
+directive @join__type(
+  graph: join__Graph!
+  key: String!
+) repeatable on OBJECT | INTERFACE
+
+directive @join__field(
+  graph: join__Graph
+  requires: String
+  provides: String
+) on FIELD_DEFINITION
+
+directive @join__graph(name: String!, url: String!) on ENUM_VALUE
+
+enum join__Graph {
+  AUTH @join__graph(name: "auth", url: "https://auth.api.com")
+  ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
+  IMAGES @join__graph(name: "images", url: "https://images.api.com")
+}
+
+type Query {
+  me: User @join__field(graph: AUTH)
+  images: [Image] @join__field(graph: IMAGES)
+}
+
+type User
+    @join__owner(graph: AUTH)
+    @join__type(graph: AUTH, key: "id")
+    @join__type(graph: ALBUMS, key: "id") {
+  id: ID! @join__field(graph: AUTH)
+  name: String @join__field(graph: AUTH)
+  albums: [Album!] @join__field(graph: ALBUMS)
+}
+
+type Album
+    @join__owner(graph: ALBUMS)
+    @join__type(graph: ALBUMS, key: "id") {
+  id: ID!
+  user: User
+  photos: [Image!]
+}
+
+type Image
+    @join__owner(graph: IMAGES)
+    @join__type(graph: ALBUMS, key: "url")
+    @join__type(graph: IMAGES, key: "url") {
+  url: Url @join__field(graph: IMAGES)
+  type: MimeType @join__field(graph: IMAGES)
+  albums: [Album!] @join__field(graph: ALBUMS)
+}
+
+scalar Url
+scalar MimeType
+
+

The meaning of the @join__* directives is explored in the Directives section.

+

The example represents one way to compose three input schemas, based on federated composition. These schemas are provided for purposes of illustration only. This spec places no normative requirements on composer input. It does not require that subgraphs use federated composition directives, and it does not place any requirements on how the composer builds a supergraph, except to say that the resulting schema must be a valid supergraph document.

+

The auth subgraph provides the User type and Query.me.

+
Example № 2 Auth schema
type User @key(fields: "id") {
+  id: ID!
+  name: String
+}
+
+type Query {
+  me: User
+}
+

The images subgraph provides the Image type and URL scalar.

+
Example № 3 Images schema
type Image @key(fields: "url") {
+  url: Url
+  type: MimeType
+}
+
+type Query {
+  images: [Image]
+}
+
+extend type User {
+  favorite: Image
+}
+
+scalar Url
+scalar MimeType
+

The albums subgraph provides the Album type and extends User and Image with album information.

+
Example № 4 Albums schema
type Album @key(fields: "id") {
+  id: ID!
+  user: User
+  photos: [Image!]
+}
+
+extend type Image {
+  albums: [Album!]
+}
+
+extend type User {
+  albums: [Album!]
+  favorite: Album
+}
+
+
+

3Actors

+
Actors and roles within an example composition pipeline
flowchart TB + classDef bg fill:#EBE6FF; + subgraph A [subgraph A] + schemaA([schema A]):::bg + style schemaA color:#000 + endpointA([endpoint A]):::bg + style endpointA color:#000 + end + style A fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + subgraph B [subgraph B] + schemaB([schema B]):::bg + style schemaB color:#000 + endpointB([endpoint B]):::bg + style endpointB color:#000 + end + style B fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + subgraph C [subgraph C] + schemaC([schema C]):::bg + style schemaC color:#000 + endpointC([endpoint C]):::bg + style endpointC color:#000 + end + style C fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + subgraph producer["Producer ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"] + Composer + style Composer color:#000 + end + style producer fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + supergraph([Supergraph]):::bg + style supergraph color:#000 + subgraph consumer["Consumer ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"] + Router + style Router color:#000 + end + style consumer fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + A-->Composer:::bg + B-->Composer:::bg + C-->Composer:::bg + Composer-->supergraphSchema([Supergraph Schema]):::bg + style supergraphSchema color:#000 + supergraphSchema-->Router:::bg + Router-->published([Published Schema]):::bg + style published color:#000 + published-->Clients:::bg + style Clients color:#000 + Clients-->Router:::bg +

Producers generate supergraphs. This spec places requirements on supergraph producers.

+

Consumers consume supergraphs. This spec places requirements on supergraph consumers.

+

Composers (or compilers) are producers which compose subgraphs into a supergraph. This document places no particular requirements on the composition algorithm, except that it must produce a valid supergraph.

+

Routers are consumers which serve a composed schema as a GraphQL endpoint. This definition is non‐normative.

+ +

Endpoints are running servers which can resolve GraphQL queries against a schema. In this version of the spec, endpoints must be URLs, typically http/https URLs.

+

Subgraphs are GraphQL schemas which are composed to form a supergraph. Subgraph names and metadata are declared within the special join__Graph enum.

+

This spec does not place any requirements on subgraph schemas. Generally, they may be of any shape. In particular, subgraph schemas do not need to be supergraphs themselves or to follow this spec in any way; neither is it an error for them to do so. Composers MAY place additional requirements on subgraph schemas to aid in composition; composers SHOULD document any such requirements.

+
+
+

4Overview

+

This section is non‐normative. It describes the motivation behind the directives defined by this specification.

+

A supergraph schema describes a GraphQL schema that can be served by a router. The router does not contain logic to resolve any of the schema’s fields; instead, the supergraph schema contains directives starting with @join__ that tell the router which subgraph endpoint can resolve each field, as well as other information needed in order to construct subgraph operations.

+

The directives described in this specification are designed for a particular query planning algorithm, and so there are some restrictions on how they can be combined that originate from the requirements of this algorithm. For example, this specification describes a concept of type ownership which exists not because we believe it describes the ideal method of structuring your subgraphs, but because this query planning algorithm depends on type ownership. We hope that future versions of this specification can relax some of these restrictions.

+

Each supergraph schema contains a list of its included subgraphs. The join__Graph enum represents this list with an enum value for each subgraph. Each enum value is annotated with a @join__graph directive telling the router what endpoint can be used to reach the subgraph, and giving the subgraph a human‐readable name that can be used for purposes such as query plan visualization and server logs.

+

To resolve a field, the router needs to know to which subgraphs it can delegate the field’s resolution. One explicit way to indicate this in a supergraph schema is by annotating the field with a @join__field directive specifying which subgraph should be used to resolve that field. (There are other ways of indicating which subgraphs can resolve a field which will be described later.)

+

In order for the router to send an operation that resolves a given field on a parent object to a subgraph, the operation needs to first resolve the parent object itself. There are several ways to accomplish this, described below. The examples below include abbreviated versions of the supergraph schemas which do not include the schema definition, directive definitions, or the join__Graph definition. This specification does not require the subgraph operations to be the same as those described in these examples; this is just intended to broadly describe the meanings of the directives.

+
+

4.1Root fields

+

If a field appears at the root of the overall operation (query or mutation), then it can be placed at the root of the subgraph operation.

+
Example № 5 Root fields
# Supergraph schema
+type Query {
+  fieldA: String @join__field(graph: A)
+  fieldAlsoFromA: String @join__field(graph: A)
+  fieldB: String @join__field(graph: B)
+}
+
+# Operation
+{ fieldA fieldAlsoFromA fieldB }
+# Generated subgraph operations
+## On A:
+{ fieldA fieldAlsoFromA }
+## On B:
+{ fieldB }
+
+
+
+

4.2Fields on the same subgraph as the parent operation

+

If a field’s parent field will be resolved by an operation on the same subgraph, then it can be resolved as part of the same operation, by putting it in a nested selection set on the parent field’s subgraph operation. Note that this example contains @join__owner and @join__type directives on an object type; these will be described later.

+
Example № 6 Fields on the same subgraph as the parent operation
# Supergraph schema
+type Query {
+  fieldA: X @join__field(graph: A)
+}
+
+type X @join__owner(graph: A) @join__type(graph: A, key: "nestedFieldA") {
+  nestedFieldA: String @join__field(graph: A)
+}
+
+# Operation
+{ fieldA { nestedFieldA } }
+# Generated subgraph operations
+## On A:
+{ fieldA { nestedFieldA }}
+
+
+
+

4.3Fields provided by the parent field

+

Sometimes, a subgraph G may be capable of resolving a field that is ordinarily resolved in a different subgraph if the field’s parent object was resolved in G. Consider an example where the Product.priceCents: Int! field is usually resolved by the Products subgraph, which knows the priceCents for every Product in your system. In the Marketing subgraph, there is a Query.todaysPromotion: Product! field. While the Marketing subgraph cannot determine the priceCents of every product in your system, it does know the priceCents of the promoted products, and so the Marketing subgraph can resolve operations like { todaysPromotion { priceCents } }.

+

When this is the case, you can include a provides argument in the @join__field listing these “pre‐calculated” fields. The router can now resolve these fields in the “providing” subgraph instead of in the subgraph that would usually be used to resolve those fields.

+
Example № 7 Provided fields
# Supergraph schema
+type Query {
+  todaysPromotion: Product! @join__field(graph: MARKETING, provides: "priceCents")
+  randomProduct: Product! @join__field(graph: PRODUCTS)
+}
+
+type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "id") {
+  id: ID! @join__field(graph: PRODUCTS)
+  priceCents: Int! @join__field(graph: PRODUCTS)
+}
+
+# Operation showing that `priceCents` is typically resolved on PRODUCTS
+{ randomProduct { priceCents } }
+# Generated subgraph operations
+## On PRODUCTS
+{ randomProduct { priceCents } }
+
+# Operation showing that `provides` allows `priceCents` to be resolved on MARKETING
+{ todaysPromotion { priceCents } }
+# Generated subgraph operations
+## On MARKETING
+{ todaysPromotion { priceCents } }
+
+
+
+

4.4Fields on value types

+

Some types have the property that all of their fields can be resolved by any subgraph that can resolve a field returning that type. These types are called value types. (Imagine a type type T { x: Int, y: String } where every resolver for a field of type T actually produces an object like {x: 1, y: "z"}, and the resolvers for the two fields on T just unpack the values already in the object.) In a supergraph schema, a type is a value type if it does not have a @join__owner directive on it.

+
Example № 8 Value types
# Supergraph schema
+type Query {
+  fieldA: X @join__field(graph: A)
+  fieldB: X @join__field(graph: B)
+}
+
+type X {
+  anywhere: String
+}
+
+# Operation
+{ fieldA { anywhere } }
+# Generated subgraph operations
+## On A
+{ fieldA { anywhere } }
+
+# Operation
+{ fieldB { anywhere } }
+# Generated subgraph operations
+## On B
+{ fieldB { anywhere } }
+
+
+
+

4.5Owned fields on owned types

+

We’ve finally reached the most interesting case: a field that must be resolved by an operation on a different subgraph from the subgraph on which its parent field was resolved. In order to do this, we need a way to tell the subgraph to resolve that parent object. We do this by defining a special root field in the subgraph’s schema: Query._entities(representations: [_Any!]!): [_Entity]!. This field takes a list of “representations” and returns a list of the same length of the corresponding objects resulting from looking up the representations in an application‐dependent way.

+

What is a representation? A representation is expressed as the scalar type _Any, and can be any JSON object with a top‐level __typename key with a string value. Often, a representation will be something like {__typename: "User", id: "abcdef"}: the type name plus one or more fields that you can use to look up the object in a database.

+

There are several ways that the router can calculate a representation to pass to a subgraph. In this specification, all non‐value types have a specific subgraph referred to as its “owner”, specified via a @join__owner directive on the type. Object types that are not value types are referred to as “entities”; the type _Entity referenced above is a union defined in each subgraph’s schema consisting of the entity types defined by that subgraph. (Only subgraphs which define entities need to define the Query._entities field.) Entity types must also have at least one @join__type directive specifying the owning subgraph along with a key. For each additional subgraph which can resolve fields returning that type, there should be exactly one @join__type directive specifying that subgraph along with a key, which should be identical to one of the keys specified with the owning subgraph.

+

A key is a set of fields on the type (potentially including sub‐selections and inline fragments), specified as a string. If a type T is annotated with @join__type(subgraph: G, key: "a b { c }"), then it must be possible to resolve the full field set provided as a key on subgraph G. Additionally, if you take an object with the structure returned by resolving that field set and add a field __typename: "T", then you should be able to pass the resulting value as a representation to the Query._entities field on subgraph G.

+

In order to resolve a field on an entity on the subgraph that owns its parent type, where that subgraph is different from the subgraph that resolved its parent object, the router first resolves a key for that object on the previous subgraph, and then uses that representation on the owning subgraph.

+

For convenience, you may omit @join__field(graph: A) directives on fields whose parent type is owned by A.

+
Example № 9 Owned fields on owned types
# Supergraph schema
+type Query {
+  fieldB: X @join__field(graph: B)
+}
+
+type X
+  @join__owner(graph: A)
+  # As the owner, A is allowed to have more than one key.
+  @join__type(graph: A, key: "x")
+  @join__type(graph: A, key: "y z")
+  # As non-owners, B and C can only have one key each and
+  # they must match a key from A.
+  @join__type(graph: B, key: "x")
+  @join__type(graph: C, key: "y z")
+{
+  # Because A owns X, we can omit @join__field(graph: A)
+  # from these three fields.
+  x: String
+  y: String
+  z: String
+}
+
+# Operation
+{ fieldB { y } }
+# Generated subgraph operations
+## On B. `y` is not available, so we need to fetch B's key for X.
+{ fieldB { x } }
+## On A
+## $r = [{__typename: "X", x: "some-x-value"}]
+query ($r: [_Any!]!) { _entities(representations: $r]) { y } }
+
+
+
+

4.6Extension fields on owned types

+

The previous section described how to jump from one subgraph to another in order to resolve a field on the subgraph that owns the field’s parent type. The situation is a bit more complicated when you want to resolve a field on a subgraph that doesn’t own the field’s parent type — what we call an extension field. That’s because we no longer have the guarantee that the subgraph you’re coming from and the subgraph you’re going to share a key in common. In this case, we may need to pass through the owning type.

+
Example № 10 Extension fields on owned types
# Supergraph schema
+type Query {
+  fieldB: X @join__field(graph: B)
+}
+
+type X
+  @join__owner(graph: A)
+  # As the owner, A is allowed to have more than one key.
+  @join__type(graph: A, key: "x")
+  @join__type(graph: A, key: "y z")
+  # As non-owners, B and C can only have one key each and
+  # they must match a key from A.
+  @join__type(graph: B, key: "x")
+  @join__type(graph: C, key: "y z")
+{
+  x: String
+  y: String
+  z: String
+  c: String @join__field(graph: C)
+}
+
+# Operation
+{ fieldB { c } }
+# Generated subgraph operations
+## On B. `c` is not available on B, so we need to eventually get over to C.
+## In order to do that, we need `y` and `z`... which aren't available on B
+## either! So we need to take two steps. First we use B's key.
+{ fieldB { x } }
+## On A. We use B's key to resolve our `X`, and we extract C's key.
+## $r = [{__typename: "X", x: "some-x-value"}]
+query ($r: [_Any!]!) { _entities(representations: $r]) { y z } }
+## On C. We can finally look up the field we need.
+## $r = [{__typename: "X", y: "some-y-value", z: "some-z-value"}]
+query ($r: [_Any!]!) { _entities(representations: $r]) { c } }
+
+

We only need to do this two‐jump process because the fields needed for C’s key are not available in B; otherwise a single jump would have worked, like in the owned‐field case.

+

Sometimes a particular extension field needs its parent object’s representation to contain more information than its parent type’s key requests. In this case, you can include a requires argument in the field’s @join__field listing those required fields (potentially including sub‐selections). All required fields must be resolvable in the owning subgraph (this restriction is why requires is only allowed on extension fields).

+
Example № 11 Required fields
# Supergraph schema
+type Query {
+  fieldA: X @join__field(graph: A)
+}
+
+type X
+  @join__owner(graph: A)
+  @join__type(graph: A, key: "x")
+  @join__type(graph: B, key: "x")
+{
+  x: String
+  y: String
+  z: String @join__field(graph: B, requires: "y")
+}
+
+# Operation
+{ fieldA { z } }
+# Generated subgraph operations
+## On A. `x` is included because it is B's key for `X`; `y`
+## is included because of the `requires`.
+{ fieldA { x y } }
+## On B..
+## $r = [{__typename: "X", x: "some-x-value", y: "some-y-value"}]
+query ($r: [_Any!]!) { _entities(representations: $r]) { z } }
+
+
+
+
+

5Basic Requirements

+

Schemas using the join core feature MUST be valid core schema documents with @core directives referencing the core specification and this specification.

+
Example № 12 @core directives for supergraphs
schema
+  @core(feature: "https://specs.apollo.dev/core/v1.0")
+  @core(feature: "https://specs.apollo.dev/join/v1.0") {
+  query: Query
+}
+

As described in the core schema specification, your schema may use a prefix other than join for all of the directive and enum names defined by this specification by including an as argument to the @core directive which references this specification. All references to directive and enum names in this specification MUST be interpreted as referring to names with the appropriate prefix chosen within your schema.

+

In order to use the directives described by this specification, GraphQL requires you to include their definitions in your schema.

+

Processors MUST validate that you have defined the directives with the same arguments, locations, and repeatable flag as given below.

+
directive @join__owner(graph: join__Graph!) on OBJECT
+
+directive @join__type(
+  graph: join__Graph!,
+  key: String!,
+) repeatable on OBJECT | INTERFACE
+
+directive @join__field(
+  graph: join__Graph,
+  requires: String,
+  provides: String,
+) on FIELD_DEFINITION
+
+directive @join__graph(name: String!, url: String!) on ENUM_VALUE
+
+

Processors MUST validate that the schema contains an enum named join__Graph; see its section below for other required properties of this enum.

+

As described in the core specification, all of the directives and enums defined by this schema should be removed from the supergraph’s API schema. For example, the join__Graph enum should not be visible via introspection.

+
+
+

6Enums

+
+

6.1join__Graph

+

Enumerate subgraphs.

+
enum join__Graph
+
+

Documents MUST define a join__Graph enum. Each enum value describes a subgraph. Each enum value MUST have a @join__graph directive applied to it.

+
Example № 13 Using join__Graph to define subgraphs and their endpoints
enum join__Graph {
+  AUTH @join__graph(name: "auth", url: "https://auth.api.com")
+  ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
+  IMAGES @join__graph(name: "images", url: "https://images.api.com")
+}
+

The join__Graph enum is used as input to the @join__owner, @join__field, and @join__type directives.

+
+
+
+

7Directives

+
+

7.1@join__graph

+

Declare subgraph metadata on join__Graph enum values.

+
directive @join__graph(name: String!, url: String!) on ENUM_VALUE
+
+
Example № 14 Using @join__graph to declare subgraph metadata on the join__Graph enum values.
enum join__Graph {
+  AUTH @join__graph(name: "auth", url: "https://auth.api.com")
+  ALBUMS @join__graph(name: "albums", url: "https://albums.api.com")
+  IMAGES @join__graph(name: "images", url: "https://images.api.com")
+}
+

The @join__graph directive MUST be applied to each enum value on join__Graph, and nowhere else. Each application of @join__graph MUST have a distinct value for the name argument; this name is an arbitrary non‐empty string that can be used as a human‐readable identifier which may be used for purposes such as query plan visualization and server logs. The url argument is an endpoint that can resolve GraphQL queries for the subgraph.

+
+
+

7.2@join__type

+

Declares an entity key for a type on a subgraph.

+
directive @join__type(
+  graph: join__Graph!
+  key: String!
+) repeatable on OBJECT | INTERFACE
+
+

When this directive is placed on a type T, it means that subgraph graph MUST be able to:

+
    +
  • Resolve selections on objects of the given type that contain the field set in key
  • +
  • Use Query._entities to resolve representations of objects containing __typename: "T" and the fields from the field set in key
  • +
+
Example № 15 Using @join__type to specify subgraph keys
type Image
+    @join__owner(graph: IMAGES)
+    @join__type(graph: ALBUMS, key: "url")
+    @join__type(graph: IMAGES, key: "url") {
+  url: Url @join__field(graph: IMAGES)
+  type: MimeType @join__field(graph: IMAGES)
+  albums: [Album!] @join__field(graph: ALBUMS)
+}
+

Every type with a @join__type MUST also have a @join__owner directive. Any type with a @join__owner directive MUST have at least one @join__type directive with the same graph as the @join__owner directive (the “owning graph”), and MUST have at most one @join__type directive for each graph value other than the owning graph. Any value that appears as a key in a @join__type directive with a graph value other than the owning graph must also appear as a key in a @join__type directive with graph equal to the owning graph.

+
+
+

7.3@join__field

+

Specify the graph that can resolve the field.

+
directive @join__field(
+  graph: join__Graph
+  requires: String
+  provides: String
+) on FIELD_DEFINITION
+
+

The field’s parent type MUST be annotated with a @join__type with the same value of graph as this directive, unless the parent type is a root operation type.

+

If a field is not annotated with @join__field (or if the graph argument is not provided or null) and its parent type is annotated with @join__owner(graph: G), then a processor MUST treat the field as if it is annotated with @join__field(graph: G). If a field is not annotated with @join__field (or if the graph argument is not provided or null) and its parent type is not annotated with @join__owner (ie, the parent type is a value type) then it MUST be resolvable in any subgraph that can resolve values of its parent type.

+
Example № 16 Using @join__field to join fields to subgraphs
type User
+    @join__owner(graph: AUTH)
+    @join__type(graph: AUTH, key: "id")
+    @join__type(graph: ALBUMS, key: "id") {
+  id: ID! @join__field(graph: AUTH)
+  name: String @join__field(graph: AUTH)
+  albums: [Album!] @join__field(graph: ALBUMS)
+}
+
+type Album
+    @join__owner(graph: ALBUMS)
+    @join__type(graph: ALBUMS, key: "id") {
+  id: ID!
+  user: User
+  photos: [Image!]
+}
+
+type Image
+    @join__owner(graph: IMAGES)
+    @join__type(graph: ALBUMS, key: "url")
+    @join__type(graph: IMAGES, key: "url") {
+  url: Url @join__field(graph: IMAGES)
+  type: MimeType @join__field(graph: IMAGES)
+  albums: [Album!] @join__field(graph: ALBUMS)
+}
+

Every field on a root operation type MUST be annotated with @join__field.

+
Example № 17 @join__field on root fields
type Query {
+  me: User @join__field(graph: AUTH)
+  images: [Image] @join__field(graph: IMAGES)
+}
+

The requires argument MUST only be specified on fields whose parent type has a @join__owner directive specifying a different graph than this @join__field directive does. All fields (including nested fields) mentioned in this field set must be resolvable in the parent type’s owning subgraph. When constructing a representation for a parent object of this field, a router will include the fields selected in this requires argument in addition to the appropriate key for the parent type.

+

The provides argument specifies fields that can be resolved in operations run on subgraph graph as a nested selection under this field, even if they ordinarily can only be resolved on other subgraphs.

+
+
+

7.4@join__owner

+

Specify the graph which owns the object type.

+
directive @join__owner(graph: join__Graph!) on OBJECT
+
+

The descriptions of @join__type and @join__field describes requirements on how @join__owner relates to @join__type and the requires argument to @join__field.

+
+Note +Type ownership is currently slated for removal in a future version of this spec. It is RECOMMENDED that router implementations consider approaches which function in the absence of these restrictions. The overview explains how the current router’s query planning algorithm depends on concept of type ownership.
+
+
+
+ + +
+
+ +
  1. 1How to read this document + +
      +
    1. 1.1What this document isn't
    2. +
    +
  2. +
  3. 2Example: Photo Library
  4. +
  5. 3Actors
  6. +
  7. 4Overview + +
      +
    1. 4.1Root fields
    2. +
    3. 4.2Fields on the same subgraph as the parent operation
    4. +
    5. 4.3Fields provided by the parent field
    6. +
    7. 4.4Fields on value types
    8. +
    9. 4.5Owned fields on owned types
    10. +
    11. 4.6Extension fields on owned types
    12. +
    +
  8. +
  9. 5Basic Requirements
  10. +
  11. 6Enums + +
      +
    1. 6.1join__Graph
    2. +
    +
  12. +
  13. 7Directives + +
      +
    1. 7.1@join__graph
    2. +
    3. 7.2@join__type
    4. +
    5. 7.3@join__field
    6. +
    7. 7.4@join__owner
    8. +
    +
  14. +
+
+ +
+ + + diff --git a/join/v0.1/join-v0.1.md b/join/v0.1/join-v0.1.md new file mode 100644 index 0000000..6b1c7b8 --- /dev/null +++ b/join/v0.1/join-v0.1.md @@ -0,0 +1,469 @@ +# Join + +

for defining *supergraphs* which join multiple *subgraphs*

+ +```raw html + + + +
StatusRelease
Version0.1
+ + +``` + +```mermaid diagram -- Schema joining multiple subgraphs +graph LR + classDef bg fill:none,color:#22262E; + s1(auth.graphql):::bg-->core(composed schema: photos.graphql) + s2(images.graphql):::bg-->core + s3(albums.graphql):::bg-->core + style core fill:none,stroke:fuchsia,color:fuchsia; +``` + +This document defines a [core schema](https://specs.apollo.dev/core/v0.1) named `join` for describing [core schemas](https://specs.apollo.dev/core/v0.1) which **join** multiple **subgraph** schemas into a single **supergraph** schema. + +This specification provides machinery to: +- define [subgraphs](#def-subgraph) with the {join__Graph} enum and the {@join__graph} directive +- assign fields to subgraphs with the {@join__field} directive +- declare additional data required and provided by subgraph field resolvers with the `requires` and `provides` arguments to {@join__field} +- assign [keys and ownership](#sec-Owned-fields-on-owned-types) to types with the {@join__type} and {@join__owner} directives + +# How to read this document + +This document uses [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) guidance regarding normative terms: MUST / MUST NOT / REQUIRED / SHALL / SHALL NOT / SHOULD / SHOULD NOT / RECOMMENDED / MAY / OPTIONAL. + +## What this document isn't + +This document specifies only the structure and semantics of supergraphs. It's expected that a supergraph will generally be the output of a compilation process which composes subgraphs. The mechanics of that process are not specified normatively here. Conforming implementations may choose any approach they like, so long as the result conforms to the requirements of this document. + +# Example: Photo Library + +*This section is non-normative.* + +We'll refer to this example of a photo library throughout the document: + +:::[example](./photos.graphql) -- Photos library composed schema + +The meaning of the `@join__*` directives is explored in the [Directives](#sec-Directives) section. + +The example represents **one way** to compose three input schemas, based on [federated composition](https://www.apollographql.com/docs/federation/federation-spec/). These schemas are provided for purposes of illustration only. This spec places no normative requirements on composer input. It does not require that subgraphs use federated composition directives, and it does not place any requirements on *how* the composer builds a supergraph, except to say that the resulting schema must be a valid supergraph document. + +The [auth](./auth.graphql) subgraph provides the `User` type and `Query.me`. + +:::[example](auth.graphql) -- Auth schema + +The [images](./images.graphql) subgraph provides the `Image` type and `URL` scalar. + +:::[example](./images.graphql) -- Images schema + +The [albums](./albums.graphql) subgraph provides the `Album` type and extends `User` and `Image` with album information. + +:::[example](./albums.graphql) -- Albums schema + + +# Actors + +```mermaid diagram -- Actors and roles within an example composition pipeline +flowchart TB + classDef bg fill:#EBE6FF; + subgraph A [subgraph A] + schemaA([schema A]):::bg + style schemaA color:#000 + endpointA([endpoint A]):::bg + style endpointA color:#000 + end + style A fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + subgraph B [subgraph B] + schemaB([schema B]):::bg + style schemaB color:#000 + endpointB([endpoint B]):::bg + style endpointB color:#000 + end + style B fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + subgraph C [subgraph C] + schemaC([schema C]):::bg + style schemaC color:#000 + endpointC([endpoint C]):::bg + style endpointC color:#000 + end + style C fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + subgraph producer["Producer ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"] + Composer + style Composer color:#000 + end + style producer fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + supergraph([Supergraph]):::bg + style supergraph color:#000 + subgraph consumer["Consumer ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"] + Router + style Router color:#000 + end + style consumer fill:#FCFDFF,stroke:#CAD0D8,color:#777F8E; + A-->Composer:::bg + B-->Composer:::bg + C-->Composer:::bg + Composer-->supergraphSchema([Supergraph Schema]):::bg + style supergraphSchema color:#000 + supergraphSchema-->Router:::bg + Router-->published([Published Schema]):::bg + style published color:#000 + published-->Clients:::bg + style Clients color:#000 + Clients-->Router:::bg +``` + +**Producers** generate supergraphs. This spec places requirements on supergraph producers. + +**Consumers** consume supergraphs. This spec places requirements on supergraph consumers. + +**Composers** (or **compilers**) are producers which compose subgraphs into a supergraph. This document places no particular requirements on the composition algorithm, except that it must produce a valid supergraph. + +**Routers** are consumers which serve a composed schema as a GraphQL endpoint. *This definition is non-normative.* + - Graph routers differ from standard GraphQL endpoints in that they are not expected to resolve fields or communicate with (non-GraphQL) backend services on their own. Instead, graph routers receive GraphQL requests and service them by performing additional GraphQL requests. This spec provides guidance for implementing routers, but does not require particular implementations of query separation or dispatch, nor does it attempt to normatively separate routers from other supergraph consumers. + - Routers expose an [API schema](https://specs.apollo.dev/core/v0.1/#sec-Parts-of-a-Core-Schema) to clients that is created by transforming the supergraph schema (for example, the {join__Graph} enum and the directives described in this spec are removed from the API schema). The API schema is used to validate client operations and may be exposed to clients via introspection. + +**Endpoints** are running servers which can resolve GraphQL queries against a schema. In this version of the spec, endpoints must be URLs, typically http/https URLs. + +**Subgraphs** are GraphQL schemas which are composed to form a supergraph. Subgraph names and metadata are declared within the special {join__Graph} enum. + +This spec does not place any requirements on subgraph schemas. Generally, they may be of any shape. In particular, subgraph schemas do not need to be supergraphs themselves or to follow this spec in any way; neither is it an error for them to do so. Composers MAY place additional requirements on subgraph schemas to aid in composition; composers SHOULD document any such requirements. + +# Overview + +*This section is non-normative.* It describes the motivation behind the directives defined by this specification. + +A supergraph schema describes a GraphQL schema that can be served by a router. The router does not contain logic to resolve any of the schema's fields; instead, the supergraph schema contains directives starting with {@join__} that tell the router which subgraph endpoint can resolve each field, as well as other information needed in order to construct subgraph operations. + +The directives described in this specification are designed for a particular query planning algorithm, and so there are some restrictions on how they can be combined that originate from the requirements of this algorithm. For example, this specification describes a concept of [type ownership](#sec-Owned-fields-on-owned-types) which exists not because we believe it describes the ideal method of structuring your subgraphs, but because this query planning algorithm depends on type ownership. We hope that future versions of this specification can relax some of these restrictions. + +Each supergraph schema contains a list of its included subgraphs. The [{join__Graph}](#join__Graph) enum represents this list with an enum value for each subgraph. Each enum value is annotated with a [{@join__graph}](#@join__graph) directive telling the router what endpoint can be used to reach the subgraph, and giving the subgraph a human-readable name that can be used for purposes such as query plan visualization and server logs. + +To resolve a field, the router needs to know to which subgraphs it can delegate the field's resolution. One explicit way to indicate this in a supergraph schema is by annotating the field with a [{@join__field}](#@join__field) directive specifying which subgraph should be used to resolve that field. (There are other ways of indicating which subgraphs can resolve a field which will be described later.) + +In order for the router to send an operation that resolves a given field on a parent object to a subgraph, the operation needs to first resolve the parent object itself. There are several ways to accomplish this, described below. The examples below include abbreviated versions of the supergraph schemas which do not include the `schema` definition, directive definitions, or the `join__Graph` definition. This specification does not require the subgraph operations to be the same as those described in these examples; this is just intended to broadly describe the meanings of the directives. + +## Root fields + +If a field appears at the root of the overall operation (query or mutation), then it can be placed at the root of the subgraph operation. + +```graphql example -- Root fields +# Supergraph schema +type Query { + fieldA: String @join__field(graph: A) + fieldAlsoFromA: String @join__field(graph: A) + fieldB: String @join__field(graph: B) +} + +# Operation +{ fieldA fieldAlsoFromA fieldB } +# Generated subgraph operations +## On A: +{ fieldA fieldAlsoFromA } +## On B: +{ fieldB } +``` + + +## Fields on the same subgraph as the parent operation + +If a field's parent field will be resolved by an operation on the same subgraph, then it can be resolved as part of the same operation, by putting it in a nested selection set on the parent field's subgraph operation. Note that this example contains {@join__owner} and {@join__type} directives on an object type; these will be described later. + +```graphql example -- Fields on the same subgraph as the parent operation +# Supergraph schema +type Query { + fieldA: X @join__field(graph: A) +} + +type X @join__owner(graph: A) @join__type(graph: A, key: "nestedFieldA") { + nestedFieldA: String @join__field(graph: A) +} + +# Operation +{ fieldA { nestedFieldA } } +# Generated subgraph operations +## On A: +{ fieldA { nestedFieldA }} +``` + +## Fields provided by the parent field + +Sometimes, a subgraph {G} may be capable of resolving a field that is ordinarily resolved in a different subgraph if the field's parent object was resolved in {G}. Consider an example where the `Product.priceCents: Int!` field is usually resolved by the Products subgraph, which knows the `priceCents` for every `Product` in your system. In the Marketing subgraph, there is a `Query.todaysPromotion: Product!` field. While the Marketing subgraph cannot determine the `priceCents` of every product in your system, it does know the `priceCents` of the promoted products, and so the Marketing subgraph can resolve operations like `{ todaysPromotion { priceCents } }`. + +When this is the case, you can include a `provides` argument in the `@join__field` listing these "pre-calculated" fields. The router can now resolve these fields in the "providing" subgraph instead of in the subgraph that would usually be used to resolve those fields. + +```graphql example -- Provided fields +# Supergraph schema +type Query { + todaysPromotion: Product! @join__field(graph: MARKETING, provides: "priceCents") + randomProduct: Product! @join__field(graph: PRODUCTS) +} + +type Product @join__owner(graph: PRODUCTS) @join__type(graph: PRODUCTS, key: "id") { + id: ID! @join__field(graph: PRODUCTS) + priceCents: Int! @join__field(graph: PRODUCTS) +} + +# Operation showing that `priceCents` is typically resolved on PRODUCTS +{ randomProduct { priceCents } } +# Generated subgraph operations +## On PRODUCTS +{ randomProduct { priceCents } } + +# Operation showing that `provides` allows `priceCents` to be resolved on MARKETING +{ todaysPromotion { priceCents } } +# Generated subgraph operations +## On MARKETING +{ todaysPromotion { priceCents } } +``` + +## Fields on value types + +Some types have the property that all of their fields can be resolved by *any* subgraph that can resolve a field returning that type. These types are called *value types*. (Imagine a type `type T { x: Int, y: String }` where every resolver for a field of type `T` actually produces an object like `{x: 1, y: "z"}`, and the resolvers for the two fields on `T` just unpack the values already in the object.) In a supergraph schema, a type is a value type if it does not have a [{@join__owner}](#@join__owner) directive on it. + +```graphql example -- Value types +# Supergraph schema +type Query { + fieldA: X @join__field(graph: A) + fieldB: X @join__field(graph: B) +} + +type X { + anywhere: String +} + +# Operation +{ fieldA { anywhere } } +# Generated subgraph operations +## On A +{ fieldA { anywhere } } + +# Operation +{ fieldB { anywhere } } +# Generated subgraph operations +## On B +{ fieldB { anywhere } } +``` + +## Owned fields on owned types + +We've finally reached the most interesting case: a field that must be resolved by an operation on a different subgraph from the subgraph on which its parent field was resolved. In order to do this, we need a way to tell the subgraph to resolve that parent object. We do this by defining a special root field in the subgraph's schema: `Query._entities(representations: [_Any!]!): [_Entity]!`. This field takes a list of "representations" and returns a list of the same length of the corresponding objects resulting from looking up the representations in an application-dependent way. + +What is a representation? A representation is expressed as the scalar type `_Any`, and can be any JSON object with a top-level `__typename` key with a string value. Often, a representation will be something like `{__typename: "User", id: "abcdef"}`: the type name plus one or more fields that you can use to look up the object in a database. + +There are several ways that the router can calculate a representation to pass to a subgraph. In this specification, all non-value types have a specific subgraph referred to as its "owner", specified via a `@join__owner` directive on the type. Object types that are not value types are referred to as "entities"; the type `_Entity` referenced above is a union defined in each subgraph's schema consisting of the entity types defined by that subgraph. (Only subgraphs which define entities need to define the `Query._entities` field.) Entity types must also have at least one `@join__type` directive specifying the owning subgraph along with a {key}. For each additional subgraph which can resolve fields returning that type, there should be exactly one `@join__type` directive specifying that subgraph along with a {key}, which should be identical to one of the keys specified with the owning subgraph. + +A key is a set of fields on the type (potentially including sub-selections and inline fragments), specified as a string. If a type `T` is annotated with `@join__type(subgraph: G, key: "a b { c }")`, then it must be possible to resolve the full field set provided as a key on subgraph G. Additionally, if you take an object with the structure returned by resolving that field set and add a field `__typename: "T"`, then you should be able to pass the resulting value as a representation to the `Query._entities` field on subgraph G. + +In order to resolve a field on an entity on the subgraph that owns its parent type, where that subgraph is different from the subgraph that resolved its parent object, the router first resolves a key for that object on the previous subgraph, and then uses that representation on the owning subgraph. + +For convenience, you may omit `@join__field(graph: A)` directives on fields whose parent type is owned by `A`. + +```graphql example -- Owned fields on owned types +# Supergraph schema +type Query { + fieldB: X @join__field(graph: B) +} + +type X + @join__owner(graph: A) + # As the owner, A is allowed to have more than one key. + @join__type(graph: A, key: "x") + @join__type(graph: A, key: "y z") + # As non-owners, B and C can only have one key each and + # they must match a key from A. + @join__type(graph: B, key: "x") + @join__type(graph: C, key: "y z") +{ + # Because A owns X, we can omit @join__field(graph: A) + # from these three fields. + x: String + y: String + z: String +} + +# Operation +{ fieldB { y } } +# Generated subgraph operations +## On B. `y` is not available, so we need to fetch B's key for X. +{ fieldB { x } } +## On A +## $r = [{__typename: "X", x: "some-x-value"}] +query ($r: [_Any!]!) { _entities(representations: $r]) { y } } +``` + +## Extension fields on owned types + +The previous section described how to jump from one subgraph to another in order to resolve a field on the subgraph that owns the field's parent type. The situation is a bit more complicated when you want to resolve a field on a subgraph that doesn't own the field's parent type — what we call an extension field. That's because we no longer have the guarantee that the subgraph you're coming from and the subgraph you're going to share a key in common. In this case, we may need to pass through the owning type. + +```graphql example -- Extension fields on owned types +# Supergraph schema +type Query { + fieldB: X @join__field(graph: B) +} + +type X + @join__owner(graph: A) + # As the owner, A is allowed to have more than one key. + @join__type(graph: A, key: "x") + @join__type(graph: A, key: "y z") + # As non-owners, B and C can only have one key each and + # they must match a key from A. + @join__type(graph: B, key: "x") + @join__type(graph: C, key: "y z") +{ + x: String + y: String + z: String + c: String @join__field(graph: C) +} + +# Operation +{ fieldB { c } } +# Generated subgraph operations +## On B. `c` is not available on B, so we need to eventually get over to C. +## In order to do that, we need `y` and `z`... which aren't available on B +## either! So we need to take two steps. First we use B's key. +{ fieldB { x } } +## On A. We use B's key to resolve our `X`, and we extract C's key. +## $r = [{__typename: "X", x: "some-x-value"}] +query ($r: [_Any!]!) { _entities(representations: $r]) { y z } } +## On C. We can finally look up the field we need. +## $r = [{__typename: "X", y: "some-y-value", z: "some-z-value"}] +query ($r: [_Any!]!) { _entities(representations: $r]) { c } } +``` + +We only need to do this two-jump process because the fields needed for C's key are not available in B; otherwise a single jump would have worked, like in the owned-field case. + +Sometimes a particular extension field needs its parent object's representation to contain more information than its parent type's key requests. In this case, you can include a `requires` argument in the field's `@join__field` listing those required fields (potentially including sub-selections). **All required fields must be resolvable in the owning subgraph** (this restriction is why `requires` is only allowed on extension fields). + +```graphql example -- Required fields +# Supergraph schema +type Query { + fieldA: X @join__field(graph: A) +} + +type X + @join__owner(graph: A) + @join__type(graph: A, key: "x") + @join__type(graph: B, key: "x") +{ + x: String + y: String + z: String @join__field(graph: B, requires: "y") +} + +# Operation +{ fieldA { z } } +# Generated subgraph operations +## On A. `x` is included because it is B's key for `X`; `y` +## is included because of the `requires`. +{ fieldA { x y } } +## On B.. +## $r = [{__typename: "X", x: "some-x-value", y: "some-y-value"}] +query ($r: [_Any!]!) { _entities(representations: $r]) { z } } +``` + +# Basic Requirements + +Schemas using the `join` core feature MUST be valid [core schema documents](https://specs.apollo.dev/core/v0.1) with {@core} directives referencing the `core` specification and this specification. + +:::[example](photos.graphql#schema) -- {@core} directives for supergraphs + +As described in the [core schema specification](https://specs.apollo.dev/core/v0.1/#sec-Prefixing), your schema may use a prefix other than `join` for all of the directive and enum names defined by this specification by including an `as` argument to the `@core` directive which references this specification. All references to directive and enum names in this specification MUST be interpreted as referring to names with the appropriate prefix chosen within your schema. + +In order to use the directives described by this specification, GraphQL requires you to include their definitions in your schema. + +Processors MUST validate that you have defined the directives with the same arguments, locations, and `repeatable` flag as given below. + +:::[definition](spec.graphql) + +Processors MUST validate that the schema contains an enum named {join__Graph}; see [its section below](#join__Graph) for other required properties of this enum. + +As described in the core specification, all of the directives and enums defined by this schema should be removed from the supergraph's [API schema](https://specs.apollo.dev/core/v0.1/#sec-Parts-of-a-Core-Schema). For example, the {join__Graph} enum should not be visible via introspection. + +# Enums + +##! join__Graph + +Enumerate subgraphs. + +```graphql definition +enum join__Graph +``` + +Documents MUST define a {join__Graph} enum. Each enum value describes a subgraph. Each enum value MUST have a [{@join__graph}](#@join__graph) directive applied to it. + +:::[example](photos.graphql#join__Graph) -- Using join__Graph to define subgraphs and their endpoints + +The {join__Graph} enum is used as input to the [{@join__owner}](#@join__owner), [{@join__field}](#@join__field), and [{@join__type}](#@join__type) directives. + +# Directives + +##! @join__graph + +Declare subgraph metadata on {join__Graph} enum values. + +```graphql definition +directive @join__graph(name: String!, url: String!) on ENUM_VALUE +``` + +:::[example](photos.graphql#join__Graph) -- Using {@join__graph} to declare subgraph metadata on the {join__Graph} enum values. + +The {@join__graph} directive MUST be applied to each enum value on {join__Graph}, and nowhere else. Each application of {@join__graph} MUST have a distinct value for the `name` argument; this name is an arbitrary non-empty string that can be used as a human-readable identifier which may be used for purposes such as query plan visualization and server logs. The `url` argument is an endpoint that can resolve GraphQL queries for the subgraph. + +##! @join__type + +Declares an entity key for a type on a subgraph. + +```graphql definition +directive @join__type( + graph: join__Graph! + key: String! +) repeatable on OBJECT | INTERFACE +``` + +When this directive is placed on a type `T`, it means that subgraph `graph` MUST be able to: +- Resolve selections on objects of the given type that contain the field set in `key` +- Use `Query._entities` to resolve representations of objects containing `__typename: "T"` and the fields from the field set in `key` + +:::[example](photos.graphql#Image) -- Using {@join__type} to specify subgraph keys + +Every type with a {@join__type} MUST also have a [{@join__owner}](#@join__owner) directive. Any type with a [{@join__owner}](#@join__owner) directive MUST have at least one {@join__type} directive with the same `graph` as the [{@join__owner}](#@join__owner) directive (the "owning graph"), and MUST have at most one {@join__type} directive for each `graph` value other than the owning graph. Any value that appears as a `key` in a {@join__type} directive with a `graph` value other than the owning graph must also appear as a `key` in a {@join__type} directive with `graph` equal to the owning graph. + +##! @join__field + +Specify the graph that can resolve the field. + +```graphql definition +directive @join__field( + graph: join__Graph + requires: String + provides: String +) on FIELD_DEFINITION +``` + +The field's parent type MUST be annotated with a {@join__type} with the same value of `graph` as this directive, unless the parent type is a [root operation type](http://spec.graphql.org/draft/#sec-Root-Operation-Types). + +If a field is not annotated with {@join__field} (or if the `graph` argument is not provided or `null`) and its parent type is annotated with `@join__owner(graph: G)`, then a processor MUST treat the field as if it is annotated with `@join__field(graph: G)`. If a field is not annotated with {@join__field} (or if the `graph` argument is not provided or `null`) and its parent type is not annotated with {@join__owner} (ie, the parent type is a value type) then it MUST be resolvable in any subgraph that can resolve values of its parent type. + +:::[example](photos.graphql#User...Image) -- Using {@join__field} to join fields to subgraphs + +Every field on a root operation type MUST be annotated with {@join__field}. + +:::[example](photos.graphql#Query) -- {@join__field} on root fields + +The `requires` argument MUST only be specified on fields whose parent type has a [{@join__owner}](#@join__owner) directive specifying a different `graph` than this {@join__field} directive does. All fields (including nested fields) mentioned in this field set must be resolvable in the parent type's owning subgraph. When constructing a representation for a parent object of this field, a router will include the fields selected in this `requires` argument in addition to the appropriate `key` for the parent type. + +The `provides` argument specifies fields that can be resolved in operations run on subgraph `graph` as a nested selection under this field, even if they ordinarily can only be resolved on other subgraphs. + +##! @join__owner + +Specify the graph which owns the object type. + +```graphql definition +directive @join__owner(graph: join__Graph!) on OBJECT +``` + +The descriptions of [{@join__type}](#@join__type) and [{@join__field}](#@join__field) describes requirements on how {@join__owner} relates to {@join__type} and the `requires` argument to {@join__field}. + +Note: Type ownership is currently slated for removal in a future version of this spec. It is RECOMMENDED that router implementations consider approaches which function in the absence of these restrictions. The [overview](#sec-Owned-fields-on-owned-types) explains how the current router's query planning algorithm depends on concept of type ownership. diff --git a/join/v0.1/package.json b/join/v0.1/package.json new file mode 100644 index 0000000..d7e225f --- /dev/null +++ b/join/v0.1/package.json @@ -0,0 +1,18 @@ +{ + "name": "csdl", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "rsync -avz --exclude .dist . .dist && spec-md spec.md > .dist/index.html", + "dev": "npm run build || true && chokidar '**/*' -i '.dist' -c 'npm run build'" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@queerviolet/speck": "git://github.com/queerviolet/speck.git#main", + "chokidar-cli": "^2.1.0", + "watch": "^1.0.2" + } +} diff --git a/join/v0.1/photos.graphql b/join/v0.1/photos.graphql new file mode 100644 index 0000000..2eb8e3c --- /dev/null +++ b/join/v0.1/photos.graphql @@ -0,0 +1,62 @@ +schema + @core(feature: "https://specs.apollo.dev/core/v1.0") + @core(feature: "https://specs.apollo.dev/join/v1.0") { + query: Query +} + +directive @core(feature: String!) repeatable on SCHEMA + +directive @join__owner(graph: join__Graph!) on OBJECT + +directive @join__type( + graph: join__Graph! + key: String! +) repeatable on OBJECT | INTERFACE + +directive @join__field( + graph: join__Graph + requires: String + provides: String +) on FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE + +enum join__Graph { + AUTH @join__graph(name: "auth", url: "https://auth.api.com") + ALBUMS @join__graph(name: "albums", url: "https://albums.api.com") + IMAGES @join__graph(name: "images", url: "https://images.api.com") +} + +type Query { + me: User @join__field(graph: AUTH) + images: [Image] @join__field(graph: IMAGES) +} + +type User + @join__owner(graph: AUTH) + @join__type(graph: AUTH, key: "id") + @join__type(graph: ALBUMS, key: "id") { + id: ID! @join__field(graph: AUTH) + name: String @join__field(graph: AUTH) + albums: [Album!] @join__field(graph: ALBUMS) +} + +type Album + @join__owner(graph: ALBUMS) + @join__type(graph: ALBUMS, key: "id") { + id: ID! + user: User + photos: [Image!] +} + +type Image + @join__owner(graph: IMAGES) + @join__type(graph: ALBUMS, key: "url") + @join__type(graph: IMAGES, key: "url") { + url: Url @join__field(graph: IMAGES) + type: MimeType @join__field(graph: IMAGES) + albums: [Album!] @join__field(graph: ALBUMS) +} + +scalar Url +scalar MimeType diff --git a/join/v0.1/spec.graphql b/join/v0.1/spec.graphql new file mode 100644 index 0000000..876f2ad --- /dev/null +++ b/join/v0.1/spec.graphql @@ -0,0 +1,14 @@ +directive @join__owner(graph: join__Graph!) on OBJECT + +directive @join__type( + graph: join__Graph!, + key: String!, +) repeatable on OBJECT | INTERFACE + +directive @join__field( + graph: join__Graph, + requires: String, + provides: String, +) on FIELD_DEFINITION + +directive @join__graph(name: String!, url: String!) on ENUM_VALUE diff --git a/link/v1.0/link-v1.0.md b/link/v1.0/link-v1.0.md index 04f5050..010dba6 100644 --- a/link/v1.0/link-v1.0.md +++ b/link/v1.0/link-v1.0.md @@ -13,9 +13,9 @@ Core schemas provide tools for linking definitions from different GraphQL schema ```graphql example -- linking a directive from another schema extend schema - # you link @link by @linking link + # 👇🏽 first, link @link from this url @link(url: "https://specs.apollo.dev/link/v1.0") - # 👇🏽 schemas are identified by a url + # 👇🏽 link other schemas by their urls @link(url: "https://internal.example.com/admin") type Query { @@ -154,13 +154,13 @@ Using `@id` is not, strictly speaking, necessary. A URL can be associated with a Core schemas have a document-wide *scope*. A document's scope is a map of {Element} ==> {Binding}. The scope is constructed from a document's [@link](#@link) and [@id](#@id) directives and is used to [attribute](#sec-Attribution) definitions and references within the document. Elements are the same as in [global graph references](#sec-Global-Graph-References). When used as scope keys, they carry the following meanings: -- Schema({name}) — a schema {@link}ed from the document. {name} can be used as a [prefix](#sec-Prefixing) for definitions and references within the document, and {name} MUST either be a valid prefix or {null}, indicating the present schema. +- Schema({name}) — a schema {@link}ed from the document. {name} can be used as a [prefix](#sec-Name-Conventions) for definitions and references within the document, and {name} MUST either be a valid prefix or {null}, indicating the present schema. - Directive({name}) — a directive [imported](#@link/import) into the document - Type({name}) — a type [imported](#@link/import) into the document A {Binding} contains: - {gref}: GRef — the [global graph reference](#sec-Global-Graph-References) which is the target of the binding -- {implicit}: Bool — indicating whether the binding was explicitly imported or created implicitly. Implicit bindings may be overwritten by explicit bindings and will not be formed if an explicit binding for the item alreaady exists +- {implicit}: Bool — indicating whether the binding was explicitly imported or created implicitly. Implicit bindings are "soft"—they may be overwritten by explicit bindings and will not be formed if an explicit binding for the item alreaady exists. Similar to a [gref](#sec-Global-Graph-References)'s elements, different types of scoped items can have the same name without conflict. For example, a scope can contain both a type and schema named "User", although this should generally be avoided if possible. @@ -174,8 +174,8 @@ A {@link} without any imports introduces two entries into the scope: ```graphql example -- {@link} bringing a single schema into scope @link(url: "https://example.com/foreignSchema") - # 1. Schema("foreignSchema") -> https://example.com/foreignSchema (explicit) - # 2. Directive("foreignSchema") -> https://example.com/foreignSchema#@foreignSchema (implicit) + # 1. foreignSchema:: -> https://example.com/foreignSchema (explicit) + # 2. @foreignSchema -> https://example.com/foreignSchema#@foreignSchema (implicit) ``` {@link}ing a foreign schema whose URL does not have a name will create a schema binding if and only if [`as:`](#@link/as) is specified, and will never create a root directive reference: @@ -183,52 +183,52 @@ A {@link} without any imports introduces two entries into the scope: ```graphql example -- {@link} bringing a single schema into scope # 👇🏽 url does not have a name @link(url: "https://api.example.com", as: "example") - # 1. Schema("example") -> https://example.com#example (explicit) + # 1. example:: -> https://example.com#example (explicit) ``` A {@link} with imports will add these entries to the scope, in addition to entries for each import: ```graphql example -- {@link} importing items into the scope @link(url: "https://example.com/foreignSchema", import: ["SomeType", "@someDirective"]) - # 1. Schema("foreignSchema") -> https://example.com/foreignSchema (explicit) - # 2. Directive("foreignSchema") -> https://example.com/foreignSchema#@foreignSchema (implicit) - # 3. Type("SomeType") -> https://example.com/foreignSchema#SomeType (explicit) - # 4. Directive("someDirective") -> https://example.com/foreignSchema#@someDirective (explicit) + # 1. foreignSchema:: -> https://example.com/foreignSchema (explicit) + # 2. @foreignSchema -> https://example.com/foreignSchema#@foreignSchema (implicit) + # 3. SomeType -> https://example.com/foreignSchema#SomeType (explicit) + # 4. @someDirective -> https://example.com/foreignSchema#@someDirective (explicit) ``` Specifying [`as:`](#@link/as) changes the names of the scope items, but not their bound grefs: ```graphql example -- {@link} conflicting schema names @link(url: "https://example.com/foreignSchema", as: "other") - # 1. Schema("other") -> https://example.com/foreignSchema (explicit) - # 2. Directive("other") -> https://example.com/foreignSchema#@foreignSchema (implicit) + # 1. other:: -> https://example.com/foreignSchema (explicit) + # 2. @other -> https://example.com/foreignSchema#@foreignSchema (implicit) ``` It is not an error to overwrite an implicit binding with an explicit one: ```graphql example -- {@link} import overriding an implicit binding @link(url: "https://example.com/foreignSchema") - # 1. Schema("foreignSchema") -> https://example.com/foreignSchema (explicit) - # 2. Directive("foreignSchema") -> https://example.com/foreignSchema#@foreignSchema (implicit) + # 1. foreignSchema:: -> https://example.com/foreignSchema (explicit) + # 2. @foreignSchema -> https://example.com/foreignSchema#@foreignSchema (implicit) # (2) will be subsequently overwritten: @link(url: "https://other.com/otherSchema, import: ["@foreignSchema"]) - # 3. Schema("otherSchema") -> https://other.com/otherSchema (explicit) - # 4. Directive("otherSchema") -> https://other.com/otherSchema#@otherSchema (implicit) - # 5. Directive("foreignSchema") -> https://other.com/otherSchema#@foreignSchema (explicit, overwrites (2)) + # 3. otherSchema:: -> https://other.com/otherSchema (explicit) + # 4. @otherSchema -> https://other.com/otherSchema#@otherSchema (implicit) + # 5. @foreignSchema -> https://other.com/otherSchema#@foreignSchema (explicit, overwrites (2)) ``` But it is an error to overwrite an explicit binding, or for two implicit bindings to overlap: ```graphql counter-example -- {@link} conflicting schema names @link(url: "https://example.com/foreignSchema") - # 1. Schema("foreignSchema") -> https://example.com/foreignSchema (explicit) - # 2. Directive("foreignSchema") -> https://example.com/foreignSchema#@foreignSchema (implicit) + # 1. foreignSchema:: -> https://example.com/foreignSchema (explicit) + # 2. @foreignSchema -> https://example.com/foreignSchema#@foreignSchema (implicit) @link(url: "https://other.com/foreignSchema") - # ❌ Schema("foreignSchema") -> https://other.com/foreignSchema (explicit) + # ❌ foreignSchema:: -> https://other.com/foreignSchema (explicit) # (error, conflicts with with (1)) - # ❌ Directive("foreignSchema") -> https://other.com/otherSchema#otherSchema (implicit) + # ❌ @foreignSchema -> https://other.com/otherSchema#otherSchema (implicit) # (error, conflicts with (2)) ``` @@ -245,6 +245,43 @@ Permissive processors (for example, a language server which wants to provide bes # 1. Schema() -> https://example.com/myself (explicit) ``` +# Name Conventions + +Within a core schema, type names and directives which begin with a valid namespace identifier followed by two underscores (`__`) will be [attributed](#sec-Attribution) to the foreign schema bound to that name in the document [scope](#sec-Scope) if one exists. + +```graphql example -- using a prefixed name +extend schema + @link(url: "https://specs.apollo.dev/link/v1.0") +# 1. Schema("link") -> "https://specs.apollo.dev/link/v1.0" (explicit) +# 2. Directive("link") -> "https://specs.apollo.dev/link/v1.0#@link" (implicit) + +# 👇🏽 🌍 https://specs.apollo.dev/link/v1.0/#Purpose +enum link__Purpose { SECURITY EXECUTION } +``` + +If no schema has been linked to that name, it is interpreted as a local name: + +```graphql example -- a strange local name +extend schema + @link(url: "https://specs.apollo.dev/link/v1.0") +# 1. Schema("link") -> "https://specs.apollo.dev/link/v1.0" (explicit) +# 2. Directive("link") -> "https://specs.apollo.dev/link/v1.0#@link" (implicit) + +# 👇🏽 🌍 #myOwn__Purpose (note, this document has no @id, so the url of this gref is null) +enum myOwn__Purpose { SECURITY EXECUTION } +``` + +```graphql example -- a strange local name in a document with an id +extend schema + @id(url: "https://api.example.com") + @link(url: "https://specs.apollo.dev/link/v1.0", import: ["@id"] + +# 👇🏽 🌍 https://api.example.com#myOwn__Purpose (note, this document has no @id, so the url of this gref is null) +enum myOwn__Purpose { SECURITY EXECUTION } +``` + +Note: GraphQL name conventions strongly suggest against such naming. But amongst the core schema design principles is *universality*—the ability to represent and link *any arbitrary* set of GraphQL schemas, no matter how weird the names in them are. + ## Bootstrapping Documents can {@link} link itself. Indeed, if they MUST do so if they use [@link](#@link) at all and are intended to be [fully valid](#sec-Fully-Valid-Core-Schemas): @@ -315,7 +352,7 @@ Link URLs serve two main purposes: Link URLs SHOULD be [RFC 3986 URLs](https://tools.ietf.org/html/rfc3986). When viewed, the URL SHOULD provide schema documentation in some human-readable form—a human reader should be able to click the link and go to the correct version of the docs. This is not an absolute functional requirement—as far as the core schema machinery is concerned, the URL is simply a globally unique namespace identifier with a particular form. -Link URLs MAY contain information about the spec's [name](#sec-Prefixing) and [version](#sec-Versioning): +Link URLs MAY contain information about the spec's [name](#sec-Name-Conventions) and [version](#sec-Versioning): ```html diagram -- Basic anatomy of a link URL @@ -343,11 +380,11 @@ All of these are valid arguments to `url`, and their interpretations: | https://spec.example.com/v1.0 | https://spec.example.com/v1.0 | *(null)* | v1.0 | | https://spec.example.com/vX | https://spec.example.com/vX | vX | *(null)* | -If `name` is present, that [namespace prefix](#sec-Prefixing) will automatically be linked to the URL. If a `name` is not present, then elements of the foreign schema must be [`imported`](#@link/import) in order to be referenced. +If `name` is present, that [namespace prefix](#sec-Name-Conventions) will automatically be linked to the URL. If a `name` is not present, then elements of the foreign schema must be [`imported`](#@link/import) in order to be referenced. ###! as: String -Change the [namespace prefix](#sec-Prefixing) assigned to the foreign schema. +Change the [namespace prefix](#sec-Name-Conventions) assigned to the foreign schema. The name MUST be a valid GraphQL identifier, MUST NOT contain the namespace separator ({"__"}), and MUST NOT end with an underscore (which would create ambiguity between whether {"x___y"} is prefix `x_` for element `y` or prefix `x` for element `_y`). @@ -391,7 +428,7 @@ See the [Import](#Import) scalar for a description of the format. An optional [purpose](#Purpose) for this link. This hints to consumers as to whether they can safely ignore metadata described by a foreign schema. -By default, {@link}s SHOULD fail open. This means that {@link}s to unknown schemas SHOULD NOT prevent a schema from being served or processed. Instead, consumers SHOULD ignore unknown feature metadata and serve or process the rest of the schema normally. +By default, {@link}s SHOULD fail open. This means that {@link}s to unknown schemas SHOULD NOT prevent a schema from being served or processed. Instead, consumers SHOULD ignore unknown links and serve or process the rest of the schema normally. This behavior is different for {@link}s with a specified purpose: - `SECURITY` links convey metadata necessary to compute the API schema and securely resolve fields within it @@ -440,22 +477,22 @@ or an object with `name` and (optionally `as`): Imports cannot currently reference transitive schemas: ```graphql counter-example -- incorrectly importing a transitive schema reference - @link(url: "https://example.com/, import: "otherSchema::") + @link(url: "https://example.com/, import: ["otherSchema::"]) ``` Note: Future versions may support this. ##! Purpose -The role of a feature referenced with {@link}. +The role of a schema referenced with {@link}. -This is not intended to be an exhaustive list of all the purposes a feature might serve. Rather, it is intended to capture cases where the default fail-open behavior of core schema consumers is undesirable. +This is not intended to be an exhaustive list of all the purposes a foreign schema or its metadata might serve. Rather, it is intended to capture cases where the default fail-open behavior of core schema consumers is undesirable. -Note we'll refer to directives from features which are `for: SECURITY` or `for: EXECUTION` as "`SECURITY` directives" and "`EXECUTION` directives", respectively. +Note we'll refer to directives from links which are `for: SECURITY` or `for: EXECUTION` as "`SECURITY` directives" and "`EXECUTION` directives", respectively. ###! SECURITY -`SECURITY` links provide metadata necessary to securely resolve fields. For instance, a hypothetical {auth} feature may provide an {@auth} directive to flag fields which require authorization. If a data core does not support the {auth} feature and serves those fields anyway, these fields will be accessible without authorization, compromising security. +`SECURITY` links provide metadata necessary to securely resolve fields. For instance, a hypothetical {auth} schema may provide an {@auth} directive to flag fields which require authorization. If a data core does not support the {auth} schemas and serves those fields anyway, these fields will be accessible without authorization, compromising security. Security-conscious consumers MUST NOT serve a field if: - the schema definition has **any** unsupported SECURITY directives, @@ -471,7 +508,7 @@ More security-conscious consumers MAY choose to enhance these requirements. For ###! EXECUTION -`EXECUTION` features provide metadata necessary to correctly resolve fields. For instance, a hypothetical {ts} feature may provide a `@ts__resolvers` annotation which references a TypeScript module of field resolvers. A consumer which does not support the {ts} feature will be unable to correctly resolve such fields. +`EXECUTION` schemas provide metadata necessary to correctly resolve fields. For instance, a hypothetical {ts} schemas may provide a `@ts__resolvers` annotation which references a TypeScript module of field resolvers. A consumer which does not support the {ts} schemas will be unable to correctly resolve such fields. Consumers MUST NOT serve a field if: - the schema's definition has **any** unsupported EXECUTION directives, @@ -479,7 +516,7 @@ Consumers MUST NOT serve a field if: - the field's return type definition has **any** unsupported EXECUTION directives, or - the field definition has **any** unsupported EXECUTION directives -Such fields are *unresolvable*. Consumers MAY attempt to serve schemas with unresolvable fields. Depending on the needs of the consumer, unresolvable fields MAY be removed from the schema prior to serving, or they MAY produce runtime errors if a query attempts to resolve them. Consumers MAY implement stricter policies, wholly refusing to serve schemas with unresolvable fields, or even refusing to serve schemas with any unsupported EXECUTION features, even if those features are never used in the schema. +Such fields are *unresolvable*. Consumers MAY attempt to serve schemas with unresolvable fields. Depending on the needs of the consumer, unresolvable fields MAY be removed from the schema prior to serving, or they MAY produce runtime errors if a query attempts to resolve them. Consumers MAY implement stricter policies, wholly refusing to serve schemas with unresolvable fields, or even refusing to serve schemas with any unsupported EXECUTION schemas, even if those schemas are never used in the schema. # Appendix: Validations & Algorithms @@ -551,7 +588,7 @@ BindingsFromLink(directive) : 1. **If** {as} is not a valid GraphQL identifier, 1. **Report** ❌ BadImportTypeMismatch 2. **Continue** - 3. ...**Else Emit** (Type({as}), Binding(gref: GRef({url}, Type({name})), implicit: {false})) + 2. ...**Else Emit** (Type({as}), Binding(gref: GRef({url}, Type({name})), implicit: {false})) ## Detecting a bootstrap directive diff --git a/tag/v0.1 b/tag/v0.1 deleted file mode 160000 index 7499bee..0000000 --- a/tag/v0.1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7499bee6709a10da5aaad8949fc9a8cd4bd1d57e diff --git a/tag/v0.1/index.html b/tag/v0.1/index.html new file mode 100644 index 0000000..a7ee498 --- /dev/null +++ b/tag/v0.1/index.html @@ -0,0 +1,1155 @@ + + + + +Tag + + + + + +
+
+

Tag

+
+

for tagging GraphQL schema elements with names

+ + + +
StatusRelease
Version0.1
+ + +

This document defines a core feature named tag for labeling schema elements with arbitrary names (or tags).

+

This specification provides machinery to apply arbitrary tags to schema elements via the application of @tag directive usages. Tags can be applied to field, object, interface, and union definitions.

+
+ +
+
+

1How to read this document

+

This document uses RFC 2119 guidance regarding normative terms: MUST / MUST NOT / REQUIRED / SHALL / SHALL NOT / SHOULD / SHOULD NOT / RECOMMENDED / MAY / OPTIONAL.

+
+

1.1What this document isn't

+

This document specifies only the definition of the @tag directive. Tags have a number of useful applications including metadata and schema processing, none of which are specified within this document.

+
+
+
+

2Example: Team Ownership Metadata

+

The following example demonstrates how team ownership over types and fields can be declaratively expressed via inline metadata. One might imagine a CI workflow which analyzes a schema diff and uses @tag names to authorize or require approval for changes to parts of the graph.

+
Example № 1
directive @tag(name: String!) repeatable on 
+  | FIELD_DEFINITION
+  | INTERFACE
+  | OBJECT
+  | UNION
+
+schema
+  @core(feature: "https://specs.apollo.dev/core/v0.2")
+  @core(feature: "https://specs.apollo.dev/tag/v0.1") {
+  query: Query
+}
+
+type Query {
+  customer(id: String!): Customer @tag(name: "team-customers")
+  employee(id: String!): Employee @tag(name: "team-admin")
+}
+
+interface User @tag(name: "team-accounts") {
+  id: String!
+  name: String!
+}
+
+type Customer implements User @tag(name: "team-customers") {
+  id: String!
+  name: String!
+  cart: [Product!] @tag(name: "team-shopping-cart")
+}
+
+type Employee implements User @tag(name: "team-admin") {
+  id: String!
+  name: String!
+  ssn: String!
+}
+
+
+
+

3Overview

+

This section is non‐normative. It describes the motivation behind the directives defined by this specification.

+

The @tag directive is, in its simplest form, a mechanism for applying arbitrary string metadata to the fields and types of a schema. This metadata is potentially useful throughout the schema’s lifecycle, including, but not limited to, processing, static analysis, and documentation.

+
+
+

4Basic Requirements

+

A schema which implements the @tag spec MUST provide a definition which is compatible with the definition below:

+
directive @tag(name: String!) repeatable on
+  | FIELD_DEFINITION
+  | INTERFACE
+  | OBJECT
+  | UNION
+
+
+
+ + +
+ + +
+ + + diff --git a/tag/v0.1/ownership-example.graphql b/tag/v0.1/ownership-example.graphql new file mode 100644 index 0000000..57fdeaf --- /dev/null +++ b/tag/v0.1/ownership-example.graphql @@ -0,0 +1,33 @@ +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION + +schema + @core(feature: "https://specs.apollo.dev/core/v0.2") + @core(feature: "https://specs.apollo.dev/tag/v0.1") { + query: Query +} + +type Query { + customer(id: String!): Customer @tag(name: "team-customers") + employee(id: String!): Employee @tag(name: "team-admin") +} + +interface User @tag(name: "team-accounts") { + id: String! + name: String! +} + +type Customer implements User @tag(name: "team-customers") { + id: String! + name: String! + cart: [Product!] @tag(name: "team-shopping-cart") +} + +type Employee implements User @tag(name: "team-admin") { + id: String! + name: String! + ssn: String! +} diff --git a/tag/v0.1/spec.graphql b/tag/v0.1/spec.graphql new file mode 100644 index 0000000..478c4ea --- /dev/null +++ b/tag/v0.1/spec.graphql @@ -0,0 +1,5 @@ +directive @tag(name: String!) repeatable on + | FIELD_DEFINITION + | INTERFACE + | OBJECT + | UNION diff --git a/tag/v0.1/tag-v0.1.md b/tag/v0.1/tag-v0.1.md new file mode 100644 index 0000000..3bef628 --- /dev/null +++ b/tag/v0.1/tag-v0.1.md @@ -0,0 +1,42 @@ +# Tag + +

for tagging GraphQL schema elements with names

+ +```raw html + + + +
StatusRelease
Version0.1
+ + +``` + +This document defines a [core feature](https://specs.apollo.dev/core) named `tag` for labeling schema elements with arbitrary names (or tags). + +This specification provides machinery to apply arbitrary tags to schema elements via the application of `@tag` directive usages. Tags can be applied to field, object, interface, and union definitions. + +# How to read this document + +This document uses [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) guidance regarding normative terms: MUST / MUST NOT / REQUIRED / SHALL / SHALL NOT / SHOULD / SHOULD NOT / RECOMMENDED / MAY / OPTIONAL. + +## What this document isn't + +This document specifies only the definition of the `@tag` directive. Tags have a number of useful applications including metadata and schema processing, none of which are specified within this document. + +# Example: Team Ownership Metadata + +The following example demonstrates how team ownership over types and fields can be declaratively expressed via inline metadata. One might imagine a CI workflow which analyzes a schema diff and uses `@tag` names to authorize or require approval for changes to parts of the graph. + +:::[example](ownership-example.graphql) + +# Overview + +*This section is non-normative.* It describes the motivation behind the directives defined by this specification. + +The `@tag` directive is, in its simplest form, a mechanism for applying arbitrary string metadata to the fields and types of a schema. This metadata is potentially useful throughout the schema's lifecycle, including, but not limited to, processing, static analysis, and documentation. + +# Basic Requirements + +A schema which implements the `@tag` spec MUST provide a definition which is compatible with the definition below: + +:::[definition](spec.graphql) \ No newline at end of file