Skip to content

Commit

Permalink
feat(router): load config from tailcall subgraph
Browse files Browse the repository at this point in the history
  • Loading branch information
meskill committed Sep 18, 2024
1 parent 0be3fae commit 2ffb716
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 13 deletions.
1 change: 1 addition & 0 deletions generated/.tailcallrc.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,7 @@ enum LinkType {
Htpasswd
Jwks
Grpc
SubGraph
}

enum HttpVersion {
Expand Down
3 changes: 2 additions & 1 deletion generated/.tailcallrc.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -832,7 +832,8 @@
"Operation",
"Htpasswd",
"Jwks",
"Grpc"
"Grpc",
"SubGraph"
]
},
"Method": {
Expand Down
1 change: 1 addition & 0 deletions src/core/config/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub enum LinkType {
Htpasswd,

Check warning on line 25 in src/core/config/link.rs

View workflow job for this annotation

GitHub Actions / Run Formatter and Lint Check

Diff in /home/runner/work/tailcall/tailcall/src/core/config/link.rs
Jwks,
Grpc,
SubGraph
}

/// The @link directive allows you to import external resources, such as
Expand Down
27 changes: 16 additions & 11 deletions src/core/config/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ use rustls_pki_types::{
use url::Url;

Check warning on line 8 in src/core/config/reader.rs

View workflow job for this annotation

GitHub Actions / Run Formatter and Lint Check

Diff in /home/runner/work/tailcall/tailcall/src/core/config/reader.rs

use super::{ConfigModule, Content, Link, LinkType};
use crate::core::config::{Config, ConfigReaderContext, Source};
use crate::core::merge_right::MergeRight;
use crate::core::proto_reader::ProtoReader;
use crate::core::resource_reader::{Cached, Resource, ResourceReader};
use crate::core::rest::EndpointSet;

Check warning on line 14 in src/core/config/reader.rs

View workflow job for this annotation

GitHub Actions / Run Formatter and Lint Check

Diff in /home/runner/work/tailcall/tailcall/src/core/config/reader.rs
use crate::core::runtime::TargetRuntime;
use crate::core::{
config::{Config, ConfigReaderContext, Source},
federation::subgraph_reader::SubGraphReader,
};

/// Reads the configuration from a file or from an HTTP URL and resolves all
/// linked extensions to create a ConfigModule.
pub struct ConfigReader {
runtime: TargetRuntime,
resource_reader: ResourceReader<Cached>,
proto_reader: ProtoReader,
subgraph_reader: SubGraphReader,
}

impl ConfigReader {
Expand All @@ -29,7 +33,8 @@ impl ConfigReader {
Self {
runtime: runtime.clone(),
resource_reader: resource_reader.clone(),
proto_reader: ProtoReader::init(resource_reader, runtime),
proto_reader: ProtoReader::init(resource_reader.clone(), runtime),
subgraph_reader: SubGraphReader::new(resource_reader),
}
}

Expand All @@ -43,7 +48,6 @@ impl ConfigReader {
let links: Vec<Link> = config_module
.config()
.links
.clone()
.iter()
.filter_map(|link| {
if link.src.is_empty() {
Expand All @@ -58,7 +62,6 @@ impl ConfigReader {
}

let mut extensions = config_module.extensions().clone();
// let mut base_config = config_module.config().clone();

for link in links.iter() {
let path = Self::resolve_path(&link.src, parent_dir);
Expand All @@ -69,14 +72,12 @@ impl ConfigReader {
let content = source.content;

let config = Config::from_source(Source::detect(&source.path)?, &content)?;
config_module = config_module.merge_right(config.clone().into());
let link_config_module = self
// recursively resolve links in the linked config
.ext_links(ConfigModule::from(config), Path::new(&link.src).parent())
.await?;

if !config.links.is_empty() {
let cfg_module = self
.ext_links(ConfigModule::from(config), Path::new(&link.src).parent())
.await?;
config_module = config_module.merge_right(cfg_module.clone());
}
config_module = config_module.merge_right(link_config_module);
}
LinkType::Protobuf => {
let meta = self.proto_reader.read(path).await?;
Expand Down Expand Up @@ -129,6 +130,10 @@ impl ConfigReader {
extensions.add_proto(m);
}

Check warning on line 131 in src/core/config/reader.rs

View workflow job for this annotation

GitHub Actions / Run Formatter and Lint Check

Diff in /home/runner/work/tailcall/tailcall/src/core/config/reader.rs
}
LinkType::SubGraph => {
let subgraph_config_module = self.subgraph_reader.fetch(link.src.as_str()).await?;
config_module = config_module.merge_right(subgraph_config_module);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/core/federation/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod subgraph_reader;
42 changes: 42 additions & 0 deletions src/core/federation/subgraph_reader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use serde::{Deserialize, Serialize};

Check warning on line 1 in src/core/federation/subgraph_reader.rs

View workflow job for this annotation

GitHub Actions / Run Formatter and Lint Check

Diff in /home/runner/work/tailcall/tailcall/src/core/federation/subgraph_reader.rs
use url::Url;

use crate::core::{
config::{Config, ConfigModule},
resource_reader::{Cached, ResourceReader},
valid::Validator,
};

#[derive(Clone)]
pub struct SubGraphReader {
reader: ResourceReader<Cached>,
}

#[derive(Serialize, Deserialize)]
struct AdminQuery {
config: String,
}

impl SubGraphReader {
pub fn new(reader: ResourceReader<Cached>) -> Self {
Self { reader }
}

pub async fn fetch(&self, src: &str) -> anyhow::Result<ConfigModule> {
let url = Url::parse(&format!("{src}/graphql"))?;

let mut request = reqwest::Request::new(reqwest::Method::POST, url);

let _ = request
.body_mut()
.insert(reqwest::Body::from(r#"{"query": "{ config }"}"#));

let file_read = self.reader.read_file(request).await?;
let admin_query: AdminQuery = serde_json::from_str(&file_read.content)?;

let config = Config::from_sdl(&admin_query.config).to_result()?;
let config_module = ConfigModule::from(config);

Ok(config_module)
}
}
1 change: 1 addition & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod transform;
pub mod try_fold;

Check warning on line 41 in src/core/mod.rs

View workflow job for this annotation

GitHub Actions / Run Formatter and Lint Check

Diff in /home/runner/work/tailcall/tailcall/src/core/mod.rs
pub mod valid;
pub mod worker;
pub mod federation;

// Re-export everything from `tailcall_macros` as `macros`
use std::borrow::Cow;
Expand Down
67 changes: 67 additions & 0 deletions tests/core/snapshots/federation-router.md_client.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
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 Post {
body: String!
id: Int!
title: String!
user: User
userId: Int!
}

type Query {
posts: [Post]
user(id: Int!): User
users: [User]
version: String
}

scalar UInt128

scalar UInt16

scalar UInt32

scalar UInt64

scalar UInt8

scalar Url

type User {
email: String!
id: Int!
name: String!
phone: String
username: String!
website: String
}

schema {
query: Query
}
35 changes: 35 additions & 0 deletions tests/core/snapshots/federation-router.md_merged.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
source: tests/core/spec.rs
expression: formatter
---
schema
@server(port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", batch: {delay: 100, headers: []}, httpCache: 42)
@link(src: "http://localhost:4000", meta: {name: "Users"}, type: SubGraph)
@link(src: "http://localhost:5000", meta: {name: "Posts"}, type: SubGraph) {
query: Query
}

type Post {
body: String!
id: Int!
title: String!
user: User @http(path: "/users/{{.value.userId}}")
userId: Int!
}

type Query {
posts: [Post] @http(path: "/posts")
user(id: Int!): User @http(path: "/users/{{.args.id}}")
users: [User] @http(path: "/users")
version: String @expr(body: "test")
}

type User {
email: String!
id: Int!
name: String!
phone: String
username: String!
website: String
}
1 change: 0 additions & 1 deletion tests/core/snapshots/test-nested-link.md_merged.snap
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ schema
@server
@upstream(baseURL: "http://jsonplaceholder.typicode.com")
@link(src: "graphql-with-link.graphql", type: Config)
@link(src: "link-enum.graphql", type: Config)
@link(src: "link-enum.graphql", type: Config) {
query: Query
}
Expand Down
74 changes: 74 additions & 0 deletions tests/execution/federation-router.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# Tailcall Federation router

```graphql @config
schema
@link(src: "http://localhost:4000", type: SubGraph, meta: {name: "Users"})
@link(src: "http://localhost:5000", type: SubGraph, meta: {name: "Posts"})
{
query: Query
}

type Query {
version: String @expr(body: "test")
}
```

```yml @mock
- request:
method: POST
url: http://localhost:4000/graphql
textBody: {"query": "{ config }"}
response:
status: 200
body:
config: |
schema
@server(port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: 42, batch: {delay: 100}) {
query: Query
}

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

type User {
id: Int!
name: String!
username: String!
email: String!
phone: String
website: String
}

type Post {
userId: Int!
user: User @http(path: "/users/{{.value.userId}}")
}

- request:
method: POST
url: http://localhost:5000/graphql
textBody: {"query": "{ config }"}
response:
status: 200
body:
config: |
schema
@server(port: 8000)
@upstream(baseURL: "http://jsonplaceholder.typicode.com", httpCache: 42, batch: {delay: 100}) {
query: Query
}

type Query {
posts: [Post] @http(path: "/posts")
}

type Post {
id: Int!
userId: Int!
title: String!
body: String!
}
```

0 comments on commit 2ffb716

Please sign in to comment.