Написание микросервисов и их шлюзов GraphQL на Go

база данных Go Программа перевода самородков GraphQL

Несколько месяцев назад отличный пакет GraphQL Govektah/gqlgenстал популярным. В этой статье описывается, как реализовать GraphQL в проекте Spidey, базовом микросервисе для интернет-магазина.

Некоторые из кодов, перечисленных ниже, могут отсутствовать, полный код, пожалуйста, посетитеGitHub.

Архитектура

Spidey содержит три разных сервиса, доступных для шлюза GraphQL. Коммуникация внутри кластера проходит черезgRPCЧто нужно сделать.

Служба учетных записей управляет всеми учетными записями, служба каталогов управляет всеми продуктами, служба заказов занимается созданием всех заказов. Он связывается с двумя другими службами, чтобы сообщить, был ли заказ выполнен нормально.

Architecture

Автономный сервис состоит из трех уровней:Уровень сервера,Сервисный уровеньа такжеСлой репозитория. За связь отвечает сервер, то есть в Spidey используется gRPC. Сервисы содержат бизнес-логику. Хранилище отвечает за операции чтения и записи в базу данных.

Начало

Для запуска Паучка требуетсяDocker,Docker Compose,Go,Protocol BuffersКомпилятор и его плагины Go и очень полезныеvektah/gqlgenСумка.

вам также необходимо установитьvgo(инструмент управления пакетами в ранней разработке). инструментdepтоже вариант, но в комплектеgo.modфайл будет проигнорирован.

Аннотация: В Go 1.11 vgo был выпущен как официальный интегрированный модуль Go, который был интегрирован в команду go и использовался с модом go Инструкции в основном такие же, как и у vgo.

Настройки докера

Каждый сервис реализован в своей подпапке и содержит как минимум одинapp.dockerfileдокумент.app.dockerfileПользователи файлов создают зеркальное отображение базы данных.

account
├── account.proto
├── app.dockerfile
├── cmd
│   └── account
│       └── main.go
├── db.dockerfile
└── up.sql

Все услуги через внешниеdocker-compose.yamlопределение.

Следующее является частью перехвата службы Account:

version: "3.6"

services:
  account:
    build:
      context: "."
      dockerfile: "./account/app.dockerfile"
    depends_on:
      - "account_db"
    environment:
      DATABASE_URL: "postgres://spidey:123456@account_db/spidey?sslmode=disable"
  account_db:
    build:
      context: "./account"
      dockerfile: "./db.dockerfile"
    environment:
      POSTGRES_DB: "spidey"
      POSTGRES_USER: "spidey"
      POSTGRES_PASSWORD: "123456"
    restart: "unless-stopped"

настраиватьcontextЦель состоит в том, чтобы обеспечитьvendorКаталоги можно копировать в контейнеры Docker. Все службы имеют одни и те же зависимости, а некоторые службы также зависят от определений других служб.

Услуги по работе с учетными записями

Служба учетных записей предоставляет методы для создания и индексации учетных записей.

Служить

Интерфейс, определяемый API службы учетной записи, выглядит следующим образом:

account/service.go

type Service interface {
  PostAccount(ctx context.Context, name string) (*Account, error)
  GetAccount(ctx context.Context, id string) (*Account, error)
  GetAccounts(ctx context.Context, skip uint64, take uint64) ([]Account, error)
}

type Account struct {
  ID   string `json:"id"`
  Name string `json:"name"`
}

Реализация должна использовать репозиторий:

type accountService struct {
  repository Repository
}

func NewService(r Repository) Service {
  return &accountService{r}
}

Этот сервис отвечает за всю бизнес-логику.PostAccountРеализация функции следующая:

func (s *accountService) PostAccount(ctx context.Context, name string) (*Account, error) {
  a := &Account{
    Name: name,
    ID:   ksuid.New().String(),
  }
  if err := s.repository.PutAccount(ctx, *a); err != nil {
    return nil, err
  }
  return a, nil
}

Он обрабатывает синтаксический анализ проводного протокола как сервер и базу данных как репозиторий.

база данных

Модель данных для учетной записи очень проста:

