Add federation example
This commit is contained in:
parent
f1d0b3f641
commit
c6dfe06ef9
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
node_modules
|
|
@ -331,8 +331,8 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
|
||||||
keys.push(name);
|
keys.push(name);
|
||||||
use_keys.push(ident);
|
use_keys.push(ident);
|
||||||
}
|
}
|
||||||
add_keys.push(quote! { registry.add_keys(&#entity_type::type_name(), #keys_str); });
|
add_keys.push(quote! { registry.add_keys(&<#entity_type as #crate_name::Type>::type_name(), #keys_str); });
|
||||||
create_entity_types.push(quote! { #entity_type::create_type_info(registry); });
|
create_entity_types.push(quote! { <#entity_type as #crate_name::Type>::create_type_info(registry); });
|
||||||
|
|
||||||
let field_ident = &method.sig.ident;
|
let field_ident = &method.sig.ident;
|
||||||
let ctx_param = if arg_ctx {
|
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((
|
find_entities.push((
|
||||||
args.len(),
|
args.len(),
|
||||||
quote! {
|
quote! {
|
||||||
if typename == &#entity_type::type_name() {
|
if typename == &<#entity_type as #crate_name::Type>::type_name() {
|
||||||
if let (#(#key_pat),*) = (#(#key_getter),*) {
|
if let (#(#key_pat),*) = (#(#key_getter),*) {
|
||||||
let ctx_obj = ctx.with_selection_set(&ctx.selection_set);
|
let ctx_obj = ctx.with_selection_set(&ctx.selection_set);
|
||||||
return #crate_name::OutputValueType::resolve(&#do_find, &ctx_obj, pos).await;
|
return #crate_name::OutputValueType::resolve(&#do_find, &ctx_obj, pos).await;
|
||||||
|
|
67
examples/federation-accounts.rs
Normal file
67
examples/federation-accounts.rs
Normal 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
|
||||||
|
}
|
87
examples/federation-products.rs
Normal file
87
examples/federation-products.rs
Normal 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
|
||||||
|
}
|
130
examples/federation-reviews.rs
Normal file
130
examples/federation-reviews.rs
Normal 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
|
||||||
|
}
|
19
examples/federation/index.js
Normal file
19
examples/federation/index.js
Normal 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}`);
|
||||||
|
});
|
10
examples/federation/jest.config.js
Normal file
10
examples/federation/jest.config.js
Normal 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',
|
||||||
|
// },
|
||||||
|
};
|
17
examples/federation/package.json
Normal file
17
examples/federation/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
4680
examples/federation/yarn.lock
Normal file
4680
examples/federation/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
25
examples/start-feceration.sh
Executable file
25
examples/start-feceration.sh
Executable 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
|
|
@ -43,12 +43,14 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
|
||||||
}
|
}
|
||||||
OperationDefinition::Query(query)
|
OperationDefinition::Query(query)
|
||||||
if query.name.is_none()
|
if query.name.is_none()
|
||||||
|
|| self.operation_name.is_none()
|
||||||
|| query.name.as_deref() == self.operation_name.as_deref() =>
|
|| query.name.as_deref() == self.operation_name.as_deref() =>
|
||||||
{
|
{
|
||||||
return Some((&query.selection_set, &query.variable_definitions, true));
|
return Some((&query.selection_set, &query.variable_definitions, true));
|
||||||
}
|
}
|
||||||
OperationDefinition::Mutation(mutation)
|
OperationDefinition::Mutation(mutation)
|
||||||
if mutation.name.is_none()
|
if mutation.name.is_none()
|
||||||
|
|| self.operation_name.is_none()
|
||||||
|| mutation.name.as_deref() == self.operation_name.as_deref() =>
|
|| mutation.name.as_deref() == self.operation_name.as_deref() =>
|
||||||
{
|
{
|
||||||
return Some((
|
return Some((
|
||||||
|
@ -59,6 +61,7 @@ impl<Query, Mutation, Subscription> QueryBuilder<Query, Mutation, Subscription>
|
||||||
}
|
}
|
||||||
OperationDefinition::Subscription(subscription)
|
OperationDefinition::Subscription(subscription)
|
||||||
if subscription.name.is_none()
|
if subscription.name.is_none()
|
||||||
|
|| self.operation_name.is_none()
|
||||||
|| subscription.name.as_deref() == self.operation_name.as_deref() =>
|
|| subscription.name.as_deref() == self.operation_name.as_deref() =>
|
||||||
{
|
{
|
||||||
return None;
|
return None;
|
||||||
|
|
|
@ -410,6 +410,13 @@ impl Registry {
|
||||||
|
|
||||||
fn create_federation_fields<'a, I: Iterator<Item = &'a Field>>(sdl: &mut String, it: I) {
|
fn create_federation_fields<'a, I: Iterator<Item = &'a Field>>(sdl: &mut String, it: I) {
|
||||||
for field in it {
|
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();
|
write!(sdl, "\t{}: {}", field.name, field.ty).ok();
|
||||||
if field.external {
|
if field.external {
|
||||||
write!(sdl, " @external").ok();
|
write!(sdl, " @external").ok();
|
||||||
|
|
|
@ -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 {
|
impl Scalar for ID {
|
||||||
fn type_name() -> &'static str {
|
fn type_name() -> &'static str {
|
||||||
"ID"
|
"ID"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user