Add federation example

This commit is contained in:
sunli 2020-04-10 10:20:43 +08:00
parent f1d0b3f641
commit c6dfe06ef9
13 changed files with 5055 additions and 3 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
Cargo.lock
.idea
.DS_Store
node_modules

View File

@ -331,8 +331,8 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
keys.push(name);
use_keys.push(ident);
}
add_keys.push(quote! { registry.add_keys(&#entity_type::type_name(), #keys_str); });
create_entity_types.push(quote! { #entity_type::create_type_info(registry); });
add_keys.push(quote! { registry.add_keys(&<#entity_type as #crate_name::Type>::type_name(), #keys_str); });
create_entity_types.push(quote! { <#entity_type as #crate_name::Type>::create_type_info(registry); });
let field_ident = &method.sig.ident;
let ctx_param = if arg_ctx {
@ -352,7 +352,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
find_entities.push((
args.len(),
quote! {
if typename == &#entity_type::type_name() {
if typename == &<#entity_type as #crate_name::Type>::type_name() {
if let (#(#key_pat),*) = (#(#key_getter),*) {
let ctx_obj = ctx.with_selection_set(&ctx.selection_set);
return #crate_name::OutputValueType::resolve(&#do_find, &ctx_obj, pos).await;

View File

@ -0,0 +1,67 @@
use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{playground_source, GQLRequest, GQLResponse};
use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema, SimpleObject, ID};
use futures::TryFutureExt;
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
#[SimpleObject]
struct User {
#[field]
id: ID,
#[field]
username: String,
}
struct Query;
#[Object(extends)]
impl Query {
#[field]
async fn me(&self) -> User {
User {
id: "1234".into(),
username: "Me".to_string(),
}
}
#[entity]
async fn find_user_by_id(&self, id: ID) -> User {
let username = if id == "1234" {
"Me".to_string()
} else {
format!("User {}", id)
};
User { id, username }
}
}
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
web::Json(GQLResponse(
futures::future::ready(req.into_inner().into_query_builder(&s))
.and_then(|builder| builder.execute())
.await,
))
}
async fn gql_playgound() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source("/", None))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
HttpServer::new(move || {
App::new()
.data(schema.clone())
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
})
.bind("127.0.0.1:4001")?
.run()
.await
}

View File

@ -0,0 +1,87 @@
use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{playground_source, GQLRequest, GQLResponse};
use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject};
use futures::TryFutureExt;
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
#[SimpleObject]
struct Product {
#[field]
upc: String,
#[field]
name: String,
#[field]
price: i32,
}
struct Query;
#[Object(extends)]
impl Query {
#[field]
async fn top_products<'a>(&self, ctx: &'a Context<'_>) -> &'a Vec<Product> {
ctx.data::<Vec<Product>>()
}
#[entity]
async fn find_product_by_upc<'a>(
&self,
ctx: &'a Context<'_>,
upc: String,
) -> Option<&'a Product> {
let hats = ctx.data::<Vec<Product>>();
hats.iter().find(|product| product.upc == upc)
}
}
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
web::Json(GQLResponse(
futures::future::ready(req.into_inner().into_query_builder(&s))
.and_then(|builder| builder.execute())
.await,
))
}
async fn gql_playgound() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source("/", None))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let hats = vec![
Product {
upc: "top-1".to_string(),
name: "Trilby".to_string(),
price: 11,
},
Product {
upc: "top-2".to_string(),
name: "Fedora".to_string(),
price: 22,
},
Product {
upc: "top-3".to_string(),
name: "Boater".to_string(),
price: 33,
},
];
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.data(hats)
.finish();
HttpServer::new(move || {
App::new()
.data(schema.clone())
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
})
.bind("127.0.0.1:4002")?
.run()
.await
}

View File

@ -0,0 +1,130 @@
use actix_web::{guard, web, App, HttpResponse, HttpServer};
use async_graphql::http::{playground_source, GQLRequest, GQLResponse};
use async_graphql::{Context, EmptyMutation, EmptySubscription, Object, Schema, SimpleObject, ID};
use futures::TryFutureExt;
type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
struct User {
id: ID,
}
#[Object(extends)]
impl User {
#[field(external)]
async fn id(&self) -> &ID {
&self.id
}
#[field]
async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> {
let reviews = ctx.data::<Vec<Review>>();
reviews
.iter()
.filter(|review| review.author.id == self.id)
.collect()
}
}
struct Product {
upc: String,
}
#[Object(extends)]
impl Product {
#[field(external)]
async fn upc(&self) -> &String {
&self.upc
}
#[field]
async fn reviews<'a>(&self, ctx: &'a Context<'_>) -> Vec<&'a Review> {
let reviews = ctx.data::<Vec<Review>>();
reviews
.iter()
.filter(|review| review.product.upc == self.upc)
.collect()
}
}
#[SimpleObject]
struct Review {
#[field]
body: String,
#[field(provides = "username")]
author: User,
#[field]
product: Product,
}
struct Query;
#[Object]
impl Query {
#[entity]
async fn find_user_by_id<'a>(&self, id: ID) -> User {
User { id }
}
#[entity]
async fn find_product_by_upc<'a>(&self, upc: String) -> Product {
Product { upc }
}
}
async fn index(s: web::Data<MySchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
web::Json(GQLResponse(
futures::future::ready(req.into_inner().into_query_builder(&s))
.and_then(|builder| builder.execute())
.await,
))
}
async fn gql_playgound() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source("/", None))
}
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
let reviews = vec![
Review {
body: "A highly effective form of birth control.".into(),
author: User { id: "1234".into() },
product: Product {
upc: "top-1".to_string(),
},
},
Review {
body: "Fedoras are one of the most fashionable hats around and can look great with a variety of outfits.".into(),
author: User { id: "1234".into() },
product: Product {
upc: "top-1".to_string(),
},
},
Review {
body: "This is the last straw. Hat you will wear. 11/10".into(),
author: User { id: "7777".into() },
product: Product {
upc: "top-1".to_string(),
},
},
];
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.data(reviews)
.finish();
HttpServer::new(move || {
App::new()
.data(schema.clone())
.service(web::resource("/").guard(guard::Post()).to(index))
.service(web::resource("/").guard(guard::Get()).to(gql_playgound))
})
.bind("127.0.0.1:4003")?
.run()
.await
}