CREATE TABLE IF NOT EXISTS accounts (
  id CHAR(27) PRIMARY KEY,
  name VARCHAR(24) NOT NULL
);

Файл SQL с определенными выше данными будет скопирован в контейнер Docker для выполнения.

account/db.dockerfile

FROM postgres:10.3

COPY up.sql /docker-entrypoint-initdb.d/1.sql

CMD ["postgres"]

Доступ к базе данных PostgreSQL осуществляется через следующий интерфейс репозитория:

account/repository.go

type Repository interface {
  Close()
  PutAccount(ctx context.Context, a Account) error
  GetAccountByID(ctx context.Context, id string) (*Account, error)
  ListAccounts(ctx context.Context, skip uint64, take uint64) ([]Account, error)
}

Репозиторий упакован на основе пакета SQL стандартной библиотеки Go:

type postgresRepository struct {
  db *sql.DB
}

func NewPostgresRepository(url string) (Repository, error) {
  db, err := sql.Open("postgres", url)
  if err != nil {
    return nil, err
  }
  err = db.Ping()
  if err != nil {
    return nil, err
  }
  return &postgresRepository{db}, nil
}

gRPC

Служба gRPC службы учетных записей определяет следующие буферы протокола:

account/account.proto

syntax = "proto3";
package pb;

message Account {
  string id = 1;
  string name = 2;
}

message PostAccountRequest {
  string name = 1;
}

message PostAccountResponse {
  Account account = 1;
}

message GetAccountRequest {
  string id = 1;
}

message GetAccountResponse {
  Account account = 1;
}

message GetAccountsRequest {
  uint64 skip = 1;
  uint64 take = 2;
}

message GetAccountsResponse {
  repeated Account accounts = 1;
}

service AccountService {
  rpc PostAccount (PostAccountRequest) returns (PostAccountResponse) {}
  rpc GetAccount (GetAccountRequest) returns (GetAccountResponse) {}
  rpc GetAccounts (GetAccountsRequest) returns (GetAccountsResponse) {}
}

Так как этот пакет настроен наpb, поэтому сгенерированный код можно изменить сpbИспользование импорта подпакета.

Код gRPC может использовать Gogenerateкоординация командaccount/server.goКомментарии в верхней части файла компилируются и генерируются:

account/server.go

//go:generate protoc ./account.proto --go_out=plugins=grpc:./pb
package account

Запустите следующую команду, чтобы сгенерировать код дляpbПодкаталоги:

$ go generate account/server.go

сервер какServiceАдаптер сервисного интерфейса, соответствующий преобразованию типов запроса и возврата.

type grpcServer struct {
  service Service
}

func ListenGRPC(s Service, port int) error {
  lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
  if err != nil {
    return err
  }
  serv := grpc.NewServer()
  pb.RegisterAccountServiceServer(serv, &grpcServer{s})
  reflection.Register(serv)
  return serv.Serve(lis)
}

НижеPostAccountРеализация функции:

func (s *grpcServer) PostAccount(ctx context.Context, r *pb.PostAccountRequest) (*pb.PostAccountResponse, error) {
  a, err := s.service.PostAccount(ctx, r.Name)
  if err != nil {
    return nil, err
  }
  return &pb.PostAccountResponse{Account: &pb.Account{
    Id:   a.ID,
    Name: a.Name,
  }}, nil
}

использование

Сервер gRPC находится вaccount/cmd/account/main.goИнициализировать в файле:

type Config struct {
  DatabaseURL string `envconfig:"DATABASE_URL"`
}

func main() {
  var cfg Config
  err := envconfig.Process("", &cfg)
  if err != nil {
    log.Fatal(err)
  }

  var r account.Repository
  retry.ForeverSleep(2*time.Second, func(_ int) (err error) {
    r, err = account.NewPostgresRepository(cfg.DatabaseURL)
    if err != nil {
      log.Println(err)
    }
    return
  })
  defer r.Close()

  log.Println("Listening on port 8080...")
  s := account.NewService(r)
  log.Fatal(account.ListenGRPC(s, 8080))
}

Реализация клиентской структуры находится вaccount/client.goв файле. Это позволяет реализовать службу учетных записей без необходимости знать внутреннюю реализацию RPC, которую мы подробно обсудим позже.

