diff --git a/docs-website/docs.json b/docs-website/docs.json index d57f6ed8ab..f2a6a175cf 100644 --- a/docs-website/docs.json +++ b/docs-website/docs.json @@ -410,6 +410,8 @@ "icon": "sign-post", "pages": [ "federation/federation-directives-index", + "federation/directives/deprecated", + "federation/directives/specifiedby", "federation/directives/key", "federation/directives/external", "federation/directives/provides", @@ -417,8 +419,10 @@ "federation/directives/shareable", "federation/directives/authenticated", "federation/directives/requiresscopes", + "federation/directives/connect__fieldresolver", "federation/directives/openfed__subscriptionfilter", - "federation/directives/openfed__configuredescription" + "federation/directives/openfed__configuredescription", + "federation/directives/openfed__requirefetchreasons" ] }, { diff --git a/docs-website/federation/directives/connect__fieldresolver.mdx b/docs-website/federation/directives/connect__fieldresolver.mdx new file mode 100644 index 0000000000..576acf7d82 --- /dev/null +++ b/docs-website/federation/directives/connect__fieldresolver.mdx @@ -0,0 +1,106 @@ +--- +title: "@connect__fieldResolver" +icon: "function" +description: "The @connect__fieldResolver directive turns a field into a dedicated gRPC RPC call with explicit parent context." +--- + +## Definition + +```graphql +directive @connect__fieldResolver(context: connect__FieldSet!) on FIELD_DEFINITION +``` + +## Arguments + +| Argument | Type | Default | Description | +| --------- | -------------------- | ------- | ------------------------------------------------------------------------------------------ | +| `context` | `connect__FieldSet!` | — | A space-separated list of sibling fields from the parent type to pass as resolver context. | + +## Overview + +`@connect__fieldResolver` is part of the [Cosmo Connect](/connect/intro) gRPC integration. +It marks a field so that the router resolves it through a separate RPC call +instead of including it in the parent type's response. + +Without this directive, +all fields on a type are resolved together in a single RPC call. +This is efficient when every field comes from the same data source, +but not when some fields are expensive to compute or served by a different backend. +For example, +`id` and `price` on a `Product` type might come from a local database, +while `shippingEstimate` requires a call to an external shipping service +with higher latency. +By annotating `shippingEstimate` with `@connect__fieldResolver`, +the router only calls the shipping service when the client actually requests that field. + +The `context` argument specifies which fields from the parent type the resolver needs. +These fields are fetched first, +then passed to a generated `Resolve{TypeName}{FieldName}` RPC method. +The router batches all context entries into a single call using a `repeated` message, +eliminating N+1 problems. + +For a complete guide including protobuf generation, +Go implementation examples, +and batching behavior, +see [Field Resolvers](/router/gRPC/field-resolvers). + +## Example + +```graphql +type Product @key(fields: "id") { + id: ID! + name: String! + price: Float! + shippingEstimate(zip: String!): Float! + @connect__fieldResolver(context: "id price") +} +``` + +In this example, +the router first resolves `id` and `price` from the parent RPC. +It then calls `ResolveProductShippingEstimate` with those values as context +and `zip` as a field argument. + +## Generated protobuf + +For the `shippingEstimate` field in the example above, +the protographic tooling generates: + +```protobuf +service ProductService { + rpc ResolveProductShippingEstimate(ResolveProductShippingEstimateRequest) + returns (ResolveProductShippingEstimateResponse) {} +} + +message ResolveProductShippingEstimateArgs { + string zip = 1; +} + +message ResolveProductShippingEstimateContext { + string id = 1; + double price = 2; +} + +message ResolveProductShippingEstimateRequest { + repeated ResolveProductShippingEstimateContext context = 1; + ResolveProductShippingEstimateArgs field_args = 2; +} + +message ResolveProductShippingEstimateResult { + double shipping_estimate = 1; +} + +message ResolveProductShippingEstimateResponse { + repeated ResolveProductShippingEstimateResult result = 1; +} +``` + +The `context` message contains the fields listed in the directive (`id` and `price`). +Because the field has a GraphQL argument (`zip`), +an `Args` message and `field_args` field are added to the request. + +## See also + +- [Field Resolvers guide](/router/gRPC/field-resolvers) for implementation details and batching behavior. +- [`@requires`](/federation/directives/requires) for declaring cross-subgraph field dependencies. +- [Cosmo Connect](/connect/intro) for the full gRPC integration overview. diff --git a/docs-website/federation/directives/deprecated.mdx b/docs-website/federation/directives/deprecated.mdx new file mode 100644 index 0000000000..2ac97ee351 --- /dev/null +++ b/docs-website/federation/directives/deprecated.mdx @@ -0,0 +1,89 @@ +--- +title: "@deprecated" +icon: "triangle-exclamation" +description: "The @deprecated directive marks fields, arguments, input fields, and enum values as deprecated in the schema." +--- + +## Definition + +```graphql +directive @deprecated(reason: String = "No longer supported") on + FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE +``` + +## Arguments + +| Argument | Type | Default | Description | +| -------- | -------- | ---------------------- | ----------------------------------------------------------------- | +| `reason` | `String` | `"No longer supported"` | Explains why the element is deprecated and what to use instead. | + +## Overview + +`@deprecated` is a built-in GraphQL directive defined in the +[GraphQL specification](https://spec.graphql.org/October2021/#sec--deprecated). +It signals to API consumers that a schema element should no longer be used. + +Deprecated elements remain fully functional. +The directive is a documentation and tooling signal, +not a removal mechanism. +GraphQL IDEs such as GraphiQL display deprecation warnings, +and introspection exposes `isDeprecated` and `deprecationReason` fields +so tools can surface this information automatically. + +### Supported locations + +- **Field definitions** on object and interface types. +- **Argument definitions** on fields and directives. +- **Input field definitions** on input object types. +- **Enum values**. + +## Examples + +### Deprecating a field + +```graphql +type Product { + id: ID! + name: String! + sku: String! @deprecated(reason: "Use `id` instead.") +} +``` + +### Deprecating an enum value + +```graphql +enum Status { + ACTIVE + INACTIVE @deprecated(reason: "Use ARCHIVED.") + ARCHIVED +} +``` + +### Deprecating a field argument + +```graphql +type Query { + users( + limit: Int + first: Int @deprecated(reason: "Use `limit`.") + ): [User!]! +} +``` + +### Deprecating an input field + +```graphql +input CreateUserInput { + name: String! + username: String @deprecated(reason: "Usernames are auto-generated.") +} +``` + +## Federation behavior + +During composition, +if the same field or enum value is marked `@deprecated` in multiple subgraphs with different reasons, +Cosmo keeps the longest reason string. + +`@deprecated` is preserved in both the client-facing schema and the router schema. +It appears in introspection responses so that consumers can detect deprecated usage. diff --git a/docs-website/federation/directives/openfed__requirefetchreasons.mdx b/docs-website/federation/directives/openfed__requirefetchreasons.mdx new file mode 100644 index 0000000000..c0e5b29592 --- /dev/null +++ b/docs-website/federation/directives/openfed__requirefetchreasons.mdx @@ -0,0 +1,153 @@ +--- +title: "@openfed requireFetchReasons" +icon: "magnifying-glass-chart" +description: "The @openfed__requireFetchReasons directive enables fetch reason propagation to subgraphs for debugging and observability." +--- + +## Definition + +```graphql +directive @openfed__requireFetchReasons repeatable on + FIELD_DEFINITION | INTERFACE | OBJECT +``` + +## Overview + +`@openfed__requireFetchReasons` is a marker directive that tells the router to include +fetch reason metadata in subgraph requests for the annotated fields. + +In a federated graph, +a field may be fetched for several reasons: +the client requested it, +another subgraph depends on it via `@key` or `@requires`, +or it is part of an entity key. +By default, +the subgraph has no visibility into why a field is being fetched. +This directive enables that visibility by adding a `fetch_reasons` extension +to the subgraph request body. + +This is particularly useful for compliance-sensitive data. +In a federated graph, +any subgraph can use `@requires` to declare a dependency on a field from another subgraph. +When that happens, +the router fetches the field automatically — no explicit permission from the providing subgraph is needed. +For fields containing regulated or personally identifiable information, +this means another team's subgraph could gain access to protected data +without an explicit handshake. + +With `@openfed__requireFetchReasons`, +the providing subgraph can inspect every incoming request to see whether a field +was requested by the end user or by another subgraph, +and which subgraph it was. +This allows the subgraph to maintain an allow list of subgraphs +that are permitted to access specific fields, +ensuring the data owner stays compliant with regulations. + +## Router configuration + +Fetch reason propagation must be enabled in the router configuration: + +```yaml +engine: + enable_require_fetch_reasons: true +``` + +Environment variable: `ENGINE_ENABLE_REQUIRE_FETCH_REASONS` (default: `false`). + +Without this setting, +the directive has no effect at runtime. + +## Subgraph request format + +When enabled, +the router adds a `fetch_reasons` array to `extensions` in the subgraph request: + +```json +{ + "query": "...", + "variables": {}, + "extensions": { + "fetch_reasons": [ + { + "typename": "User", + "field": "id", + "by_user": true + }, + { + "typename": "User", + "field": "email", + "by_subgraphs": ["account"], + "is_requires": true + } + ] + } +} +``` + +Each entry includes: + +| Field | Type | Description | +| -------------- | ---------- | ------------------------------------------------------------------ | +| `typename` | `String` | The type containing the field. | +| `field` | `String` | The field name. | +| `by_user` | `Boolean` | `true` if the client query explicitly requested this field. | +| `by_subgraphs` | `[String]` | Subgraph names whose `@key` or `@requires` caused this fetch. | +| `is_key` | `Boolean` | `true` if the field is fetched as part of an entity key. | +| `is_requires` | `Boolean` | `true` if the field is fetched to satisfy a `@requires` dependency. | + +Only fields annotated with `@openfed__requireFetchReasons` +(directly or through type-level inheritance) +have their reasons included. +Other fields in the same request are not tracked. + +## Inheritance and scoping + +When applied to an object or interface type, +the directive is inherited by all fields defined in that same declaration block. + +```graphql +# All fields in this block inherit the directive +type User @openfed__requireFetchReasons { + id: ID! + name: String! + email: String! +} + +# Fields in a separate extension do NOT inherit it +extend type User { + avatar: String! +} +``` + +To mark only specific fields, +apply the directive at the field level: + +```graphql +type User { + id: ID! + name: String! + email: String! @openfed__requireFetchReasons +} +``` + +Fields in `extend` blocks require their own annotation +or a type-level directive on the extension: + +```graphql +extend type User @openfed__requireFetchReasons { + phone: String! +} +``` + +## Use cases + +- **Debugging**: Inspect why a subgraph is receiving certain fields in its requests. +- **Observability**: Log or metric fetch reasons in subgraph middleware to track + cross-subgraph dependencies. +- **Conditional logic**: Subgraphs can optimize responses based on whether a field + was requested by the client or by the federation engine. + +## See also + +- [`@requires`](/federation/directives/requires) for declaring cross-subgraph field dependencies. +- [`@key`](/federation/directives/key) for defining entity keys. diff --git a/docs-website/federation/directives/specifiedby.mdx b/docs-website/federation/directives/specifiedby.mdx new file mode 100644 index 0000000000..4b418e5260 --- /dev/null +++ b/docs-website/federation/directives/specifiedby.mdx @@ -0,0 +1,62 @@ +--- +title: "@specifiedBy" +icon: "link" +description: "The @specifiedBy directive links a custom scalar type to a human-readable specification of its format." +--- + +## Definition + +```graphql +directive @specifiedBy(url: String!) on SCALAR +``` + +## Arguments + +| Argument | Type | Default | Description | +| -------- | --------- | ------- | -------------------------------------------------------------------------- | +| `url` | `String!` | — | A URL pointing to a human-readable specification for the custom scalar. | + +## Overview + +`@specifiedBy` is a built-in GraphQL directive defined in the +[GraphQL specification](https://spec.graphql.org/October2021/#sec--specifiedBy). +It provides a machine-readable link between a custom scalar type and the document +that describes its serialization, +coercion, +and validation rules. + +The URL is exposed through introspection via the `specifiedByURL` field on scalar types. +Tools and clients can use this to generate documentation or validate scalar values. + +This directive must not be applied to built-in scalar types +(`String`, `Int`, `Float`, `Boolean`, `ID`). + +## Examples + +### Linking a scalar to an RFC + +```graphql +scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") +``` + +### Linking to the GraphQL Scalars registry + +```graphql +scalar DateTime @specifiedBy(url: "https://scalars.graphql.org/andimarek/date-time") +``` + +### Custom format specification + +```graphql +scalar HexColor @specifiedBy(url: "https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color") +``` + +## Federation behavior + +In a federated graph, +every subgraph that defines the same custom scalar should use the same `@specifiedBy` URL. +Cosmo preserves the directive through composition so that introspection on the federated schema +returns the `specifiedByURL` for each annotated scalar. + +The router does not perform any runtime validation based on this directive. +It is purely a schema metadata directive for documentation and tooling. diff --git a/docs-website/federation/federation-directives-index.mdx b/docs-website/federation/federation-directives-index.mdx index 0456d35e59..930c6970e5 100644 --- a/docs-website/federation/federation-directives-index.mdx +++ b/docs-website/federation/federation-directives-index.mdx @@ -10,8 +10,10 @@ This page lists the GraphQL directives currently supported by WunderGraph Cosmo. | Directive | Explanation | | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`@deprecated`](/federation/directives/deprecated) | Marks fields, arguments, input fields, and enum values as deprecated. The optional `reason` argument describes what to use instead. Deprecated elements remain functional; the directive is a documentation and tooling signal exposed through introspection. | | `@oneOf` | This built-in directive is used within the type system definition language to indicate an Input Object is a OneOf Input Object. See [specification](https://spec.graphql.org/September2025/#sec-OneOf-Input-Objects) for details. Requires [router 0.259.0](https://github.com/wundergraph/cosmo/releases/tag/router%400.259.0) and [wgc 0.93.3](https://github.com/wundergraph/cosmo/releases/tag/wgc%400.93.3). | | [`@semanticNonNull`](/federation/directives/semanticnonnull) | Indicates that a position is semantically non null: it is only null if there is a matching error in the `errors` array. In all other cases, the position is non-null. Requires [wgc 0.93.1](https://github.com/wundergraph/cosmo/releases/tag/wgc%400.93.1). | +| [`@specifiedBy`](/federation/directives/specifiedby) | Links a custom scalar type to a human-readable specification URL describing its format, serialization, and coercion rules. The URL is exposed through introspection via the `specifiedByURL` field. Must not be applied to built-in scalars. | ### Federation @@ -19,6 +21,7 @@ This page lists the GraphQL directives currently supported by WunderGraph Cosmo. | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [`@authenticated`](/federation/directives/authenticated) | Marks a field or type as requiring authentication. Only requests with valid credentials can access it. | | `@composeDirective` (planned) | Tells the composition process to preserve a specific custom type system directive in the supergraph. | +| [`@connect__fieldResolver`](/federation/directives/connect__fieldresolver) | Marks a field so the router resolves it through a separate gRPC RPC call. The `context` argument specifies which parent fields to pass. Part of the [Cosmo Connect](/connect/intro) gRPC integration. See [Field Resolvers](/router/gRPC/field-resolvers) for the full guide. | | [`@edfs__kafkaPublish`](/federation/event-driven-federated-subscriptions/kafka#%40edfs-kafkapublish) | Marks a mutation field that publishes an event to a Kafka topic. Requires a topic and provider ID. | | [`@edfs__kafkaSubscribe`](/federation/event-driven-federated-subscriptions/kafka#%40edfs-kafkasubscribe) | Declares a subscription field that listens to one or more Kafka topics. Used in EDG Subscription fields. | | [`@edfs__natsPublish`](/federation/event-driven-federated-subscriptions/nats#%40edfs-natspublish) | Marks a mutation field that publishes an event to a NATS subject. Requires a subject and provider ID. | @@ -31,6 +34,7 @@ This page lists the GraphQL directives currently supported by WunderGraph Cosmo. | [`@key`](/federation/directives/key) | Declares one or more fields that uniquely identify an entity across subgraphs. It has two arguments. The `fields` argument is of type `fieldSet`, which represents a GraphQL selection set with omitted curly braces `{}`. The `resolvable` argument defaults to true. If `resolvable` is set to false, the subgraph will not be used to resolve fields for that entity, because it does not implement the entity reference resolver for the given key. The only exception is when one of the fields uses the `@requires` directive, and in that case, the planner may still route a query to the subgraph, but only the field resolver will run, not the entity reference resolver. | | `@link` | Used to reference an external specification and optionally import types or directives into a subgraph schema. For Cosmo, this directive is ignored and removed during composition. | | [`@openfed__configureDescription`](/federation/directives/openfed__configuredescription) | Allows controlling how and whether descriptions are propagated to the federated schema. Supports overrides, hiding, and composition on extensions. | +| [`@openfed__requireFetchReasons`](/federation/directives/openfed__requirefetchreasons) | Enables fetch reason propagation to subgraph requests for annotated fields. The router adds a `fetch_reasons` extension describing why each field is being fetched (by user, by subgraph dependency, as a key, or to satisfy `@requires`). Requires `enable_require_fetch_reasons: true` in router config. | | [`@openfed__subscriptionFilter`](/federation/directives/openfed__subscriptionfilter) | Applies a filter condition to a subscription field, restricting events based on field values or logical expressions. | | `@override` | Specifies that a field now resolves from the current subgraph instead of the one that previously defined it. | | [`@provides`](/federation/directives/provides) | Indicates that specific fields can be resolved from this subgraph, but only on the schema path where the directive is applied. The fields listed in the `fields` argument must be marked as `@external`. If a parent field of a nested external field is marked as `@external`, and there are sibling fields that are not external, those sibling fields will also be available for resolution on the parent external path. |