Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add enableFederation flag to server #2919

Merged
merged 4 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ directive @server(
as nonce-based APIs.
"""
dedupe: Boolean
"""
`enableFederation` enables functionality to Tailcall server to act as a federation
subgraph.
"""
enableFederation: Boolean
enableJIT: Boolean
"""
`globalResponseTimeout` sets the maximum query duration before termination, acting
Expand Down
7 changes: 7 additions & 0 deletions generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,13 @@
"null"
]
},
"enableFederation": {
"description": "`enableFederation` enables functionality to Tailcall server to act as a federation subgraph.",
"type": [
"boolean",
"null"
]
},
"enableJIT": {
"type": [
"boolean",
Expand Down
9 changes: 9 additions & 0 deletions src/core/config/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ pub struct Server {
/// and operations. @default `true`.
pub introspection: Option<bool>,

/// `enableFederation` enables functionality to Tailcall server to act
/// as a federation subgraph.
#[serde(default, skip_serializing_if = "is_default")]
pub enable_federation: Option<bool>,

#[serde(default, skip_serializing_if = "is_default")]
/// `pipelineFlush` allows to control flushing behavior of the server
/// pipeline.
Expand Down Expand Up @@ -277,6 +282,10 @@ impl Server {
pub fn get_routes(&self) -> Routes {
self.routes.clone().unwrap_or_default()
}

pub fn get_enable_federation(&self) -> bool {
self.enable_federation.unwrap_or(false)
}
}

#[cfg(test)]
Expand Down
95 changes: 50 additions & 45 deletions src/core/config/transformer/subgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ impl Transform for Subgraph {
type Error = String;

fn transform(&self, mut config: Self::Value) -> Valid<Self::Value, Self::Error> {
if !config.server.get_enable_federation() {
// if federation is disabled don't process the config
return Valid::succeed(config);
}

let mut resolver_by_type = BTreeMap::new();

let valid = Valid::from_iter(config.types.iter_mut(), |(type_name, ty)| {
Expand All @@ -55,26 +60,6 @@ impl Transform for Subgraph {
return valid.map_to(config);
}

if resolver_by_type.is_empty() {
return Valid::succeed(config);
}

let entity_union = Union {
types: resolver_by_type.keys().cloned().collect(),
..Default::default()
};

let entity_resolver = config::EntityResolver { resolver_by_type };

// union that wraps any possible types for entities
config
.unions
.insert(UNION_ENTITIES_NAME.to_owned(), entity_union);
// any scalar for argument `representations`
config
.types
.insert(ENTITIES_TYPE_NAME.to_owned(), config::Type::default());

let service_field = Field { type_of: "String".to_string().into(), ..Default::default() };

let service_type = config::Type {
Expand All @@ -87,38 +72,15 @@ impl Transform for Subgraph {
.types
.insert(SERVICE_TYPE_NAME.to_owned(), service_type);

let query_type = match config.schema.query.as_ref() {
let query_type_name = match config.schema.query.as_ref() {
Some(name) => name,
None => {
config.schema.query = Some("Query".to_string());
"Query"
}
};

let query_type = config.types.entry(query_type.to_owned()).or_default();

let arg = Arg {
type_of: Type::from(ENTITIES_TYPE_NAME.to_string())
.into_required()
.into_list()
.into_required(),
..Default::default()
};

query_type.fields.insert(
ENTITIES_FIELD_NAME.to_string(),
Field {
type_of: Type::from(UNION_ENTITIES_NAME.to_owned())
.into_list()
.into_required(),
args: [(ENTITIES_ARG_NAME.to_owned(), arg)].into_iter().collect(),
doc: Some("Apollo federation Query._entities resolver".to_string()),
resolver: Some(Resolver::ApolloFederation(
ApolloFederation::EntityResolver(entity_resolver),
)),
..Default::default()
},
);
let query_type = config.types.entry(query_type_name.to_owned()).or_default();

query_type.fields.insert(
SERVICE_FIELD_NAME.to_string(),
Expand All @@ -130,6 +92,49 @@ impl Transform for Subgraph {
},
);

if !resolver_by_type.is_empty() {
let entity_union = Union {
types: resolver_by_type.keys().cloned().collect(),
..Default::default()
};

let entity_resolver = config::EntityResolver { resolver_by_type };

// union that wraps any possible types for entities
config
.unions
.insert(UNION_ENTITIES_NAME.to_owned(), entity_union);
// any scalar for argument `representations`
config
.types
.insert(ENTITIES_TYPE_NAME.to_owned(), config::Type::default());

let query_type = config.types.entry(query_type_name.to_owned()).or_default();

let arg = Arg {
type_of: Type::from(ENTITIES_TYPE_NAME.to_string())
.into_required()
.into_list()
.into_required(),
..Default::default()
};

query_type.fields.insert(
ENTITIES_FIELD_NAME.to_string(),
Field {
type_of: Type::from(UNION_ENTITIES_NAME.to_owned())
.into_list()
.into_required(),
args: [(ENTITIES_ARG_NAME.to_owned(), arg)].into_iter().collect(),
doc: Some("Apollo federation Query._entities resolver".to_string()),
resolver: Some(Resolver::ApolloFederation(
ApolloFederation::EntityResolver(entity_resolver),
)),
..Default::default()
},
);
}

Valid::succeed(config)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ expression: response
"body": {
"data": {
"_service": {
"sdl": "schema @server(port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @graphQL(args: [{key: \"id\", value: \"{{.value.id}}\"}], baseURL: \"http://upstream/graphql\", batch: true, name: \"post\") @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @http(batchKey: [\"id\"], path: \"/users\", query: [{key: \"id\", value: \"{{.value.id}}\"}]) @key(fields: \"id\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
"sdl": "schema @server(enableFederation: true, port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @graphQL(args: [{key: \"id\", value: \"{{.value.id}}\"}], baseURL: \"http://upstream/graphql\", batch: true, name: \"post\") @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @http(batchKey: [\"id\"], path: \"/users\", query: [{key: \"id\", value: \"{{.value.id}}\"}]) @key(fields: \"id\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source: tests/core/spec.rs
expression: formatter
---
schema
@server(port: 8000)
@server(enableFederation: true, port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42) {
query: Query
}
Expand Down
2 changes: 1 addition & 1 deletion tests/core/snapshots/apollo-federation-entities.md_1.snap
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ expression: response
"body": {
"data": {
"_service": {
"sdl": "schema @server(port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @expr(body: {id: \"{{.value.id}}\", title: \"post-title-{{.value.id}}\"}) @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @call(steps: [{query: \"user\", args: {id: \"{{.value.id}}\"}}]) @key(fields: \"id\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
"sdl": "schema @server(enableFederation: true, port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\nscalar _Any\n\nunion _Entity = Post | User\n\ntype Post @expr(body: {id: \"{{.value.id}}\", title: \"post-title-{{.value.id}}\"}) @key(fields: \"id\") {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._entities resolver\n \"\"\"\n _entities(representations: [_Any!]!): [_Entity]!\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User @call(steps: [{query: \"user\", args: {id: \"{{.value.id}}\"}}]) @key(fields: \"id\") {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ source: tests/core/spec.rs
expression: formatter
---
schema
@server(port: 8000)
@server(enableFederation: true, port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42)
@link(src: "./posts.graphql", type: Config) {
query: Query
Expand Down
13 changes: 13 additions & 0 deletions tests/core/snapshots/federation-subgraph-force-disabled.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {}
}
}
13 changes: 13 additions & 0 deletions tests/core/snapshots/federation-subgraph-force-disabled.md_1.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
source: tests/core/spec.rs
expression: formatted
---
scalar Bytes

scalar Date

scalar DateTime

scalar Email

scalar Empty

scalar Int128

scalar Int16

scalar Int32

scalar Int64

scalar Int8

scalar JSON

scalar PhoneNumber

type Query {
user(id: Int!): User
}

scalar UInt128

scalar UInt16

scalar UInt32

scalar UInt64

scalar UInt8

scalar Url

type User {
id: Int!
name: String!
}

schema {
query: Query
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: tests/core/spec.rs
expression: formatter
---
schema
@server(enableFederation: false, port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42) {
query: Query
}

type Post @expr(body: {id: "{{.value.id}}", title: "post-title-{{.value.id}}"}) {
id: Int!
title: String!
}

type Query {
user(id: Int!): User @http(path: "/users/{{.args.id}}")
}

type User @call(steps: [{query: "user", args: {id: "{{.value.id}}"}}]) {
id: Int!
name: String!
}
13 changes: 13 additions & 0 deletions tests/core/snapshots/federation-subgraph-force-enabled.md_0.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {}
}
}
17 changes: 17 additions & 0 deletions tests/core/snapshots/federation-subgraph-force-enabled.md_1.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: tests/core/spec.rs
expression: response
---
{
"status": 200,
"headers": {
"content-type": "application/json"
},
"body": {
"data": {
"_service": {
"sdl": "schema @server(enableFederation: true, port: 8000) @upstream(baseURL: \"http://jsonplaceholder.typicode.com\", batch: {delay: 100, headers: []}, httpCache: 42) {\n query: Query\n}\n\ntype Post {\n id: Int!\n title: String!\n}\n\ntype Query {\n \"\"\"\n Apollo federation Query._service resolver\n \"\"\"\n _service: _Service!\n user(id: Int!): User @http(path: \"/users/{{.args.id}}\")\n}\n\ntype User {\n id: Int!\n name: String!\n}\n\ntype _Service {\n sdl: String\n}\nextend schema @link(\n\turl: \"https://specs.apollo.dev/federation/v2.3\",\n\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]\n)\n"
}
}
}
}
Loading
Loading