account, err := accountClient.GetAccount(ctx, accountId)
if err != nil {
  log.Fatal(err)
}

служба каталогов

Служба каталогов обрабатывает товары в магазине Spidey. Он реализует функции, аналогичные службе учетных записей, но использует Elasticsearch для сохранения элементов.

Служить

Службы каталогов следуют следующему интерфейсу:

catalog/service.go

type Service interface {
  PostProduct(ctx context.Context, name, description string, price float64) (*Product, error)
  GetProduct(ctx context.Context, id string) (*Product, error)
  GetProducts(ctx context.Context, skip uint64, take uint64) ([]Product, error)
  GetProductsByIDs(ctx context.Context, ids []string) ([]Product, error)
  SearchProducts(ctx context.Context, query string, skip uint64, take uint64) ([]Product, error)
}

type Product struct {
  ID          string  `json:"id"`
  Name        string  `json:"name"`
  Description string  `json:"description"`
  Price       float64 `json:"price"`
}

база данных

Репозиторий на основе Elasticsearcholivere/elasticпакет для реализации.

catalog/repository.go

type Repository interface {
  Close()
  PutProduct(ctx context.Context, p Product) error
  GetProductByID(ctx context.Context, id string) (*Product, error)
  ListProducts(ctx context.Context, skip uint64, take uint64) ([]Product, error)
  ListProductsWithIDs(ctx context.Context, ids []string) ([]Product, error)
  SearchProducts(ctx context.Context, query string, skip uint64, take uint64) ([]Product, error)
}

Поскольку Elasticsearch хранит документы и идентификаторы отдельно, вспомогательная структура элемента реализована без идентификаторов:

type productDocument struct {
  Name        string  `json:"name"`
  Description string  `json:"description"`
  Price       float64 `json:"price"`
}

Вставьте элемент в базу данных:

func (r *elasticRepository) PutProduct(ctx context.Context, p Product) error {
  _, err := r.client.Index().
    Index("catalog").
    Type("product").
    Id(p.ID).
    BodyJson(productDocument{
      Name:        p.Name,
      Description: p.Description,
      Price:       p.Price,
    }).
    Do(ctx)
  return err
}

gRPC

Служба gRPC для служб каталогов определена вcatalog/catalog.protoфайл, и вcatalog/server.goреализован в. В отличие от службы учетных записей, она не определяет все конечные точки в интерфейсе службы.

catalog/catalog.proto

syntax = "proto3";
package pb;

message Product {
  string id = 1;
  string name = 2;
  string description = 3;
  double price = 4;
}

message PostProductRequest {
  string name = 1;
  string description = 2;
  double price = 3;
}

message PostProductResponse {
  Product product = 1;
}

message GetProductRequest {
  string id = 1;
}

message GetProductResponse {
  Product product = 1;
}

message GetProductsRequest {
  uint64 skip = 1;
  uint64 take = 2;
  repeated string ids = 3;
  string query = 4;
}

message GetProductsResponse {
  repeated Product products = 1;
}

service CatalogService {
  rpc PostProduct (PostProductRequest) returns (PostProductResponse) {}
  rpc GetProduct (GetProductRequest) returns (GetProductResponse) {}
  rpc GetProducts (GetProductsRequest) returns (GetProductsResponse) {}
}

несмотря на то чтоGetProductRequestСообщения содержат дополнительные поля, но ищутся и индексируются по идентификатору.

Код ниже показываетGetProductsРеализация функции:

catalog/server.go

func (s *grpcServer) GetProducts(ctx context.Context, r *pb.GetProductsRequest) (*pb.GetProductsResponse, error) {
  var res []Product
  var err error
  if r.Query != "" {
    res, err = s.service.SearchProducts(ctx, r.Query, r.Skip, r.Take)
  } else if len(r.Ids) != 0 {
    res, err = s.service.GetProductsByIDs(ctx, r.Ids)
  } else {
    res, err = s.service.GetProducts(ctx, r.Skip, r.Take)
  }
  if err != nil {
    log.Println(err)
    return nil, err
  }

  products := []*pb.Product{}
  for _, p := range res {
    products = append(
      products,
      &pb.Product{
        Id:          p.ID,
        Name:        p.Name,
        Description: p.Description,
        Price:       p.Price,
      },
    )
  }
  return &pb.GetProductsResponse{Products: products}, nil
}

