graphql-hive / graphql-hive/federation-composition

support `@oneOf` directive

Open
#302 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
53
Forks
8
Avg merge
20h 50m
Merged PRs (30d)
10

Description

## Bug / Feature Request

`composeServices` from `@theguild/federation-composition` strips the standard GraphQL `@oneOf` directive (and other GraphQL-spec directives like `@specifiedBy`) during composition. Subgraphs that emit valid GraphQL with `@oneOf` on input types end up with a supergraph SDL where the directive is gone — both the definition and every application — leaving consumers with a plain `INPUT_OBJECT` that has lost its tagged-union semantics.

## Why this matters

`@oneOf` is part of the official GraphQL spec ([RFC merged Oct 2023](https://github.com/graphql/graphql-spec/pull/825)) and is the canonical way to model tagged-union inputs. It is not user-defined — every spec-conformant GraphQL implementation now supports it natively. Tools that emit GraphQL from other schema languages (e.g. OpenAPI loaders that map `oneOf` + `discriminator` to `@oneOf` input types) rely on the directive surviving composition so that downstream gateways can route discriminated input correctly.

Today, the only way to keep `@oneOf` alive through composition is to opt-in via Apollo Federation's `@composeDirective(name: "@oneOf")` mechanism, which itself requires:

1. Adding a matching `directive @oneOf on INPUT_OBJECT` definition to the subgraph SDL.
2. Adding a custom `@link(url: "...", import: ["@oneOf"])` to the schema definition.
3. Adding `@composeDirective(name: "@oneOf")` to the schema definition.

That's reasonable for genuinely user-defined directives. It is ceremonial overhead for a directive that the GraphQL spec already mandates.

## Where it gets stripped

`subgraph/state.js` ignores any directive application not in `composedDirectives`:

```js
// subgraph/state.js
Directive(node) {
if (composedDirectives.has(node.name.value)) {
// save directive into state
}
// else: silently dropped
}
```

`composedDirectives` is populated only by `compose-directive` validation when it sees `@composeDirective`:

```js
// subgraph/validation/rules/elements/compose-directive.js
context.stateBuilder.directive.setComposed(matchingDirective.name.value);
context.stateBuilder.composedDirectives.add(matchingDirective.name.value);
```

`compose.js` hardcodes the supergraph SDL header to only reference federation specs (`link`, `join`, `tag`, `inaccessible`, `policy`, `requiresScopes`, `authenticated`). There is no slot where `directive @oneOf on INPUT_OBJECT` would be added.

A grep across the whole package confirms zero references to `oneOf`. The composer has no concept of standard GraphQL directives beyond Federation's own.

## Repro

Minimal subgraph:

```graphql
schema { query: Query }
type Query { _empty: String }
directive @oneOf on INPUT_OBJECT
input CatVariant @oneOf {
small: SmallCat
big: BigCat
}
input SmallCat { weight: Int }
input BigCat { weight: Int, fierce: Boolean }
```

Run through `composeServices`. The supergraph SDL contains `input CatVariant { ... }` with no `@oneOf` directive applied and no `directive @oneOf` definition. A gateway that builds an executable schema from that SDL has no way to know `CatVariant` is a tagged union.

## Real-world impact

We hit this with `graphql-mesh` + `@omnigraph/openapi`. The OpenAPI loader correctly emits `@oneOf` on input types derived from `oneOf` schemas (see `getComposerFromJSONSchema.js`'s "oneOf without sibling properties" branch). The subgraph SDL is fine. But after `composeSubgraphs` (which delegates to this package), the directive is gone, and Mesh's runtime `resolveDataByUnionInputType` — which gates wrapper-key unwrapping on `getDirective(schema, type, 'oneOf')` — never fires. Wrapped input objects leak through to the REST upstream, where they get rejected.

We worked around it with a defensive unwrap shim on the upstream service. Functional, but the shim shouldn't have to exist; the directive marker is already in the subgraph and was deliberately dropped by the composer.

## Proposed fix

Auto-allowlist a small set of standard GraphQL directives that are part of the spec, no opt-in required:

- `@oneOf` (input tagged unions)
- `@specifiedBy` (scalar URL specifications)
- Possibly `@deferrable` / others as the spec evolves.

Sketch:

```js
// subgraph/state.js
const SPEC_DIRECTIVES = new Set(['oneOf', 'specifiedBy']);

// inside subgraph state setup:
for (const name of SPEC_DIRECTIVES) {
composedDirectives.add(name);
state.directives.setComposed(name);
}
```

…plus emitting the directive definitions in the supergraph SDL header when they are used (similar to `usedTagSpec` / `usedInaccessibleSpec` flags).

Alternative: detect `directive @oneOf on INPUT_OBJECT` in any subgraph SDL and treat it as auto-composed without requiring `@composeDirective`. The directive's location set is fixed by the spec, so no merge-conflict logic is needed.

## Environment

- `@theguild/federation-composition`: latest
- Composing OpenAPI-derived subgraphs via `@graphql-mesh/fusion-composition`
- Node 22

Happy to send a PR for `@oneOf` + `@specifiedBy` if the approach sounds reasonable.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.