[JavaScript, API, Rust, Микросервисы] GraphQL на Rust (перевод)
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
В этой статье я покажу как создать GraphQL сервер, используя Rust и его экосистему; будут приведены примеры реализации наиболее часто встречающихся задач при разработке GraphQL API. В итоге API трёх микросервисов будут объединены в единую точку доступа с помощью Apollo Server и Apollo Federation. Это позволит клиентам запрашивать данные одновременно из нескольких источников без необходимости знать какие данные приходят из какого сервиса.ВведениеОбзорС точки зрения функциональности описываемый проект довольно похож на представленный в моей предыдущей статье, но в этот раз с использованием стэка Rust. Архитектурно проекта выглядит так:
Каждый компонент архитектуры освещает несколько вопросов, которые могут возникнуть при реализации GraphQL API. Доменная модель включает данные о планетах Солнечной системы и их спутниках. Проект имеет многомодульную структуру (или монорепозиторий) и состоит из следующих модулей:
- planets-service (Rust)
- satellites-service (Rust)
- auth-service (Rust)
- apollo-server (JS)
Существуют две основных библиотеки для разработки GraphQL сервера на Rust: Juniper и Async-graphql, но только последняя поддерживает Apollo Federation, поэтому она была выбрана для реализации проекта (есть также открытый запрос на реализацию поддержки Federation в Juniper). Обе библиотеки предлагают code-first подход.Помимо этого использованы PostgreSQL — для реализации слоя данных, JWT — для аутентификации и Kafka — для асинхронного обмена сообщениями.Стэк технологийВ следующей таблице показан стэк основных технологий, использованных в проекте:ТипНазваниеСайтGitHubЯзык программированияRustlinklinkGraphQL библиотекаAsync-graphqllinklinkЕдиная GraphQL точка доступаApollo ServerlinklinkWeb фреймворкactix-weblinklinkБаза даныхPostgreSQLlinklinkБрокер сообщенийApache KafkalinklinkОркестрация контейнеровDocker ComposelinklinkТакже некоторые использованные Rust библиотеки:ТипНазваниеСайтGitHubORMDiesellinklinkKafka клиентrust-rdkafkalinklinkХэширование паролейargonauticalinklinkJWT библиотекаjsonwebtokenlinklinkБибилиотека для тестированияTestcontainers-rslinklinkНеобходимое ПОЧтобы запустить проект локально, вам нужен только Docker Compose. В противном случае вам может понадобиться следующее:
- Rust
- Diesel CLI (для установки выполните cargo install diesel_cli --no-default-features --features postgres)
- LLVM (это нужно для работы крэйта argonautica)
- CMake (это нужно для работы крэйта rust-rdkafka)
- PostgreSQL
- Apache Kafka
- npm
РеализацияВ Cargo.toml в корне проекта указаны три приложения и одна библиотека:Root Cargo.toml
[workspace]
members = [
"auth-service",
"planets-service",
"satellites-service",
"common-utils",
]
Начнём с planets-service.ЗависимостиCargo.toml выглядит так:Cargo.toml
[package]
name = "planets-service"
version = "0.1.0"
edition = "2018"
[dependencies]
common-utils = { path = "../common-utils" }
async-graphql = "2.4.3"
async-graphql-actix-web = "2.4.3"
actix-web = "3.3.2"
actix-rt = "1.1.1"
actix-web-actors = "3.0.0"
futures = "0.3.8"
async-trait = "0.1.42"
bigdecimal = { version = "0.1.2", features = ["serde"] }
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.60"
diesel = { version = "1.4.5", features = ["postgres", "r2d2", "numeric"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
strum = "0.20.0"
strum_macros = "0.20.1"
rdkafka = { version = "0.24.0", features = ["cmake-build"] }
async-stream = "0.3.0"
lazy_static = "1.4.0"
[dev-dependencies]
jsonpath_lib = "0.2.6"
testcontainers = "0.9.1"
async-graphql — это GraphQL библиотека, actix-web — web фреймворк, а async-graphql-actix-web обеспечивает интеграцию между ними.Ключевые функцииНачнём с main.rs:main.rs
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
dotenv().ok();
let pool = create_connection_pool();
run_migrations(&pool);
let schema = create_schema_with_context(pool);
HttpServer::new(move || App::new()
.configure(configure_service)
.data(schema.clone())
)
.bind("0.0.0.0:8001")?
.run()
.await
}
Здесь окружение и HTTP сервер конфигурируются с помощью функций, определённых в lib.rs:lib.rs
pub fn configure_service(cfg: &mut web::ServiceConfig) {
cfg
.service(web::resource("/")
.route(web::post().to(index))
.route(web::get().guard(guard::Header("upgrade", "websocket")).to(index_ws))
.route(web::get().to(index_playground))
);
}
async fn index(schema: web::Data, http_req: HttpRequest, req: Request) -> Response {
let mut query = req.into_inner();
let maybe_role = common_utils::get_role(http_req);
if let Some(role) = maybe_role {
query = query.data(role);
}
schema.execute(query).await.into()
}
async fn index_ws(schema: web::Data, req: HttpRequest, payload: web::Payload) -> Result {
WSSubscription::start(Schema::clone(&*schema), &req, payload)
}
async fn index_playground() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(playground_source(GraphQLPlaygroundConfig::new("/").subscription_endpoint("/")))
}
pub fn create_schema_with_context(pool: PgPool) -> Schema {
let arc_pool = Arc::new(pool);
let cloned_pool = Arc::clone(&arc_pool);
let details_batch_loader = Loader::new(DetailsBatchLoader {
pool: cloned_pool
}).with_max_batch_size(10);
let kafka_consumer_counter = Mutex::new(0);
Schema::build(Query, Mutation, Subscription)
.data(arc_pool)
.data(details_batch_loader)
.data(kafka::create_producer())
.data(kafka_consumer_counter)
.finish()
}
Эти функции делают следующее:
- index — обрабатывает GraphQL запросы (query) и мутации
- index_ws — обрабатывает GraphQL подписки
- index_playground — предоставляет Playground GraphQL IDE
- create_schema_with_context — создаёт GraphQL схему с глобальным контекстом доступным в рантайме, например, пул соединений с БД
Определение GraphQL запроса и типаРассмотрим как определить запрос:Определение запроса
#[Object]
impl Query {
async fn get_planets(&self, ctx: &Context<'_>) -> Vec {
repository::get_all(&get_conn_from_ctx(ctx)).expect("Can't get planets")
.iter()
.map(|p| { Planet::from(p) })
.collect()
}
async fn get_planet(&self, ctx: &Context<'_>, id: ID) -> Option {
find_planet_by_id_internal(ctx, id)
}
#[graphql(entity)]
async fn find_planet_by_id(&self, ctx: &Context<'_>, id: ID) -> Option {
find_planet_by_id_internal(ctx, id)
}
}
fn find_planet_by_id_internal(ctx: &Context<'_>, id: ID) -> Option {
let id = id.to_string().parse::().expect("Can't get id from String");
repository::get(id, &get_conn_from_ctx(ctx)).ok()
.map(|p| { Planet::from(&p) })
}
Эти запросы получают данные из БД используя слой репозитория. Полученные сущности конвертируются в GraphQL DTO (это позволяет соблюсти принцип единственной ответственности для каждой структуры). Запросы get_planets и get_planet могут быть выполнены из любой GraphQL IDE например так:Пример использования запроса
{
getPlanets {
name
type
}
}
Структура Planet определена так:Определение GraphQL типа
#[derive(Serialize, Deserialize)]
struct Planet {
id: ID,
name: String,
planet_type: PlanetType,
}
#[Object]
impl Planet {
async fn id(&self) -> &ID {
&self.id
}
async fn name(&self) -> &String {
&self.name
}
/// From an astronomical point of view
#[graphql(name = "type")]
async fn planet_type(&self) -> &PlanetType {
&self.planet_type
}
#[graphql(deprecation = "Now it is not in doubt. Do not use this field")]
async fn is_rotating_around_sun(&self) -> bool {
true
}
async fn details(&self, ctx: &Context<'_>) -> Details {
let loader = ctx.data::>().expect("Can't get loader");
let planet_id = self.id.to_string().parse::().expect("Can't convert id");
loader.load(planet_id).await
}
}
В impl определяется резолвер для каждого поля. Также для некоторых полей определены описание (в виде Rust комментария) и deprecation reason. Это будет отображено в GraphQL IDE.Проблема N+1В случае наивной реализации функции Planet.details выше возникла бы проблема N+1, то есть, при выполнении такого запроса:Пример возможного ресурсоёмкого GraphQL запроса
{
getPlanets {
name
details {
meanRadius
}
}
}
для поля details каждой из планет был бы сделан отдельный SQL запрос, т. к. Details — отдельная от Planet сущность и хранится в собственной таблице.Но с помощью DataLoader, реализованного в Async-graphql, резолвер details может быть определён так:Определение резолвера
async fn details(&self, ctx: &Context<'_>) -> Result {
let data_loader = ctx.data::>().expect("Can't get data loader");
let planet_id = self.id.to_string().parse::().expect("Can't convert id");
let details = data_loader.load_one(planet_id).await?;
details.ok_or_else(|| "Not found".into())
}
data_loader — это объект в контектсе приложения, определённый так:Определение DataLoader'а
let details_data_loader = DataLoader::new(DetailsLoader {
pool: cloned_pool
}).max_batch_size(10);
DetailsLoader реализован следующим образом:DetailsLoader definition
pub struct DetailsLoader {
pub pool: Arc
}
#[async_trait::async_trait]
impl Loader for DetailsLoader {
type Value = Details;
type Error = Error;
async fn load(&self, keys: &[i32]) -> Result, Self::Error> {
let conn = self.pool.get().expect("Can't get DB connection");
let details = repository::get_details(keys, &conn).expect("Can't get planets' details");
Ok(details.iter()
.map(|details_entity| (details_entity.planet_id, Details::from(details_entity)))
.collect::>())
}
}
Такой подход позволяет предотвратить проблему N+1, т. к. каждый вызов DetailsLoader.load выполняет только один SQL запрос, возвращающий пачку DetailsEntity.Определение интерфейсаGraphQL интерфейс и его реализации могут быть определены следующим образом:Определение GraphQL интерфейса
#[derive(Interface, Clone)]
#[graphql(
field(name = "mean_radius", type = "&CustomBigDecimal"),
field(name = "mass", type = "&CustomBigInt"),
)]
pub enum Details {
InhabitedPlanetDetails(InhabitedPlanetDetails),
UninhabitedPlanetDetails(UninhabitedPlanetDetails),
}
#[derive(SimpleObject, Clone)]
pub struct InhabitedPlanetDetails {
mean_radius: CustomBigDecimal,
mass: CustomBigInt,
/// In billions
population: CustomBigDecimal,
}
#[derive(SimpleObject, Clone)]
pub struct UninhabitedPlanetDetails {
mean_radius: CustomBigDecimal,
mass: CustomBigInt,
}
Здесь вы также можете видеть, что если в структуре нет ни одного поля со "сложным" резолвером, то она может быть реализована с использованием атрибута SimpleObject.Определение кастомного скалярного типаКастомные скаляры позволяют определить как представлять и как парсить значения определённого типа. Проект содержит два примера определения кастомных скаляров; оба являются обёртками для числовых структур (т. к. невозможно определить внешний трейт на внешней структуре из-за orphan rule). Эти обёртки определены так:Кастомный скаляр: обёртка для BigInt
#[derive(Clone)]
pub struct CustomBigInt(BigDecimal);
#[Scalar(name = "BigInt")]
impl ScalarType for CustomBigInt {
fn parse(value: Value) -> InputValueResult {
match value {
Value::String(s) => {
let parsed_value = BigDecimal::from_str(&s)?;
Ok(CustomBigInt(parsed_value))
}
_ => Err(InputValueError::expected_type(value)),
}
}
fn to_value(&self) -> Value {
Value::String(format!("{:e}", &self))
}
}
impl LowerExp for CustomBigInt {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let val = &self.0.to_f64().expect("Can't convert BigDecimal");
LowerExp::fmt(val, f)
}
}
Кастомный скаляр: обёртка для BigDecimal
#[derive(Clone)]
pub struct CustomBigDecimal(BigDecimal);
#[Scalar(name = "BigDecimal")]
impl ScalarType for CustomBigDecimal {
fn parse(value: Value) -> InputValueResult {
match value {
Value::String(s) => {
let parsed_value = BigDecimal::from_str(&s)?;
Ok(CustomBigDecimal(parsed_value))
}
_ => Err(InputValueError::expected_type(value)),
}
}
fn to_value(&self) -> Value {
Value::String(self.0.to_string())
}
}
В первом примере также показано, как представить гигантское число в виде экспоненциальной записи.Определение мутацииМутация может быть определена следующим образом:Определение мутации
pub struct Mutation;
#[Object]
impl Mutation {
#[graphql(guard(RoleGuard(role = "Role::Admin")))]
async fn create_planet(&self, ctx: &Context<'_>, planet: PlanetInput) -> Result {
let new_planet = NewPlanetEntity {
name: planet.name,
planet_type: planet.planet_type.to_string(),
};
let details = planet.details;
let new_planet_details = NewDetailsEntity {
mean_radius: details.mean_radius.0,
mass: BigDecimal::from_str(&details.mass.0.to_string()).expect("Can't get BigDecimal from string"),
population: details.population.map(|wrapper| { wrapper.0 }),
planet_id: 0,
};
let created_planet_entity = repository::create(new_planet, new_planet_details, &get_conn_from_ctx(ctx))?;
let producer = ctx.data::().expect("Can't get Kafka producer");
let message = serde_json::to_string(&Planet::from(&created_planet_entity)).expect("Can't serialize a planet");
kafka::send_message(producer, message).await;
Ok(Planet::from(&created_planet_entity))
}
}
Чтобы использовать объект как входной параметр мутации, надо определить структуру следующим образом:Определение input type
#[derive(InputObject)]
struct PlanetInput {
name: String,
#[graphql(name = "type")]
planet_type: PlanetType,
details: DetailsInput,
}
Мутация защищена RoleGuard'ом, который гарантирует что только пользователи с ролбю Admin могут выполнить её. Таким образом, для выполнения, например, следующей мутации:Example of mutation usage
mutation {
createPlanet(
planet: {
name: "test_planet"
type: TERRESTRIAL_PLANET
details: { meanRadius: "10.5", mass: "8.8e24", population: "0.5" }
}
) {
id
}
}
вам нужно указать заголовок Authorization с JWT, полученным из auth-service (это будет описано далее).Определение подпискиВ определении мутации выше вы могли видеть что при добавлении новой планеты отправляется сообщение:Отправка сообщения в Kafka
let producer = ctx.data::().expect("Can't get Kafka producer");
let message = serde_json::to_string(&Planet::from(&created_planet_entity)).expect("Can't serialize a planet");
kafka::send_message(producer, message).await;
Клиент API может быть уведомлен об этом событии с помощью подписки, слушающей Kafka consumer:Определение подписки
pub struct Subscription;
#[Subscription]
impl Subscription {
async fn latest_planet<'ctx>(&self, ctx: &'ctx Context<'_>) -> impl Stream + 'ctx {
let kafka_consumer_counter = ctx.data::>().expect("Can't get Kafka consumer counter");
let consumer_group_id = kafka::get_kafka_consumer_group_id(kafka_consumer_counter);
let consumer = kafka::create_consumer(consumer_group_id);
async_stream::stream! {
let mut stream = consumer.start();
while let Some(value) = stream.next().await {
yield match value {
Ok(message) => {
let payload = message.payload().expect("Kafka message should contain payload");
let message = String::from_utf8_lossy(payload).to_string();
serde_json::from_str(&message).expect("Can't deserialize a planet")
}
Err(e) => panic!("Error while Kafka message processing: {}", e)
};
}
}
}
}
Подписка может быть использована так же, как запросы и мутации:Пример использования подписки
subscription {
latestPlanet {
id
name
type
details {
meanRadius
}
}
}
Подписки должны отправляться на ws://localhost:8001.Интеграционные тестыТесты запросов и мутаций можно написать так:Тест запроса
#[actix_rt::test]
async fn test_get_planets() {
let docker = Cli::default();
let (_pg_container, pool) = common::setup(&docker);
let mut service = test::init_service(App::new()
.configure(configure_service)
.data(create_schema_with_context(pool))
).await;
let query = "
{
getPlanets {
id
name
type
details {
meanRadius
mass
... on InhabitedPlanetDetails {
population
}
}
}
}
".to_string();
let request_body = GraphQLCustomRequest {
query,
variables: Map::new(),
};
let request = test::TestRequest::post().uri("/").set_json(&request_body).to_request();
let response: GraphQLCustomResponse = test::read_response_json(&mut service, request).await;
fn get_planet_as_json(all_planets: &serde_json::Value, index: i32) -> &serde_json::Value {
jsonpath::select(all_planets, &format!("$.getPlanets[{}]", index)).expect("Can't get planet by JSON path")[0]
}
let mercury_json = get_planet_as_json(&response.data, 0);
common::check_planet(mercury_json, 1, "Mercury", "TERRESTRIAL_PLANET", "2439.7");
let earth_json = get_planet_as_json(&response.data, 2);
common::check_planet(earth_json, 3, "Earth", "TERRESTRIAL_PLANET", "6371.0");
let neptune_json = get_planet_as_json(&response.data, 7);
common::check_planet(neptune_json, 8, "Neptune", "ICE_GIANT", "24622.0");
}
Если часть запроса может быть переиспользована в другом запросе, вы можете использовать фрагменты:Тест запроса с использованием фрагмента
const PLANET_FRAGMENT: &str = "
fragment planetFragment on Planet {
id
name
type
details {
meanRadius
mass
... on InhabitedPlanetDetails {
population
}
}
}
";
#[actix_rt::test]
async fn test_get_planet_by_id() {
...
let query = "
{
getPlanet(id: 3) {
... planetFragment
}
}
".to_string() + PLANET_FRAGMENT;
let request_body = GraphQLCustomRequest {
query,
variables: Map::new(),
};
...
}
Чтобы использовать переменные, запишите тест так:Тест запроса с использованием фрагмента и переменной
#[actix_rt::test]
async fn test_get_planet_by_id_with_variable() {
...
let query = "
query testPlanetById($planetId: String!) {
getPlanet(id: $planetId) {
... planetFragment
}
}".to_string() + PLANET_FRAGMENT;
let jupiter_id = 5;
let mut variables = Map::new();
variables.insert("planetId".to_string(), jupiter_id.into());
let request_body = GraphQLCustomRequest {
query,
variables,
};
...
}
В этом проекте используется библиотека Testcontainers-rs, что позволяет подготовить тестовое окружение, то есть, создать временную БД PostgreSQL.Клиент к GraphQL APIВы можете использовать код из предыдущего раздела для создания клиента к внешнему GraphQL API. Также для этого существуют специальные библиотеки, например, graphql-client, но я их не использовал.Безопасность APIСуществуют различные угрозы безопасности GraphQL API (см. список); рассмотрим некоторые из них.Ограничения глубины и сложности запросаЕсли бы структура Satellite содержала поле planet, был бы возможен такой запрос:Пример тяжёлого запроса
{
getPlanet(id: "1") {
satellites {
planet {
satellites {
planet {
satellites {
... # more deep nesting!
}
}
}
}
}
}
}
Сделать такой запрос невалидным можно так:Пример ограничения глубины и сложности запроса
pub fn create_schema_with_context(pool: PgPool) -> Schema {
...
Schema::build(Query, Mutation, Subscription)
.limit_depth(3)
.limit_complexity(15)
...
}
Стоит отметить, что при указании ограничений выше может перестать отображаться документация сервиса в GraphQL IDE. Это происходит потому, что IDE пытается выполнить introspection query, который имеет заметные глубину и сложность.АутентификацияЭта функциональность реализована в auth-service с использованием крэйтов argonautica и jsonwebtoken. Первый отвечает за хэшироование паролей пользователей с использованием алгоритма Argon2. Аутентификация и авторизация показаны исключительно в демонстрационных целях; пожалуйста, проведите изучите вопрос более тщательно преед использованием в продакшене.Рассмотрим как реализован вход в систему:Реализация входа в систему
pub struct Mutation;
#[Object]
impl Mutation {
async fn sign_in(&self, ctx: &Context<'_>, input: SignInInput) -> Result {
let maybe_user = repository::get_user(&input.username, &get_conn_from_ctx(ctx)).ok();
if let Some(user) = maybe_user {
if let Ok(matching) = verify_password(&user.hash, &input.password) {
if matching {
let role = AuthRole::from_str(user.role.as_str()).expect("Can't convert &str to AuthRole");
return Ok(common_utils::create_token(user.username, role));
}
}
}
Err(Error::new("Can't authenticate a user"))
}
}
#[derive(InputObject)]
struct SignInInput {
username: String,
password: String,
}
Посмотреть реализацию функции verify_password можно в модуле and create_token in common_utils module utils. Как вы могли бы ожидать, функция sign_in возвращает JWT, который в дальнейшем может быть использован для авторизации в других сервисах.Для получения JWT выполните следующую мутацию:Получение JWT
mutation {
signIn(input: { username: "john_doe", password: "password" })
}
Используйте параметры john_doe/password. Включение полученного JWT в последующий е запросы позволит получить доступ к защищённым ресурсам (см. следующий раздел).АвторизацияЧтобы запросить защищённые данные, добавьте заголовок в HTTP запрос в формате Authorization: Bearer $JWT. Функция index извлечёт роль пользователя из HTTP запроса и добавит её в параметры GraphQL запроса/мутации:Получение роли
async fn index(schema: web::Data, http_req: HttpRequest, req: Request) -> Response {
let mut query = req.into_inner();
let maybe_role = common_utils::get_role(http_req);
if let Some(role) = maybe_role {
query = query.data(role);
}
schema.execute(query).await.into()
}
К ранее показанной мутации create_planet применён следующий атрибут:Использование гарда
#[graphql(guard(RoleGuard(role = "Role::Admin")))]
Сам гард реализован так:Реализация гарда
struct RoleGuard {
role: Role,
}
#[async_trait::async_trait]
impl Guard for RoleGuard {
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
if ctx.data_opt::() == Some(&self.role) {
Ok(())
} else {
Err("Forbidden".into())
}
}
}
Таким образом, если вы не укажете токен, сервер ответит сообщением "Forbidden".Определение перечисленияGraphQL перечисление может быть определено так:Определение перечисления
#[derive(SimpleObject)]
struct Satellite {
...
life_exists: LifeExists,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum, EnumString)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
pub enum LifeExists {
Yes,
OpenQuestion,
NoData,
}
Работа с датамиAsync-graphql поддерживает типы даты/времени из библиотеки chrono, поэтому вы можете определить такие поля как обычно:Определение поля с датой
#[derive(SimpleObject)]
struct Satellite {
...
first_spacecraft_landing_date: Option,
}
Поддержка Apollo FederationОдна из целей satellites-service — продемонстрировать как распределённая GraphQL сущность (Planet) может резолвиться в двух (или более) сервисах и затем запрашиваться через Apollo Server.Тип Planet был ранее определён в planets-service так:Определение типаPlanet в planets-service
#[derive(Serialize, Deserialize)]
struct Planet {
id: ID,
name: String,
planet_type: PlanetType,
}
Также в planets-service тип Planet является сущностью:Определение сущностиPlanet
#[Object]
impl Query {
#[graphql(entity)]
async fn find_planet_by_id(&self, ctx: &Context<'_>, id: ID) -> Option {
find_planet_by_id_internal(ctx, id)
}
}
satellites-service расширяет сущность Planet путём добавления поля satellites:Расширение типаPlanet в satellites-service
struct Planet {
id: ID
}
#[Object(extends)]
impl Planet {
#[graphql(external)]
async fn id(&self) -> &ID {
&self.id
}
async fn satellites(&self, ctx: &Context<'_>) -> Vec {
let id = self.id.to_string().parse::().expect("Can't get id from String");
repository::get_by_planet_id(id, &get_conn_from_ctx(ctx)).expect("Can't get satellites of planet")
.iter()
.map(|e| { Satellite::from(e) })
.collect()
}
}
Также вам нужно реализовать функцию поиска для расширяемого типа. В примере ниже функция просто создаёт новый инстанс Planet:Функция поиска для типаPlanet
#[Object]
impl Query {
#[graphql(entity)]
async fn get_planet_by_id(&self, id: ID) -> Planet {
Planet { id }
}
}
Async-graphql генерирует два дополнительных запроса (_service and _entities), которые будут использованы Apollo Server'ом. Эти запросы — внутренние, то есть они не будут отображены в API Apollo Server'а. Конечно, сервис с поддержкой Apollo Federation по-прежнему может работать автономно.Apollo ServerApollo Server и Apollo Federation позволяют достичь две основные цели:
- создать единую точку доступа к нескольким GraphQL API
- создать единый граф данных из распределённых сущностей
Таким образом, даже если вы не используете распределённые сущности, для frontend разработчиков удобнее использовать одну точку доступа, чем несколько.Существует и другой способ создания единой GraphQL схемы, schema stitching, но пока что я его не использовал.Модуль включает следующий исходный код:Мета-информация и зависимости
{
"name": "api-gateway",
"main": "gateway.js",
"scripts": {
"start-gateway": "nodemon gateway.js"
},
"devDependencies": {
"concurrently": "5.3.0",
"nodemon": "2.0.6"
},
"dependencies": {
"@apollo/gateway": "0.21.3",
"apollo-server": "2.19.0",
"graphql": "15.4.0"
}
}
Определение Apollo Server
const {ApolloServer} = require("apollo-server");
const {ApolloGateway, RemoteGraphQLDataSource} = require("@apollo/gateway");
class AuthenticatedDataSource extends RemoteGraphQLDataSource {
willSendRequest({request, context}) {
if (context.authHeaderValue) {
request.http.headers.set('Authorization', context.authHeaderValue);
}
}
}
let node_env = process.env.NODE_ENV;
function get_service_url(service_name, port) {
let host;
switch (node_env) {
case 'docker':
host = service_name;
break;
case 'local': {
host = 'localhost';
break
}
}
return "http://" + host + ":" + port;
}
const gateway = new ApolloGateway({
serviceList: [
{name: "planets-service", url: get_service_url("planets-service", 8001)},
{name: "satellites-service", url: get_service_url("satellites-service", 8002)},
{name: "auth-service", url: get_service_url("auth-service", 8003)},
],
buildService({name, url}) {
return new AuthenticatedDataSource({url});
},
});
const server = new ApolloServer({
gateway, subscriptions: false, context: ({req}) => ({
authHeaderValue: req.headers.authorization
})
});
server.listen({host: "0.0.0.0", port: 4000}).then(({url}) => {
console.log(`? Server ready at ${url}`);
});
Если код выше может быть упрощён, не стесняйтесь поправить.Авторизация в apollo-service работает так же, как было показано ранее для Rust сервисов (вам надо указать заголовок Authorization и его значение).Приложение, написанное на любом языке или фреймворке, может быть добавлено в качестве нижележащего сервиса под Apollo Server, если оно реализует спецификацию Federation; список библиотек, добавляющих поддержку этой спецификации доступен в документации.При реализации модуля я столкнулся со следующими ограничениями:
- Apollo Gateway не поддерживает подписки (но они по-прежнему работают в standalone Rust сервисе)
- сервису, пытающемуся расширить GraphQL интерфейс требуется информация о его конкретных имплементациях
Взаимодействие с БДУровень хранения реализован с помощью PostgreSQL and Diesel. Если вы не используете Docker при локальном запуске, то нужно выполнить diesel setup, находясь в директории каждого из сервисов. Это создаст пустую БД, к которой далее будут применены миграции, создающие таблицы и инициализирующие данные.Запуск проекта и тестирование APIКак было отмечено ранее, проект можно запустить двумя способами:
- с использованием Docker Compose (docker-compose.yml)Здесь, в свою очередь, также возможны два варианта:
- режим разработки (используя локально собранные образы)docker-compose up
- production mode (используя релизные образы)docker-compose -f docker-compose.yml up
- без DockerЗапустите каждый Rust сервис с помощью cargo run, потом запустите Apollo Server:
- cd в папку apollo-server
- определите переменную среды NODE_ENV, например, set NODE_ENV=local (для Windows)
- npm install
- npm run start-gateway
Успешный запуск apollo-server должен выглядеть так:Лог запуска Apollo Server
[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node gateway.js`
Server ready at http://0.0.0.0:4000/
Вы можете перейти на http://localhost:4000 в браузере и использовать встроенную Playground IDE:
Здесь возможно выполнять запросы, мутации и подписки, определённые в нижележащих сервисах. Кроме того, каждый из этих сервисов имеет собственную Playground IDE.Тест подпискиЧтобы убедиться в том, что подписка работает, откройте две вкладки любой GraphQL IDE; в первой подпишитесь таким образом:Пример подписки
subscription {
latestPlanet {
name
type
}
}
Во второй укажите заголовок Authorization как было описано ранее и выполните мутацию:Пример мутации
mutation {
createPlanet(
planet: {
name: "Pluto"
type: DWARF_PLANET
details: { meanRadius: "1188", mass: "1.303e22" }
}
) {
id
}
}
Подписанный клиент будет уведомлен о событии:
CI/CDCI/CD сконфигурирован с помощью GitHub Actions (workflow), который запускает тесты приложений, собирает их Docker образы и разворачивает их на Google Cloud Platform.Вы можете посмотреть на описанные API здесь.Замечание: На "продакшн" среде пароль отличается от указанного ранее, чтобы предотвратить изменение данных.ЗаключениеВ этой статье я рассмотрел как решать наиболее частые вопросы, которые могут возникнуть при разработке GraphQL API на Rust. Также было показано как объединить API Rust GraphQL микросервисов для получения единого GraphQL интерфейса; в подобной архитектуре сущность может быть распределена среди нескольких микросервисов. Это достигается за счёт использования Apollo Server, Apollo Federation и библиотеки Async-graphql. Исходный код рассмотренного проекта доступен на GitHub. Не стесняйтесь написать мне, если найдёте ошибки в статье или исходном коде. Благодарю за внимание!Полезные ссылки
- graphql.org
- spec.graphql.org
- graphql.org/learn/best-practices
- howtographql.com
- Async-graphql
- Async-graphql book
- Awesome GraphQL
- Public GraphQL APIs
- Apollo Federation demo
===========
Источник:
habr.com
===========
===========
Автор оригинала: Roman Kudryashov
===========Похожие новости:
- [JavaScript, Программирование] Программная генерация изображений с помощью API CSS Painting (перевод)
- [Разработка веб-сайтов, JavaScript, Angular, TypeScript] Как мы делаем базовые компоненты в Taiga UI более гибкими: концепция контроллеров компонента в Angular
- [JavaScript, ReactJS, TypeScript] Структура React REST API приложения + TypeScript + Styled-Components
- [JavaScript, Программирование, Расширения для браузеров, Браузеры] Hello, Word! Разрабатываем браузерное расширение в 2021-м
- [JavaScript, ReactJS] Небольшая практика с JS Proxy для оптимизации перерисовок React компонентов при использовании useContext
- [Высокая производительность, JavaScript, Программирование, WebAssembly] Разгоняем JS-парсер с помощью WebAssembly (часть 1: базовые возможности)
- [Обработка изображений, API] Удаляем фон с изображений с помощью бесплатного API
- [Разработка веб-сайтов, JavaScript, Проектирование и рефакторинг, ООП, ReactJS] Техники повторного использования кода
- [Киберпанк, Научно-популярное, Социальные сети и сообщества, Будущее здесь] Что делать, если технический прогресс ухудшает жизнь людей? Перестаньте кормить зверя
- [Отладка, Микросервисы, Serverless] Руководство по отладке бессерверных приложений (перевод)
Теги для поиска: #_javascript, #_api, #_rust, #_mikroservisy (Микросервисы), #_graph, #_rust, #_api, #_microservices, #_javascript, #_api, #_rust, #_mikroservisy (
Микросервисы
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:39
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
В этой статье я покажу как создать GraphQL сервер, используя Rust и его экосистему; будут приведены примеры реализации наиболее часто встречающихся задач при разработке GraphQL API. В итоге API трёх микросервисов будут объединены в единую точку доступа с помощью Apollo Server и Apollo Federation. Это позволит клиентам запрашивать данные одновременно из нескольких источников без необходимости знать какие данные приходят из какого сервиса.ВведениеОбзорС точки зрения функциональности описываемый проект довольно похож на представленный в моей предыдущей статье, но в этот раз с использованием стэка Rust. Архитектурно проекта выглядит так: Каждый компонент архитектуры освещает несколько вопросов, которые могут возникнуть при реализации GraphQL API. Доменная модель включает данные о планетах Солнечной системы и их спутниках. Проект имеет многомодульную структуру (или монорепозиторий) и состоит из следующих модулей:
[workspace]
members = [ "auth-service", "planets-service", "satellites-service", "common-utils", ] [package]
name = "planets-service" version = "0.1.0" edition = "2018" [dependencies] common-utils = { path = "../common-utils" } async-graphql = "2.4.3" async-graphql-actix-web = "2.4.3" actix-web = "3.3.2" actix-rt = "1.1.1" actix-web-actors = "3.0.0" futures = "0.3.8" async-trait = "0.1.42" bigdecimal = { version = "0.1.2", features = ["serde"] } serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.60" diesel = { version = "1.4.5", features = ["postgres", "r2d2", "numeric"] } diesel_migrations = "1.4.0" dotenv = "0.15.0" strum = "0.20.0" strum_macros = "0.20.1" rdkafka = { version = "0.24.0", features = ["cmake-build"] } async-stream = "0.3.0" lazy_static = "1.4.0" [dev-dependencies] jsonpath_lib = "0.2.6" testcontainers = "0.9.1" #[actix_rt::main]
async fn main() -> std::io::Result<()> { dotenv().ok(); let pool = create_connection_pool(); run_migrations(&pool); let schema = create_schema_with_context(pool); HttpServer::new(move || App::new() .configure(configure_service) .data(schema.clone()) ) .bind("0.0.0.0:8001")? .run() .await } pub fn configure_service(cfg: &mut web::ServiceConfig) {
cfg .service(web::resource("/") .route(web::post().to(index)) .route(web::get().guard(guard::Header("upgrade", "websocket")).to(index_ws)) .route(web::get().to(index_playground)) ); } async fn index(schema: web::Data, http_req: HttpRequest, req: Request) -> Response { let mut query = req.into_inner(); let maybe_role = common_utils::get_role(http_req); if let Some(role) = maybe_role { query = query.data(role); } schema.execute(query).await.into() } async fn index_ws(schema: web::Data, req: HttpRequest, payload: web::Payload) -> Result { WSSubscription::start(Schema::clone(&*schema), &req, payload) } async fn index_playground() -> HttpResponse { HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(playground_source(GraphQLPlaygroundConfig::new("/").subscription_endpoint("/"))) } pub fn create_schema_with_context(pool: PgPool) -> Schema { let arc_pool = Arc::new(pool); let cloned_pool = Arc::clone(&arc_pool); let details_batch_loader = Loader::new(DetailsBatchLoader { pool: cloned_pool }).with_max_batch_size(10); let kafka_consumer_counter = Mutex::new(0); Schema::build(Query, Mutation, Subscription) .data(arc_pool) .data(details_batch_loader) .data(kafka::create_producer()) .data(kafka_consumer_counter) .finish() }
#[Object]
impl Query { async fn get_planets(&self, ctx: &Context<'_>) -> Vec { repository::get_all(&get_conn_from_ctx(ctx)).expect("Can't get planets") .iter() .map(|p| { Planet::from(p) }) .collect() } async fn get_planet(&self, ctx: &Context<'_>, id: ID) -> Option { find_planet_by_id_internal(ctx, id) } #[graphql(entity)] async fn find_planet_by_id(&self, ctx: &Context<'_>, id: ID) -> Option { find_planet_by_id_internal(ctx, id) } } fn find_planet_by_id_internal(ctx: &Context<'_>, id: ID) -> Option { let id = id.to_string().parse::().expect("Can't get id from String"); repository::get(id, &get_conn_from_ctx(ctx)).ok() .map(|p| { Planet::from(&p) }) } {
getPlanets { name type } } #[derive(Serialize, Deserialize)]
struct Planet { id: ID, name: String, planet_type: PlanetType, } #[Object] impl Planet { async fn id(&self) -> &ID { &self.id } async fn name(&self) -> &String { &self.name } /// From an astronomical point of view #[graphql(name = "type")] async fn planet_type(&self) -> &PlanetType { &self.planet_type } #[graphql(deprecation = "Now it is not in doubt. Do not use this field")] async fn is_rotating_around_sun(&self) -> bool { true } async fn details(&self, ctx: &Context<'_>) -> Details { let loader = ctx.data::>().expect("Can't get loader"); let planet_id = self.id.to_string().parse::().expect("Can't convert id"); loader.load(planet_id).await } } {
getPlanets { name details { meanRadius } } } async fn details(&self, ctx: &Context<'_>) -> Result {
let data_loader = ctx.data::>().expect("Can't get data loader"); let planet_id = self.id.to_string().parse::().expect("Can't convert id"); let details = data_loader.load_one(planet_id).await?; details.ok_or_else(|| "Not found".into()) } let details_data_loader = DataLoader::new(DetailsLoader {
pool: cloned_pool }).max_batch_size(10); pub struct DetailsLoader {
pub pool: Arc } #[async_trait::async_trait] impl Loader for DetailsLoader { type Value = Details; type Error = Error; async fn load(&self, keys: &[i32]) -> Result, Self::Error> { let conn = self.pool.get().expect("Can't get DB connection"); let details = repository::get_details(keys, &conn).expect("Can't get planets' details"); Ok(details.iter() .map(|details_entity| (details_entity.planet_id, Details::from(details_entity))) .collect::>()) } } #[derive(Interface, Clone)]
#[graphql( field(name = "mean_radius", type = "&CustomBigDecimal"), field(name = "mass", type = "&CustomBigInt"), )] pub enum Details { InhabitedPlanetDetails(InhabitedPlanetDetails), UninhabitedPlanetDetails(UninhabitedPlanetDetails), } #[derive(SimpleObject, Clone)] pub struct InhabitedPlanetDetails { mean_radius: CustomBigDecimal, mass: CustomBigInt, /// In billions population: CustomBigDecimal, } #[derive(SimpleObject, Clone)] pub struct UninhabitedPlanetDetails { mean_radius: CustomBigDecimal, mass: CustomBigInt, } #[derive(Clone)]
pub struct CustomBigInt(BigDecimal); #[Scalar(name = "BigInt")] impl ScalarType for CustomBigInt { fn parse(value: Value) -> InputValueResult { match value { Value::String(s) => { let parsed_value = BigDecimal::from_str(&s)?; Ok(CustomBigInt(parsed_value)) } _ => Err(InputValueError::expected_type(value)), } } fn to_value(&self) -> Value { Value::String(format!("{:e}", &self)) } } impl LowerExp for CustomBigInt { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let val = &self.0.to_f64().expect("Can't convert BigDecimal"); LowerExp::fmt(val, f) } } #[derive(Clone)]
pub struct CustomBigDecimal(BigDecimal); #[Scalar(name = "BigDecimal")] impl ScalarType for CustomBigDecimal { fn parse(value: Value) -> InputValueResult { match value { Value::String(s) => { let parsed_value = BigDecimal::from_str(&s)?; Ok(CustomBigDecimal(parsed_value)) } _ => Err(InputValueError::expected_type(value)), } } fn to_value(&self) -> Value { Value::String(self.0.to_string()) } } pub struct Mutation;
#[Object] impl Mutation { #[graphql(guard(RoleGuard(role = "Role::Admin")))] async fn create_planet(&self, ctx: &Context<'_>, planet: PlanetInput) -> Result { let new_planet = NewPlanetEntity { name: planet.name, planet_type: planet.planet_type.to_string(), }; let details = planet.details; let new_planet_details = NewDetailsEntity { mean_radius: details.mean_radius.0, mass: BigDecimal::from_str(&details.mass.0.to_string()).expect("Can't get BigDecimal from string"), population: details.population.map(|wrapper| { wrapper.0 }), planet_id: 0, }; let created_planet_entity = repository::create(new_planet, new_planet_details, &get_conn_from_ctx(ctx))?; let producer = ctx.data::().expect("Can't get Kafka producer"); let message = serde_json::to_string(&Planet::from(&created_planet_entity)).expect("Can't serialize a planet"); kafka::send_message(producer, message).await; Ok(Planet::from(&created_planet_entity)) } } #[derive(InputObject)]
struct PlanetInput { name: String, #[graphql(name = "type")] planet_type: PlanetType, details: DetailsInput, } mutation {
createPlanet( planet: { name: "test_planet" type: TERRESTRIAL_PLANET details: { meanRadius: "10.5", mass: "8.8e24", population: "0.5" } } ) { id } } let producer = ctx.data::().expect("Can't get Kafka producer");
let message = serde_json::to_string(&Planet::from(&created_planet_entity)).expect("Can't serialize a planet"); kafka::send_message(producer, message).await; pub struct Subscription;
#[Subscription] impl Subscription { async fn latest_planet<'ctx>(&self, ctx: &'ctx Context<'_>) -> impl Stream + 'ctx { let kafka_consumer_counter = ctx.data::>().expect("Can't get Kafka consumer counter"); let consumer_group_id = kafka::get_kafka_consumer_group_id(kafka_consumer_counter); let consumer = kafka::create_consumer(consumer_group_id); async_stream::stream! { let mut stream = consumer.start(); while let Some(value) = stream.next().await { yield match value { Ok(message) => { let payload = message.payload().expect("Kafka message should contain payload"); let message = String::from_utf8_lossy(payload).to_string(); serde_json::from_str(&message).expect("Can't deserialize a planet") } Err(e) => panic!("Error while Kafka message processing: {}", e) }; } } } } subscription {
latestPlanet { id name type details { meanRadius } } } #[actix_rt::test]
async fn test_get_planets() { let docker = Cli::default(); let (_pg_container, pool) = common::setup(&docker); let mut service = test::init_service(App::new() .configure(configure_service) .data(create_schema_with_context(pool)) ).await; let query = " { getPlanets { id name type details { meanRadius mass ... on InhabitedPlanetDetails { population } } } } ".to_string(); let request_body = GraphQLCustomRequest { query, variables: Map::new(), }; let request = test::TestRequest::post().uri("/").set_json(&request_body).to_request(); let response: GraphQLCustomResponse = test::read_response_json(&mut service, request).await; fn get_planet_as_json(all_planets: &serde_json::Value, index: i32) -> &serde_json::Value { jsonpath::select(all_planets, &format!("$.getPlanets[{}]", index)).expect("Can't get planet by JSON path")[0] } let mercury_json = get_planet_as_json(&response.data, 0); common::check_planet(mercury_json, 1, "Mercury", "TERRESTRIAL_PLANET", "2439.7"); let earth_json = get_planet_as_json(&response.data, 2); common::check_planet(earth_json, 3, "Earth", "TERRESTRIAL_PLANET", "6371.0"); let neptune_json = get_planet_as_json(&response.data, 7); common::check_planet(neptune_json, 8, "Neptune", "ICE_GIANT", "24622.0"); } const PLANET_FRAGMENT: &str = "
fragment planetFragment on Planet { id name type details { meanRadius mass ... on InhabitedPlanetDetails { population } } } "; #[actix_rt::test] async fn test_get_planet_by_id() { ... let query = " { getPlanet(id: 3) { ... planetFragment } } ".to_string() + PLANET_FRAGMENT; let request_body = GraphQLCustomRequest { query, variables: Map::new(), }; ... } #[actix_rt::test]
async fn test_get_planet_by_id_with_variable() { ... let query = " query testPlanetById($planetId: String!) { getPlanet(id: $planetId) { ... planetFragment } }".to_string() + PLANET_FRAGMENT; let jupiter_id = 5; let mut variables = Map::new(); variables.insert("planetId".to_string(), jupiter_id.into()); let request_body = GraphQLCustomRequest { query, variables, }; ... } {
getPlanet(id: "1") { satellites { planet { satellites { planet { satellites { ... # more deep nesting! } } } } } } } pub fn create_schema_with_context(pool: PgPool) -> Schema {
... Schema::build(Query, Mutation, Subscription) .limit_depth(3) .limit_complexity(15) ... } pub struct Mutation;
#[Object] impl Mutation { async fn sign_in(&self, ctx: &Context<'_>, input: SignInInput) -> Result { let maybe_user = repository::get_user(&input.username, &get_conn_from_ctx(ctx)).ok(); if let Some(user) = maybe_user { if let Ok(matching) = verify_password(&user.hash, &input.password) { if matching { let role = AuthRole::from_str(user.role.as_str()).expect("Can't convert &str to AuthRole"); return Ok(common_utils::create_token(user.username, role)); } } } Err(Error::new("Can't authenticate a user")) } } #[derive(InputObject)] struct SignInInput { username: String, password: String, } mutation {
signIn(input: { username: "john_doe", password: "password" }) } async fn index(schema: web::Data, http_req: HttpRequest, req: Request) -> Response {
let mut query = req.into_inner(); let maybe_role = common_utils::get_role(http_req); if let Some(role) = maybe_role { query = query.data(role); } schema.execute(query).await.into() } #[graphql(guard(RoleGuard(role = "Role::Admin")))]
struct RoleGuard {
role: Role, } #[async_trait::async_trait] impl Guard for RoleGuard { async fn check(&self, ctx: &Context<'_>) -> Result<()> { if ctx.data_opt::() == Some(&self.role) { Ok(()) } else { Err("Forbidden".into()) } } } #[derive(SimpleObject)]
struct Satellite { ... life_exists: LifeExists, } #[derive(Copy, Clone, Eq, PartialEq, Debug, Enum, EnumString)] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] pub enum LifeExists { Yes, OpenQuestion, NoData, } #[derive(SimpleObject)]
struct Satellite { ... first_spacecraft_landing_date: Option, } #[derive(Serialize, Deserialize)]
struct Planet { id: ID, name: String, planet_type: PlanetType, } #[Object]
impl Query { #[graphql(entity)] async fn find_planet_by_id(&self, ctx: &Context<'_>, id: ID) -> Option { find_planet_by_id_internal(ctx, id) } } struct Planet {
id: ID } #[Object(extends)] impl Planet { #[graphql(external)] async fn id(&self) -> &ID { &self.id } async fn satellites(&self, ctx: &Context<'_>) -> Vec { let id = self.id.to_string().parse::().expect("Can't get id from String"); repository::get_by_planet_id(id, &get_conn_from_ctx(ctx)).expect("Can't get satellites of planet") .iter() .map(|e| { Satellite::from(e) }) .collect() } } #[Object]
impl Query { #[graphql(entity)] async fn get_planet_by_id(&self, id: ID) -> Planet { Planet { id } } }
{
"name": "api-gateway", "main": "gateway.js", "scripts": { "start-gateway": "nodemon gateway.js" }, "devDependencies": { "concurrently": "5.3.0", "nodemon": "2.0.6" }, "dependencies": { "@apollo/gateway": "0.21.3", "apollo-server": "2.19.0", "graphql": "15.4.0" } } const {ApolloServer} = require("apollo-server");
const {ApolloGateway, RemoteGraphQLDataSource} = require("@apollo/gateway"); class AuthenticatedDataSource extends RemoteGraphQLDataSource { willSendRequest({request, context}) { if (context.authHeaderValue) { request.http.headers.set('Authorization', context.authHeaderValue); } } } let node_env = process.env.NODE_ENV; function get_service_url(service_name, port) { let host; switch (node_env) { case 'docker': host = service_name; break; case 'local': { host = 'localhost'; break } } return "http://" + host + ":" + port; } const gateway = new ApolloGateway({ serviceList: [ {name: "planets-service", url: get_service_url("planets-service", 8001)}, {name: "satellites-service", url: get_service_url("satellites-service", 8002)}, {name: "auth-service", url: get_service_url("auth-service", 8003)}, ], buildService({name, url}) { return new AuthenticatedDataSource({url}); }, }); const server = new ApolloServer({ gateway, subscriptions: false, context: ({req}) => ({ authHeaderValue: req.headers.authorization }) }); server.listen({host: "0.0.0.0", port: 4000}).then(({url}) => { console.log(`? Server ready at ${url}`); });
[nodemon] 2.0.6
[nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node gateway.js` Server ready at http://0.0.0.0:4000/ Здесь возможно выполнять запросы, мутации и подписки, определённые в нижележащих сервисах. Кроме того, каждый из этих сервисов имеет собственную Playground IDE.Тест подпискиЧтобы убедиться в том, что подписка работает, откройте две вкладки любой GraphQL IDE; в первой подпишитесь таким образом:Пример подписки subscription {
latestPlanet { name type } } mutation {
createPlanet( planet: { name: "Pluto" type: DWARF_PLANET details: { meanRadius: "1188", mass: "1.303e22" } } ) { id } } CI/CDCI/CD сконфигурирован с помощью GitHub Actions (workflow), который запускает тесты приложений, собирает их Docker образы и разворачивает их на Google Cloud Platform.Вы можете посмотреть на описанные API здесь.Замечание: На "продакшн" среде пароль отличается от указанного ранее, чтобы предотвратить изменение данных.ЗаключениеВ этой статье я рассмотрел как решать наиболее частые вопросы, которые могут возникнуть при разработке GraphQL API на Rust. Также было показано как объединить API Rust GraphQL микросервисов для получения единого GraphQL интерфейса; в подобной архитектуре сущность может быть распределена среди нескольких микросервисов. Это достигается за счёт использования Apollo Server, Apollo Federation и библиотеки Async-graphql. Исходный код рассмотренного проекта доступен на GitHub. Не стесняйтесь написать мне, если найдёте ошибки в статье или исходном коде. Благодарю за внимание!Полезные ссылки
=========== Источник: habr.com =========== =========== Автор оригинала: Roman Kudryashov ===========Похожие новости:
Микросервисы ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:39
Часовой пояс: UTC + 5