Он определяет, какая сервисная функция вызывается при заданных параметрах. Его цель — имитировать конечную точку REST HTTP.

за/products?[ids=...]&[query=...]&skip=0&take=100В форме запросов относительно легко создать конечную точку для завершения вызова API.

Заказать услугу

Заказ Служба заказа сложнее. Ему нужно позвонить в службу учетной записи и каталога, чтобы подтвердить запрос, потому что заказ может быть создан только для определенной учетной записи и существующего элемента.

Service

ServiceИнтерфейс определяет интерфейс для создания и индексации всех заказов через аккаунт.

order/service.go

type Service interface {
  PostOrder(ctx context.Context, accountID string, products []OrderedProduct) (*Order, error)
  GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)
}

type Order struct {
  ID         string
  CreatedAt  time.Time
  TotalPrice float64
  AccountID  string
  Products   []OrderedProduct
}

type OrderedProduct struct {
  ID          string
  Name        string
  Description string
  Price       float64
  Quantity    uint32
}

база данных

Заказ может содержать несколько элементов, поэтому модель данных должна поддерживать эту форму. следующееorder_productsВ таблице идентификатор описан какproduct_idзаказанных товаров и количество таких товаров. иproduct_idПоля должны быть извлекаемыми из службы каталогов.

order/up.sql

CREATE TABLE IF NOT EXISTS orders (
  id CHAR(27) PRIMARY KEY,
  created_at TIMESTAMP WITH TIME ZONE NOT NULL,
  account_id CHAR(27) NOT NULL,
  total_price MONEY NOT NULL
);

CREATE TABLE IF NOT EXISTS order_products (
  order_id CHAR(27) REFERENCES orders (id) ON DELETE CASCADE,
  product_id CHAR(27),
  quantity INT NOT NULL,
  PRIMARY KEY (product_id, order_id)
);

RepositoryИнтерфейс прост:

order/repository.go

type Repository interface {
  Close()
  PutOrder(ctx context.Context, o Order) error
  GetOrdersForAccount(ctx context.Context, accountID string) ([]Order, error)
}

Но реализовать его непросто.

Заказ должен быть вставлен в два шага с использованием механизма транзакций, а затем запрошен оператором соединения.

Чтение заказа из базы данных требует синтаксического анализа табличной структуры и чтения ее в объектную структуру. Код ниже считывает элементы в заказ на основе идентификатора заказа:

orders := []Order{}
order := &Order{}
lastOrder := &Order{}
orderedProduct := &OrderedProduct{}
products := []OrderedProduct{}

// 将每行读取到 Order 结构体
for rows.Next() {
  if err = rows.Scan(
    &order.ID,
    &order.CreatedAt,
    &order.AccountID,
    &order.TotalPrice,
    &orderedProduct.ID,
    &orderedProduct.Quantity,
  ); err != nil {
    return nil, err
  }
  // 读取订单
  if lastOrder.ID != "" && lastOrder.ID != order.ID {
    newOrder := Order{
      ID:         lastOrder.ID,
      AccountID:  lastOrder.AccountID,
      CreatedAt:  lastOrder.CreatedAt,
      TotalPrice: lastOrder.TotalPrice,
      Products:   products,
    }
    orders = append(orders, newOrder)
    products = []OrderedProduct{}
  }
  // 读取商品
  products = append(products, OrderedProduct{
    ID:       orderedProduct.ID,
    Quantity: orderedProduct.Quantity,
  })

  *lastOrder = *order
}

// 添加最后一个订单 (或者第一个 :D)
if lastOrder != nil {
  newOrder := Order{
    ID:         lastOrder.ID,
    AccountID:  lastOrder.AccountID,
    CreatedAt:  lastOrder.CreatedAt,
    TotalPrice: lastOrder.TotalPrice,
    Products:   products,
  }
  orders = append(orders, newOrder)
}

