Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions apollo-federation/src/connectors/expand/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,14 @@ mod helpers {
self.spec,
)?;

// If the return type is an object with no fields after walking
// the selection shape (e.g., mappingOnly with `$({})` for the
// namespace pattern), synthesize a dummy inaccessible field so
// the type is valid GraphQL.
if let TypeDefinitionPosition::Object(obj) = &field_type {
Self::ensure_type_not_empty(&mut schema, obj)?;
}

// Add the root type for this connector, optionally inserting a dummy query root
// if the connector is not defined within a field on a Query (since a subgraph is invalid
// without at least a root-level Query)
Expand Down Expand Up @@ -589,6 +597,13 @@ mod helpers {
.map_err(|_| FederationError::internal("error creating resolvable key"))?;

let Some(resolvable_key) = resolvable_key else {
// When an implicit entity resolver has no $this variables (e.g., the
// "namespace" pattern where a type acts as a grouping container), we use
// @key(fields: "__typename") as a singleton entity key. The query planner
// handles this correctly, fetching only __typename to identify the entity.
if matches!(connector.entity_resolver, Some(EntityResolver::Implicit)) {
return self.add_singleton_entity_key(parent_type_name, to_schema);
}
return self.copy_interface_object_keys(output_type_name, to_schema);
};

Expand Down Expand Up @@ -749,6 +764,58 @@ mod helpers {
Ok(())
}

/// Add a `@key(fields: "__typename")` directive to a type to make it a
/// singleton entity. This is used for the "namespace" pattern where a
/// connector field doesn't reference `$this` but still needs to be
/// resolvable as an entity by the query planner.
fn add_singleton_entity_key(
&self,
type_name: Name,
to_schema: &mut FederationSchema,
) -> Result<(), FederationError> {
let pos = ObjectTypeDefinitionPosition { type_name };
let key_directive = Directive {
name: self.key_name.clone(),
arguments: vec![Node::new(Argument {
name: name!("fields"),
value: Node::new(Value::String("__typename".to_string())),
})],
};
pos.insert_directive(to_schema, Component::new(key_directive))?;
Ok(())
}

/// If an object type exists in the schema but has no fields, add a dummy
/// `_: ID @inaccessible` field so it is valid GraphQL. This occurs for the
/// "namespace" pattern where a `mappingOnly` connector returns a type whose
/// fields all have their own connectors (expanded into separate subgraphs).
fn ensure_type_not_empty(
schema: &mut FederationSchema,
obj: &ObjectTypeDefinitionPosition,
) -> Result<(), FederationError> {
let type_def = obj.get(schema.schema())?;
if type_def.fields.is_empty() {
let field_pos = ObjectFieldDefinitionPosition {
type_name: obj.type_name.clone(),
field_name: name!("_"),
};
field_pos.insert(
schema,
Component::new(FieldDefinition {
description: None,
name: name!("_"),
arguments: Vec::new(),
ty: ty!(ID),
directives: ast::DirectiveList(vec![Node::new(Directive {
name: name!("federation__inaccessible"),
arguments: Vec::new(),
})]),
}),
)?;
}
Ok(())
}

/// Inserts a custom leaf type into the schema
fn insert_custom_leaf(
&self,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
schema
@link(url: "https://specs.apollo.dev/link/v1.0")
@link(url: "https://specs.apollo.dev/join/v0.5", for: EXECUTION)
@link(url: "https://specs.apollo.dev/connect/v0.4", for: EXECUTION)
@join__directive(graphs: [CONNECTORS], name: "link", args: {url: "https://specs.apollo.dev/connect/v0.4", import: ["@connect", "@source"]})
@join__directive(graphs: [CONNECTORS], name: "source", args: {name: "v1", http: {baseURL: "http://127.0.0.1"}})
{
query: Query
mutation: Mutation
}

directive @join__directive(graphs: [join__Graph!], name: String!, args: join__DirectiveArguments) repeatable on SCHEMA | OBJECT | INTERFACE | FIELD_DEFINITION

directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean, overrideLabel: String, contextArguments: [join__ContextArgument!]) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE

directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

input join__ContextArgument {
name: String!
type: String!
context: String!
selection: join__FieldValue!
}

scalar join__DirectiveArguments

scalar join__FieldSet

scalar join__FieldValue

enum join__Graph {
CONNECTORS @join__graph(name: "connectors", url: "none")
}

scalar link__Import

enum link__Purpose {
"""
`SECURITY` features provide metadata necessary to securely resolve fields.
"""
SECURITY

"""
`EXECUTION` features provide metadata necessary for operation execution.
"""
EXECUTION
}

type Query
@join__type(graph: CONNECTORS)
{
me: User @join__field(graph: CONNECTORS) @join__directive(graphs: [CONNECTORS], name: "connect", args: {source: "v1", http: {GET: "/me"}, selection: "id name"})
}

type User
@join__type(graph: CONNECTORS)
{
id: ID!
name: String
}

type Mutation
@join__type(graph: CONNECTORS)
{
user: UserMutations @join__field(graph: CONNECTORS) @join__directive(graphs: [CONNECTORS], name: "connect", args: {mappingOnly: true, selection: "$({})"})
}

type UserMutations
@join__type(graph: CONNECTORS)
{
update(id: ID!): String @join__field(graph: CONNECTORS) @join__directive(graphs: [CONNECTORS], name: "connect", args: {source: "v1", http: {POST: "/api/users/{$args.id}"}, selection: "$.result"})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: apollo-federation/src/connectors/expand/tests/mod.rs
expression: api_schema
input_file: apollo-federation/src/connectors/expand/tests/schemas/expand/namespace.graphql
---
directive @defer(label: String, if: Boolean! = true) on FRAGMENT_SPREAD | INLINE_FRAGMENT

type Query {
me: User
}

type User {
id: ID!
name: String
}

type Mutation {
user: UserMutations
}

type UserMutations {
update(id: ID!): String
}
Loading
Loading