View File

@ -0,0 +1,19 @@
const { ApolloServer } = require('apollo-server');
const { ApolloGateway } = require("@apollo/gateway");
const gateway = new ApolloGateway({
serviceList: [
{ name: 'accounts', url: 'http://localhost:4001' },
{ name: 'products', url: 'http://localhost:4002' },
{ name: 'reviews', url: 'http://localhost:4003' }
],
});
const server = new ApolloServer({
gateway,
subscriptions: false,
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});

View File

@ -0,0 +1,10 @@
module.exports = {
testEnvironment: "node",
testMatch: ["<rootDir>/**/*-test.js"],
testPathIgnorePatterns: ["<rootDir>/node_modules/"],
moduleFileExtensions: ["js"],
modulePaths: ["<rootDir>/node_modules"],
// transform: {
// '^.+\\.jsx?$': 'babel-jest',
// },
};

View File

@ -0,0 +1,17 @@
{
"name": "gateway",
"version": "1.0.0",
"main": "index.js",
"dependencies": {
"@apollo/gateway": "^0.11.7",
"apollo-server": "^2.9.16",
"graphql": "^14.6.0"
},
"devDependencies": {
"apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8",
"apollo-link-http": "^1.5.16",
"jest": "^25.1.0",
"node-fetch": "^2.6.0"
}
}

File diff suppressed because it is too large Load Diff

25
examples/start-feceration.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
function cleanup {
kill "$ACCOUNTS_PID"
kill "$PRODUCTS_PID"
kill "$REVIEWS_PID"
}
trap cleanup EXIT
cargo build --example federation-accounts
cargo build --example federation-products
cargo build --example federation-reviews
cargo run --example federation-accounts &
ACCOUNTS_PID=$!
cargo run --example federation-products &
PRODUCTS_PID=$!
cargo run --example federation-reviews &
REVIEWS_PID=$!
sleep 3
node federation/index.js

View File

@ -43,12 +43,14 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
}
OperationDefinition::Query(query)
if query.name.is_none()
|| self.operation_name.is_none()
|| query.name.as_deref() == self.operation_name.as_deref() =>
{
return Some((&query.selection_set, &query.variable_definitions, true));
}
OperationDefinition::Mutation(mutation)
if mutation.name.is_none()
|| self.operation_name.is_none()
|| mutation.name.as_deref() == self.operation_name.as_deref() =>
{
return Some((
@ -59,6 +61,7 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
}
OperationDefinition::Subscription(subscription)
if subscription.name.is_none()
|| self.operation_name.is_none()
|| subscription.name.as_deref() == self.operation_name.as_deref() =>
{
return None;

View File

@ -410,6 +410,13 @@ impl Registry {
fn create_federation_fields<'a, I: Iterator<Item = &'a Field>>(sdl: &mut String, it: I) {
for field in it {
if field.name.starts_with("__") {
continue;
}
if field.name == "_service" || field.name == "_entities" {
continue;
}
write!(sdl, "\t{}: {}", field.name, field.ty).ok();
if field.external {
write!(sdl, " @external").ok();

View File

@ -45,6 +45,12 @@ impl From<usize> for ID {
}
}
impl PartialEq<&str> for ID {
fn eq(&self, other: &&str) -> bool {
self.0.as_str() == *other
}
}
impl Scalar for ID {
fn type_name() -> &'static str {
"ID"