gRPC

Серверу gRPC службы заказов необходимо установить соединение со службами учетных записей и каталогов при его реализации.

Буферы протокола определяются следующим образом:

order/order.proto

syntax = "proto3";
package pb;

message Order {
  message OrderProduct {
    string id = 1;
    string name = 2;
    string description = 3;
    double price = 4;
    uint32 quantity = 5;
  }

  string id = 1;
  bytes createdAt = 2;
  string accountId = 3;
  double totalPrice = 4;
  repeated OrderProduct products = 5;
}

message PostOrderRequest {
  message OrderProduct {
    string productId = 2;
    uint32 quantity = 3;
  }

  string accountId = 2;
  repeated OrderProduct products = 4;
}

message PostOrderResponse {
  Order order = 1;
}

message GetOrderRequest {
  string id = 1;
}

message GetOrderResponse {
  Order order = 1;
}

message GetOrdersForAccountRequest {
  string accountId = 1;
}

message GetOrdersForAccountResponse {
  repeated Order orders = 1;
}

service OrderService {
  rpc PostOrder (PostOrderRequest) returns (PostOrderResponse) {}
  rpc GetOrdersForAccount (GetOrdersForAccountRequest) returns (GetOrdersForAccountResponse) {}
}

Запуск службы заказов требует передачи URL-адреса другой службы:

order/server.go

type grpcServer struct {
  service       Service
  accountClient *account.Client
  catalogClient *catalog.Client
}

func ListenGRPC(s Service, accountURL, catalogURL string, port int) error {
  accountClient, err := account.NewClient(accountURL)
  if err != nil {
    return err
  }

  catalogClient, err := catalog.NewClient(catalogURL)
  if err != nil {
    accountClient.Close()
    return err
  }

  lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
  if err != nil {
    accountClient.Close()
    catalogClient.Close()
    return err
  }

  serv := grpc.NewServer()
  pb.RegisterOrderServiceServer(serv, &grpcServer{
    s,
    accountClient,
    catalogClient,
  })
  reflection.Register(serv)

  return serv.Serve(lis)
}

Создание заказа включает вызов службы учетной записи, проверку существования учетной записи, а затем то же самое для продукта. Цена продукта также считывается при расчете общей стоимости. Вы не хотите, чтобы пользователи могли указать общую стоимость своего товара.

func (s *grpcServer) PostOrder(
  ctx context.Context,
  r *pb.PostOrderRequest,
) (*pb.PostOrderResponse, error) {
  // 检查账户是否存在
  _, err := s.accountClient.GetAccount(ctx, r.AccountId)
  if err != nil {
    log.Println(err)
    return nil, err
  }

  // 获取订单商品
  productIDs := []string{}
  for _, p := range r.Products {
    productIDs = append(productIDs, p.ProductId)
  }
  orderedProducts, err := s.catalogClient.GetProducts(ctx, 0, 0, productIDs, "")
  if err != nil {
    log.Println(err)
    return nil, err
  }

  // 构造商品
  products := []OrderedProduct{}
  for _, p := range orderedProducts {
    product := OrderedProduct{
      ID:          p.ID,
      Quantity:    0,
      Price:       p.Price,
      Name:        p.Name,
      Description: p.Description,
    }
    for _, rp := range r.Products {
      if rp.ProductId == p.ID {
        product.Quantity = rp.Quantity
        break
      }
    }

    if product.Quantity != 0 {
      products = append(products, product)
    }
  }

  // 调用服务实现
  order, err := s.service.PostOrder(ctx, r.AccountId, products)
  if err != nil {
    log.Println(err)
    return nil, err
  }

  // 创建订单响应
  orderProto := &pb.Order{
    Id:         order.ID,
    AccountId:  order.AccountID,
    TotalPrice: order.TotalPrice,
    Products:   []*pb.Order_OrderProduct{},
  }
  orderProto.CreatedAt, _ = order.CreatedAt.MarshalBinary()
  for _, p := range order.Products {
    orderProto.Products = append(orderProto.Products, &pb.Order_OrderProduct{
      Id:          p.ID,
      Name:        p.Name,
      Description: p.Description,
      Price:       p.Price,
      Quantity:    p.Quantity,
    })
  }
  return &pb.PostOrderResponse{
    Order: orderProto,
  }, nil
}

При запросе заказа для конкретной учетной записи необходимо позвонить в службу каталогов, потому что требуются детали продукта.

Служба GraphQL

Схема GraphQL определена вgraphql/schema.graphqlВ файле:

scalar Time

type Account {
  id: String!
  name: String!
  orders: [Order!]!
}

type Product {
  id: String!
  name: String!
  description: String!
  price: Float!
}

type Order {
  id: String!
  createdAt: Time!
  totalPrice: Float!
  products: [OrderedProduct!]!
}

type OrderedProduct {
  id: String!
  name: String!
  description: String!
  price: Float!
  quantity: Int!
}

input PaginationInput {
  skip: Int
  take: Int
}

input AccountInput {
  name: String!
}

input ProductInput {
  name: String!
  description: String!
  price: Float!
}

input OrderProductInput {
  id: String!
  quantity: Int!
}

input OrderInput {
  accountId: String!
  products: [OrderProductInput!]!
}

type Mutation {
  createAccount(account: AccountInput!): Account
  createProduct(product: ProductInput!): Product
  createOrder(order: OrderInput!): Order
}

type Query {
  accounts(pagination: PaginationInput, id: String): [Account!]!
  products(pagination: PaginationInput, query: String, id: String): [Product!]!
}

gqlgenИнструмент генерирует кучу типов, но также долженOrderМодель выполняет некоторое управление вgraphql/types.jsonфайл, чтобы модель не генерировалась автоматически:

{
  "Order": "github.com/tinrab/spidey/graphql/graph.Order"
}

Теперь это можно сделать вручнуюOrderСтруктурировано:

graphql/graph/models.go

package graph

import time "time"

type Order struct {
  ID         string           `json:"id"`
  CreatedAt  time.Time        `json:"createdAt"`
  TotalPrice float64          `json:"totalPrice"`
  Products   []OrderedProduct `json:"products"`
}

Сгенерируйте директивы типа вgraphql/graph/graph.goверхняя:

//go:generate gqlgen -schema ../schema.graphql -typemap ../types.json
package graph

Запустите его с помощью следующей команды:

$ go generate ./graphql/graph/graph.go

Сервер GraphQL ссылается на все остальные службы.

graphql/graph/graph.go

type GraphQLServer struct {
  accountClient *account.Client
  catalogClient *catalog.Client
  orderClient   *order.Client
}

func NewGraphQLServer(accountUrl, catalogURL, orderURL string) (*GraphQLServer, error) {
  // 连接账户服务
  accountClient, err := account.NewClient(accountUrl)
  if err != nil {
    return nil, err
  }

  // 连接目录服务
  catalogClient, err := catalog.NewClient(catalogURL)
  if err != nil {
    accountClient.Close()
    return nil, err
  }

  // 连接订单服务
  orderClient, err := order.NewClient(orderURL)
  if err != nil {
    accountClient.Close()
    catalogClient.Close()
    return nil, err
  }

  return &GraphQLServer{
    accountClient,
    catalogClient,
    orderClient,
  }, nil
}

GraphQLServerСтруктура должна реализовать все сгенерированные преобразователи. Мутация может быть обнаружена вgraphql/graph/mutations.goможно найти в запросе (Query) можно найти вgraphql/graph/queries.goнайти в.

Операция модификации реализуется путем вызова соответствующего клиента службы и передачи параметров:

func (s *GraphQLServer) Mutation_createAccount(ctx context.Context, in AccountInput) (*Account, error) {
  ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
  defer cancel()

  a, err := s.accountClient.PostAccount(ctx, in.Name)
  if err != nil {
    log.Println(err)
    return nil, err
  }

  return &Account{
    ID:   a.ID,
    Name: a.Name,
  }, nil
}

Запросы могут быть вложены друг в друга. В Spidey запрос учетной записи может также запрашивать ее заказы, см.Account_ordersфункция.

func (s *GraphQLServer) Query_accounts(ctx context.Context, pagination *PaginationInput, id *string) ([]Account, error) {
  // 会被首先调用
  // ...
}

func (s *GraphQLServer) Account_orders(ctx context.Context, obj *Account) ([]Order, error) {
  // 然后执行这个函数,返回 "obj" 账户的订单
  // ...
}

Суммировать

Запустите Spidey, выполнив следующую команду:

$ vgo vendor
$ docker-compose up -d --build

Затем вы можете получить к нему доступ в своем браузереhttp://localhost:8000/playgroundДавайте создадим учетную запись с помощью инструмента GraphQL:

mutation {
  createAccount(account: {name: "John"}) {
    id
    name
  }
}

Возвращаемый результат:

{
  "data": {
    "createAccount": {
      "id": "15t4u0du7t6vm9SRa4m3PrtREHb",
      "name": "John"
    }
  }
}

Затем могут быть созданы некоторые продукты:

mutation {
  a: createProduct(product: {name: "Kindle Oasis", description: "Kindle Oasis is the first waterproof Kindle with our largest 7-inch 300 ppi display, now with Audible when paired with Bluetooth.", price: 300}) { id },
  b: createProduct(product: {name: "Samsung Galaxy S9", description: "Discover Galaxy S9 and S9+ and the revolutionary camera that adapts like the human eye.", price: 720}) { id },
  c: createProduct(product: {name: "Sony PlayStation 4", description: "The PlayStation 4 is an eighth-generation home video game console developed by Sony Interactive Entertainment", price: 300}) { id },
  d: createProduct(product: {name: "ASUS ZenBook Pro UX550VE", description: "Designed to entice. Crafted to perform.", price: 300}) { id },
  e: createProduct(product: {name: "Mpow PC Headset 3.5mm", description: "Computer Headset with Microphone Noise Cancelling, Lightweight PC Headset Wired Headphones, Business Headset for Skype, Webinar, Phone, Call Center", price: 43}) { id }
}

Обратите внимание на возвращенное значение идентификатора:

{
  "data": {
    "a": {
      "id": "15t7jjANR47uODEPUIy1od5APnC"
    },
    "b": {
      "id": "15t7jsTyrvs1m4EYu7TCes1EN5z"
    },
    "c": {
      "id": "15t7jrfDhZKgxOdIcEtTUsriAsY"
    },
    "d": {
      "id": "15t7jpKt4VkJ5iHbwt4rB5xR77w"
    },
    "e": {
      "id": "15t7jsYs0YzK3B7drQuf1mX5Dyg"
    }
  }
}

Затем инициируйте несколько заказов:

mutation {
  createOrder(order: { accountId: "15t4u0du7t6vm9SRa4m3PrtREHb", products: [
    { id: "15t7jjANR47uODEPUIy1od5APnC", quantity: 2 },
    { id: "15t7jpKt4VkJ5iHbwt4rB5xR77w", quantity: 1 },
    { id: "15t7jrfDhZKgxOdIcEtTUsriAsY", quantity: 5 }
  ]}) {
    id
    createdAt
    totalPrice
  }
}

Сравните возвращенную комиссию с возвращенным результатом:

{
  "data": {
    "createOrder": {
      "id": "15t8B6lkg80ZINTASts92nBzyE8",
      "createdAt": "2018-06-11T21:18:18Z",
      "totalPrice": 2400
    }
  }
}

Пожалуйста, смотрите полный кодGitHub.

Если вы обнаружите ошибки в переводе или в других областях, требующих доработки, добро пожаловать наПрограмма перевода самородковВы также можете получить соответствующие бонусные баллы за доработку перевода и PR. начало статьиПостоянная ссылка на эту статьюЭто ссылка MarkDown этой статьи на GitHub.


Программа перевода самородковэто сообщество, которое переводит высококачественные технические статьи из Интернета сНаггетсДелитесь статьями на английском языке на . Охват контентаAndroid,iOS,внешний интерфейс,задняя часть,блокчейн,продукт,дизайн,искусственный интеллекти другие поля, если вы хотите видеть больше качественных переводов, пожалуйста, продолжайте обращать вниманиеПрограмма перевода самородков,официальный Вейбо,Знай колонку.