Использование graphQL+gRPC+Golang для создания микросервиса аренды велосипедов с персистентностью на ArangoDB.


Вход в полноэкранный режим Выход из полноэкранного режима

ArangoDB, Golang, Grpc (святая троица микросервисов)

В этой серии из двух частей я хочу построить простое микросервисное приложение для аренды велосипедов, чтобы продемонстрировать интеграцию gRPC с graphql-клиентом с помощью go и сохранение данных в базе данных ArangoDb. Это будет разделено на следующие части:

  1. В первой части этой серии мы разработаем два API на основе фреймворка gRPC, «Users API», который будет представлять людей, которые будут арендовать наши велосипеды. Второй API будет «Bikes API», затем мы заставим эти два сервиса поддерживать независимые экземпляры баз данных в базе данных ArangoDB.

  2. Во второй части мы свяжем два вышеупомянутых сервиса с graphql-клиентом.

Код этого проекта можно найти «здесь».

Технологии, архитектуры и FrameWorks будут реализованы и использованы следующие:

Golang:

Официально известен как язык программирования Go. Go — это язык, который был создан с нуля для параллелизма. Он был разработан и создан инженерами Google. Это статически типизированный, нативно компилируемый, собираемый из мусора, параллельный, пост-OOP (объектно-ориентированный язык).
В современном ландшафте распределенных облачных вычислений, небольшая справка: распределенная система — это совокупность независимых компьютеров, которая представляется пользователям как единая целостная система. Сейчас Golang особенно сияет в этой сфере, используя параллелизм. Параллелизм в Go — это способность функций выполняться независимо друг от друга. Механизмы параллелизма позволяют легко писать программы, которые максимально используют возможности многоядерных и сетевых машин (распределенных систем). Для этого используются Goroutines — независимые единицы работы, которые планируются и затем выполняются на доступном логическом процессоре. Ознакомьтесь с их официальным сайтом.

Микросервисы.

Архитектура микросервисов позволяет разработчикам декомпозировать наши приложения на относительно небольшие приложения (сервисы), слабо связанные друг с другом. Эти небольшие приложения могут быть созданы для решения конкретных уникальных бизнес-задач независимо друг от друга с использованием различных программных и аппаратных стеков, а развертывание и масштабирование после запуска также может быть выполнено независимо.

Такая структура обеспечивает множество преимуществ, таких как гибкость в разработке, надежность в развертывании и эксплуатации и модульность в масштабировании. Погрузитесь глубже здесь.

gRPC (google Remote Procedure Call)

Это фреймворк RPC (Remote Procedure Call), разработанный инженерами google. Он позволяет нам внедрять интерфейсы прикладного программирования (API) на основе RPC на HTTP/2 в отличие от API на основе REST.

Благодаря использованию HTTP/2 gRPC является более эффективным, HTTP/2 может выполнять несколько запросов параллельно на долгоживущем соединении в потокобезопасном режиме. Полезная нагрузка в gRPC основана на двоичном формате, что делает ее меньше, чем сообщения на основе JSON. Кроме того, HTTP/2 имеет встроенную функцию сжатия заголовков.
Посетите их сайт

GrapQL

Graphql — это язык запросов API, который был разработан для устранения недостатков REST, таких как часто встречающаяся избыточная и недостаточная выборка. Он был специально разработан для обеспечения гибкости и эффективности. Он также позволяет быстро итерации продукта на нашем front-end. Подробнее об этом здесь.

Буфер протокола

Protobuf — это независимый от платформы и языка метод сериализации данных, который позволяет нам изначально описывать наши данные в виде сообщений. Затем он позволяет нам определить набор операций над «сообщениями», которые мы только что определили в формате запрос/ответ. Погрузиться глубже в эту тему можно здесь.

Генераторы кода

Мы будем использовать генераторы кода в нашем процессе конструирования для создания некоторых частей кода нашего приложения. Эта возможность позволит нам потратить время на создание бизнес-логики сервисов, а не на низкоуровневую логику реализации API, о которой мы должны позаботиться автоматически. Мы будем использовать Prototool в этой первой части и gqlgen во второй.

ArangoDB

Это база данных NOSQL, созданная для обеспечения высокой доступности и масштабируемости, идеально подходящая для реализации персистентности в микросервисах. ArangoDB — это нативная мультимодельная база данных с открытым исходным кодом, которая поддерживает графовые, документальные и ключевые модели данных, позволяя пользователям свободно комбинировать все модели данных в одном запросе. Более подробно об этой базе данных и ее возможностях читайте здесь.

Начнем ….

Прежде всего, давайте убедимся, что все наши генераторы кода установлены и работают,

установка prototool:

Prototool в первую очередь обеспечивает централизованное расположение для поддержания согласованности файлов Protobuf в приложениях, особенно в больших приложениях.
Для пользователей linux,

curl -sSL 
https://github.com/uber/prototool/releases/download/v1.8.0/prototool-$(uname -s)-$(uname -m) 
  -o /usr/local/bin/prototool && 
  chmod +x /usr/local/bin/prototool 
Войдите в полноэкранный режим Выйти из полноэкранного режима

или просто скачайте prototool-Linux-x86_64.tar.gz и затем распакуйте двоичный файл, поместив его в папку /usr/local/bin, чтобы сделать его исполняемым

Проверьте версию, чтобы убедиться в успешности установки:

mykmyk@skynet:~$ prototool version
Version:                 1.9.0
Default protoc version:  3.8.0
Go version:              go1.12.4
OS/Arch:                 linux/amd64
Войти в полноэкранный режим Выйти из полноэкранного режима

Погрузитесь глубже в тему prototool здесь.

Теперь давайте установим protoc…

Полное название — protobuf compiler, он используется для компиляции .proto файла. Погрузиться глубже можно здесь.

Для пользователей linux:

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28


$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
Войти в полноэкранный режим Выйти из полноэкранного режима

обновите переменную окружения PATH, чтобы помочь компилятору найти вышеупомянутый плагин «protoc-gen-go-grpc».

$ export PATH="$PATH:$(go env GOPATH)/bin"
Войдите в полноэкранный режим Выйти из полноэкранного режима

GOPATH должен быть корневой папкой для бинарных файлов, расположенных в $GOPATH/bin, и должен быть корневой папкой для проектов.
Чтобы убедиться в успешности вышеуказанной установки, используйте следующую команду:

mykmyk@skynet:~$ protoc --version
libprotoc 3.15.8
Войти в полноэкранный режим Выйти из полноэкранного режима

Архитектура проекта

Готовый проект будет состоять из трех сервисов: двух микросервисов, которые мы построим в этой первой части, и сервиса-шлюза graphql, над которым мы будем работать во второй части этой серии.
Шлюз должен быть входом для всех клиентских запросов, связанных с велосипедами и их арендаторами, а два сервиса будут хранить свои собственные данные в ArangoDB. Когда арендатор арендует велосипед, арендатор и велосипед связаны идентификатором велосипеда. На основе этой связи мы можем получить ID арендатора, используя ID велосипеда.

Структура файлов проекта

Создайте корневую папку «bikerenting» с подпапками,

  • gen для хранения нашего сгенерированного Go кода,
  • Proto для хранения наших файлов proto,
  • bikes для хранения нашей реализации API bikes,
  • rentees для хранения нашей реализации API rentees и — graph_api для размещения нашей реализации graphl-шлюза.
  • dockerfiles для целей контейнеризации.

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting$ ll
total 32
drwxrwxr-x  8 mykmyk mykmyk 4096 Jun  1 23:18 ./
drwxrwxr-x 24 mykmyk mykmyk 4096 Jun  1 23:16 ../
drwxrwxr-x  2 mykmyk mykmyk 4096 Jun  1 23:17 bikes/
drwxrwxr-x  2 mykmyk mykmyk 4096 Jun  1 23:17 db/
-rw-rw-r--  1 mykmyk mykmyk    0 Jun  1 23:18 docker-compose.yml
-rw-rw-r--  1 mykmyk mykmyk    0 Jun  1 23:18 Dockerfile.dev
drwxrwxr-x  2 mykmyk mykmyk 4096 Jun  1 23:17 gen/
drwxrwxr-x  2 mykmyk mykmyk 4096 Jun  1 23:17 graph_api/
drwxrwxr-x  2 mykmyk mykmyk 4096 Jun  1 23:17 proto/
drwxrwxr-x  2 mykmyk mykmyk 4096 Jun  1 23:17 rentees/

Вход в полноэкранный режим Выход из полноэкранного режима

Далее мы хотим создать конфигурационный файл prototool.yaml с помощью инструмента генерации кода prototool, который мы установили в начале этого проекта. Чтобы создать файл prototool.yaml в корневой папке проекта, выполним следующую команду:

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting$ prototool config init
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы заметите, что будет создан файл «prototool.yaml»,

  1 protoc:                                                                     
  2   version: 3.8.0
  3 lint:
  4   group: uber2
Войти в полноэкранный режим Выйти из полноэкранного режима

теперь добавьте следующую информацию;

1 protoc:
  2   version: 3.8.0
  3 lint:
  4   group: uber2
  5 
  6 generate:                                                                       
  7   go_options:                                                                   
  8     import_path: bikerenting
  9   plugins:
 10     - name: go
 11       type: go
 12       flags: plugins=grpc
 13       output: gen/go      

Вход в полноэкранный режим Выйти из полноэкранного режима

В приведенном выше коде мы просто указываем пути для размещения/хранения нашего сгенерированного кода Go.

Создание нашего API для велосипедов и проката

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

Давайте «cd» в корневую папку proto, которую мы создали выше, и создадим две новые папки для хранения наших соответствующих файлов proto, папку bikes и папку rentees, это будет выглядеть следующим образом.

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/proto$ ll
total 16
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun  1 23:44 ./
drwxrwxr-x 8 mykmyk mykmyk 4096 Jun  1 23:36 ../
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  1 23:44 bikes/
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  1 23:44 rentees/
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь перейдите в папку «bikes» и создайте следующие файлы;

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/proto/bikes$ ll
total 8
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  1 23:46 ./
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun  1 23:44 ../
-rw-rw-r-- 1 mykmyk mykmyk    0 Jun  1 23:46 bikes_messages.proto
-rw-rw-r-- 1 mykmyk mykmyk    0 Jun  1 23:46 bikes.proto

Вход в полноэкранный режим Выйти из полноэкранного режима

откройте файл «bikes_messages.proto» и добавьте следующий код:

  1 syntax = "proto3";
  2 
  3 package bikerenting.grpc.bikes.v1;
  4 option go_package = "bikes";
  5 
  6 // Bike definition                                                          
  7 
  8 message Bike {
  9   string id     = 1;
 10   string owner_name = 2;
 11   string type   = 3;
 12   string make   = 4;
 13   string serial = 5;
 14  }
Войти в полноэкранный режим Выйти из полноэкранного режима

Затем откройте файл «bikes.proto» и добавьте следующий код:

syntax = "proto3";
package bikerenting.grpc.bikes.v1;
option go_package = "bikes";
import "proto/bikes/bikes_messages.proto";


//API for managing bikes
service BikesAPI {
    //Get all bikes
    rpc ListBikes(ListBikesRequest) returns (ListBikesResponse);
    //Get bike by id
    rpc GetBike(GetBikeRequest)     returns (GetBikeResponse);
    //Get bikes by ids
    rpc GetBikes(GetBikesRequest)   returns (GetBikesResponse);
    // Get bikes by type
    rpc GetBikesByTYPE(GetBikesByTYPERequest) returns (GetBikesByTYPEResponse);
    // Get bikes by make
    rpc GetBikesByMAKE(GetBikesByMAKERequest) returns (GetBikesByMAKEResponse);
    // Get bikes by owner_name
    rpc GetBikesByOWNER(GetBikesByOWNERRequest) returns (GetBikesByOWNERResponse);
    // Add new bike
    rpc AddBike(AddBikeRequest)     returns  (AddBikeResponse);
    // Delete bike
    rpc DeleteBike(DeleteBikeRequest) returns (DeleteBikeResponse);
  }

  message ListBikesRequest {
  }
  message ListBikesResponse {
     repeated Bike bikes = 1;
  }

  message GetBikeRequest {
    string id = 1;
  }
  message GetBikeResponse {
    Bike bike = 1;
  }

  message GetBikesRequest {
    repeated string ids = 1;
  }
  message GetBikesResponse {
    repeated Bike bikes = 1;
  }

  message GetBikesByTYPERequest {
    string type = 1;
  }
  message GetBikeByTYPEResponse {
    repeated Bike bikes = 1;
  }

  message GetBikesByMAKERequest {
    string make = 1;
  }
  message GetBikesByMAKEResponse {
    repeated Bike bikes = 1;
  }

  message GetBikesByOWNERRequest {
    string owner_name = 1;
  }
  message GetBikesByOWNERResponse {
    repeated Bike bikes = 1;
  }

  message AddBikeRequest {
    Bike bike = 1;
  }
  message AddBikeResponse {
    Bike bike = 1;
  }

  message DeleteBikeRequest {
    string id = 1;
  }

  message DeleteBikeResponse { 
  }
Вход в полноэкранный режим Выход из полноэкранного режима

После определения двух файлов, один для хранения нашей структуры сообщений, а другой для определения операций над нашей структурой.
Далее выполните команды «go mod init» и «go mod tidy» в терминале, находясь в папке bikes.

В нашем коде выше

;
Proto3; (означает protocol buffers version 3) указывает, какой протокол мы используем для определения нашего API, генератор кода будет использовать эту версию для интерпретации наших определенных сообщений и операций.

option go_package = «bikes»; — В этом случае мы локально указываем путь импорта Go в этом .proto файле.

package package bikerenting.grpc.bikes.v1; — это позволяет генератору поместить сгенерированный код заглушки сервера-клиента bikes в это место.

Для наших арендаторов код будет иметь схожую структуру, откройте папку proto/rentees и создайте следующие файлы.

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/proto/rentees$ ll
total 8
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  2 00:45 ./
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun  1 23:44 ../
-rw-rw-r-- 1 mykmyk mykmyk    0 Jun  2 00:45 rentees_messages.proto
-rw-rw-r-- 1 mykmyk mykmyk    0 Jun  2 00:45 rentees.proto

Вход в полноэкранный режим Выход из полноэкранного режима

Файл «rentees_messages.proto» будет выглядеть следующим образом:

  1 syntax = "proto3";                                                          
  2 package bikerenting.grpc.rentees.v1;
  3 option go_package ="rentees";
  4 
  5 // Customer definition
  6 message Rentee {
  7   string id         = 1;
  8   string first_name = 2;
  9   string last_name  = 3;
 10   string National_Id_Number = 4;
 11   string phone      = 5;
 12   string email      = 6;
 13   repeated string held_bikes = 7;
 14 }
Вход в полноэкранный режим Выйти из полноэкранного режима

Файл «rentee.proto» будет выглядеть следующим образом:

syntax = "proto3";
package tutorial.grpc.rentees.v1;
option go_package = "rentees";
import "proto/rentees/rentees_messages.proto";

//API for managing rentees
service RenteesAPI {
    //Get all rentees
    rpc ListRentees(ListRenteesRequest) returns (ListRenteesResponse);
    // Get rentee by bike id
    rpc GetRenteeByBikeId(GetRenteeByBikeIdRequest) returns (GetRenteeByBikeIdResponse);
    // Get rentee by bike type
    rpc GetRenteesByBikeTYPE(GetRenteesByBikeTYPERequest) returns (GetRenteesByBikeTYPEResponse);
    // Get rentee by bike make
    rpc GetRenteesByBikeMAKE(GetRenteesByBikeMAKERequest) returns (GetRenteesByBikeMAKEResponse);
    // Get rentee by bike owner
    rpc GetRenteesByBikeOWNER(GetRenteesByBikeOWNERRequest) returns (GetRenteesByBikeOWNERResponse);   
    // Get rentee by id
    rpc GetRentee(GetRenteeRequest) returns (GetRenteeResponse);
    // Add new rentee
    rpc AddRentee(AddRenteeRequest) returns (AddRenteeResponse);
    // Update rentee
    rpc UpdateRentee(UpdateRenteeRequest) returns (UpdateRenteeResponse);
}

message ListRenteesRequest {
}

message ListRenteesResponse {
  repeated Rentee rentees = 1;
}

message GetRenteeByBikeIdRequest{
  string id = 1;
}
message GetRenteeByBikeIdResponse {
  Rentee rentee = 1;
}

message GetRenteesByBikeTYPERequest{
  string type = 1;
}
message GetRenteeByBikeTYPEResponse {
  repeated Rentee rentees = 1;
}

message GetRenteeByBikeMAKERequest{
  string make = 1;
}
message GetRenteeByBikeMAKEResponse {
  repeated Rentee rentees = 1;
}

message GetRenteeByBikeOWNERRequest{
  string owner_name = 1;
}
message GetRenteeByBikeOWNERResponse {
  repeated Rentee rentees = 1;
}

message GetRenteeRequest {
  string id = 1 ;
}
message GetRenteeResponse {
  Rentee rentee = 1;
}

message AddRenteeRequest {
  Rentee rentee = 1;
}
message AddRenteeResponse {
  Rentee rentee = 1;
}

message UpdateRenteeRequest {
  Rentee rentee = 1;
}
message UpdateRenteeResponse {
  Rentee rentee = 1;
}
Вход в полноэкранный режим Выход из полноэкранного режима

После определения наших API rentees и bikes в наших .proto файлах, выполните команды «go mod init» и затем «go mod tidy». Теперь мы будем генерировать код для соответствующих клиент-серверных модулей, используя наш протоинструмент:
В нашей корневой папке, т.е. «bikerenting», где находится наш файл prototool.yaml, выполните следующую команду:

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting$ prototool generate
Войти в полноэкранный режим Выйти из полноэкранного режима

Prototool найдет все наши протофайлы и использует конфигурацию prototool.yaml для создания заглушек наших клиент-серверных сервисных интерфейсов по пути, который мы указали для размещения этих файлов в соответствии с нашими конфигурациями в файле prototool.yaml. Теперь, если вы перейдете в папку «gen/go/proto». Мы обнаружим автогенерированные модули поддержки, оболочка для серверов API и клиента готова, и таким образом мы можем сосредоточиться на создании бизнес-логики или контроллера (или представления — Django) для наших двух API,
Папка «gen/go/proto» выглядит как :

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/gen/go/proto$ ll
total 16
drwxrwxr-x 4 mykmyk mykmyk 4096 Jun  2 01:28 ./
drwxr--r-- 3 mykmyk mykmyk 4096 Jun  2 01:28 ../
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  2 01:28 bikes/
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  2 01:28 rentees/
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь, если мы «cd» в папку bikes, мы найдем структуру ниже:

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/gen/go/proto/bikes$ ll
total 40
drwxrwxr-x 2 mykmyk mykmyk  4096 Jun  2 01:28 ./
drwxrwxr-x 4 mykmyk mykmyk  4096 Jun  2 01:28 ../
-rw-rw-r-- 1 mykmyk mykmyk  2728 Jun  2 01:28 bikes_messages.pb.go
-rw-rw-r-- 1 mykmyk mykmyk 25373 Jun  2 01:28 bikes.pb.go

Вход в полноэкранный режим Выход из полноэкранного режима

Если мы «cd» в папку rentees, мы найдем следующую структуру:

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/gen/go/proto/rentees$ ll
total 40
drwxrwxr-x 2 mykmyk mykmyk  4096 Jun  2 01:28 ./
drwxrwxr-x 4 mykmyk mykmyk  4096 Jun  2 01:28 ../
-rw-rw-r-- 1 mykmyk mykmyk  3637 Jun  2 01:28 rentees_messages.pb.go
-rw-rw-r-- 1 mykmyk mykmyk 27424 Jun  2 01:28 rentees.pb.go

Вход в полноэкранный режим Выход из полноэкранного режима

Вы можете заглянуть в этот файл, чтобы проверить сериализацию, которую выполнил grpc для наших сообщений:
Убедитесь, что вы запустили команды «go mod init» и «go mod tidy» как в папке rentees, так и в папке bikes.

Создание подключения к базе данных…..

Скачайте и установите базу данных Arangodb, на официальной странице здесь есть легкая инструкция по этому процессу (скачайте версию для сообщества).

PS: если вы столкнулись с ошибкой «

Setting up arangodb3 (3.2.10) ... FATAL ERROR: EXIT_FAILED - "exit with error" dpkg: error processing package arangodb3 (--configure): subprocess installed post-installation script returned error exit status 1 Errors were encountered while processing: arangodb3 E: Sub-process /usr/bin/dpkg returned an error code (1)
Войдите в полноэкранный режим Выход из полноэкранного режима

посмотрите мой ответ на эту ошибку на stack-overflow здесь

Чтобы убедиться, что база данных установлена правильно, используйте команду «arangosh», как показано ниже:

mykmyk@skynet:~$ arangosh
Please specify a password: 

                                       _     
  __ _ _ __ __ _ _ __   __ _  ___  ___| |__  
 / _` | '__/ _` | '_  / _` |/ _ / __| '_  
| (_| | | | (_| | | | | (_| | (_) __  | | |
 __,_|_|  __,_|_| |_|__, |___/|___/_| |_|
                       |___/                 

arangosh (ArangoDB 3.3.0 [linux] 64bit, using jemalloc, VPack 0.1.30, RocksDB 5.6.0, ICU 58.1, V8 5.7.492.77, OpenSSL 1.1.0f  25 May 2017)
Copyright (c) ArangoDB GmbH

Pretty printing values.
Connected to ArangoDB 'http+tcp://127.0.0.1:8529' version: 3.3.0 [server], database: '_system', username: 'root'

Please note that a new minor version '3.7.11' is available
Type 'tutorial' for a tutorial or 'help' to see common examples
127.0.0.1:8529@_system> 
Войти в полноэкранный режим Выйти из полноэкранного режима

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

127.0.0.1:8529@_system> db._createDatabase("bikesrentees_db");
true

127.0.0.1:8529@_system> var users = require("@arangodb/users");

127.0.0.1:8529@_system> users.save("root@bikesrentees","rootpassword");
{ 
  "user" : "root@bikesrentees", 
  "active" : true, 
  "extra" : { 
  }, 
  "code" : 201 
}

127.0.0.1:8529@_system> users.grantDatabase("root@bikesrentees", "bikesrentees_db");

127.0.0.1:8529@_system> 

Войти в полноэкранный режим Выйти из полноэкранного режима

После создания базы данных и пользователя, сохраните учетные данные и информацию и из нашей корневой папки перейдите в папку «db», в папке db, что означает database, создайте два файла, connect.go и query.go,
Теперь эта папка будет выглядеть следующим образом:

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/db$ ll
total 8
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  2 02:06 ./
drwxrwxr-x 8 mykmyk mykmyk 4096 Jun  1 23:36 ../
-rw-rw-r-- 1 mykmyk mykmyk    0 Jun  2 02:06 connect.go
-rw-rw-r-- 1 mykmyk mykmyk    0 Jun  2 02:06 query.go

Вход в полноэкранный режим Выход из полноэкранного режима

Файл connect.go будет содержать код, который мы будем вызывать, когда захотим установить соединение с нашей базой данных arangodb, которую мы только что создали;

Код будет выглядеть как показано ниже, обратите внимание, что мы вставляем databasename, databaseuser и т.д:

package db

import (
    "context"
    "fmt"
    "log"
    "os"
    "github.com/joho/godotenv"
    "github.com/arangodb/go-driver"
    "github.com/arangodb/go-driver/http"
)

func parseEnvVars(key string) string {
    //load .env file
    err := godotenv.Load(".env")

    if err != nil {
       log.Fatalf("Error loading .env file")
    }

    return os.Getenv(key)
}


const (
    DbHost     = "http://127.0.0.1"
    DbPort     = "8529"
    DbUserName = "root@bikesrentees_db"
    DbPassword = "rootpassword"
)


type DatabaseConfig struct {
    Host     string
    Port     string
    Username string
    Password string
    DatabaseName string
}


func Connect(ctx context.Context, config DatabaseConfig)(db driver.Database, err error){
    conn, err := http.NewConnection(http.ConnectionConfig{
        Endpoints: []string{fmt.Sprintf("%s:%s", config.Host, config.Port)},
    })
    if err != nil {
        return nil, err
    }
    cl, err := driver.NewClient(driver.ClientConfig{
        Connection: conn,
        Authentication: driver.BasicAuthentication(config.Username, config.Password),
    })

    if err != nil {
        return nil, err
    }

    db, err = cl.Database(ctx, config.DatabaseName)
    if driver.IsNotFound(err) {
        db, err = cl.CreateDatabase(ctx, config.DatabaseName, nil)
    }
    return db, err
}


func AttachCollection(ctx context.Context, db driver.Database, colName string)(driver.Collection, error){
    col, err := db.Collection(ctx, colName)
    if err != nil {
        if driver.IsNotFound(err){
            col, err = db.CreateCollection(ctx, colName, nil)
        }
    }
    return col, err
}

func GetDbConfig() DatabaseConfig{
    dbName := parseEnvVars("ARANGODB_DB")
    if dbName == "" {
        log.Fatalf("Failed to load environment variable '%s'", "ARANGODB_DB")
    }
    return DatabaseConfig {
        Host:     DbHost,
        Port:     DbPort,
        Username: DbUserName,
        Password: DbPassword,
        DatabaseName: dbName,
    }
}
Вход в полноэкранный режим Выйти из полноэкранного режима

Далее откройте наш файл query.go и добавьте следующий код

package db

import "fmt"

func ListRecords(collectionName string) string {
    const listRecordsQuery =   `
    FOR record IN %s
        RETURN record`
    return fmt.Sprintf(listRecordsQuery, collectionName)
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Не забудьте выполнить ставшую уже монотонной пару команд «go mod init» и «go mod tidy» в папке db.
Теперь, когда у нас есть подключение к базе данных и логика запросов, давайте, наконец, создадим наши серверы rentees и bikes.
Из корня папки проекта перейдите в папку bikes, создайте папку server и файл main.go, папка будет выглядеть как показано ниже:

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/bikes$ ll
total 12
drwxrwxr-x 3 mykmyk mykmyk 4096 Jun  2 02:19 ./
drwxrwxr-x 8 mykmyk mykmyk 4096 Jun  1 23:36 ../
-rw-rw-r-- 1 mykmyk mykmyk    0 Jun  2 02:19 main.go
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  2 02:19 server/

Вход в полноэкранный режим Выход из полноэкранного режима

«cd» в только что созданную папку server и создайте файл под названием «server.go».

Откройте файл server.go с помощью вашего любимого текстового редактора и добавьте следующий код:

package server


import (
    "context"
    "fmt"
    "log"
    "net"
    "os"
    "github.com/myk4040okothogodo/bikerenting/db"
    bikesv1 "github.com/myk4040okothogodo/bikerenting/gen/go/proto/bikes"
    "github.com/arangodb/go-driver"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"
)

const (
    bikesCollectionName = "Bikes"
    defaultPort         = "60001"
)

type Server struct {
    database             driver.Database
    bikesCollection      driver.Collection
}

func NewServer(ctx context.Context, database driver.Database)(*Server, error) {
    collection, err := db.AttachCollection(ctx, database, bikesCollectionName)
    if err != nil {
        return nil, err
    }

    _, _, err = collection.EnsurePersistentIndex(ctx, []string{"serial"}, &driver.EnsurePersistentIndexOptions{Unique: true})
    if err != nil {
        return nil, err
    }

    return &Server{
        database:        database,
        bikesCollection: collection,
    }, nil
}


func (s *Server) Run() {
    port := os.Getenv("APP_PORT")
    if port == "" {
        port = defaultPort
    }

    listener,err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", port))
    if err != nil {
        log.Fatal("net.Listen failed")
        return
    }

    grpcServer := grpc.NewServer()

    bikesv1.RegisterBikesAPIServer(grpcServer, s)
    reflection.Register(grpcServer)

    log.Printf("Starting Rental Bikes server on port %s", port)

    go func() {
        grpcServer.Serve(listener)
    }()
}


func (s *Server) ListBikes(ctx context.Context, in *bikesv1.ListBikesRequest)(*bikesv1.ListBikesResponse, error){
    if in == nil {
        return nil,  fmt.Errorf("Request is empty")
    }

    cursor, err := s.database.Query(ctx, db.ListRecords(s.bikesCollection.Name()), nil)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over documents: %s", err)
    }
    defer cursor.Close()

    allBikes := []*bikesv1.Bike{}
    for {
        bike := new(bikesv1.Bike)
        var meta driver.DocumentMeta
        meta, err := cursor.ReadDocument(ctx, bike)
        if driver.IsNoMoreDocuments(err){
            break
        } else if err != nil {
            return nil, fmt.Errorf("Failed to read bike document: %s", err)
        }
        bike.Id = meta.Key
        allBikes = append(allBikes, bike)
    }
    return &bikesv1.ListBikesResponse{Bikes: allBikes}, nil
}


func (s *Server) GetBike(ctx context.Context, in *bikesv1.GetBikeRequest)(*bikesv1.GetBikeResponse, error){
    if in == nil || in.Id == "" {
        return nil, fmt.Errorf("Bike id is not provided")
    }

    bike := new(bikesv1.Bike)
    meta, err := s.bikesCollection.ReadDocument(ctx, in.Id, bike)
    if err != nil {
        if driver.IsNotFound(err){
            err = fmt.Errorf("Bike with id '%s' not found", in.Id)

        } else {
            err = fmt.Errorf("Failed to get bike with id '%s'", in.Id, err)
        }
        return nil, err
    }
    bike.Id = meta.Key
    return &bikesv1.GetBikeResponse{Bike: bike}, nil
}


func (s *Server) GetBikes(ctx context.Context, in *bikesv1.GetBikesRequest) (*bikesv1.GetBikesResponse, error){
    if in == nil || len(in.Ids) == 0 {
        return nil, fmt.Errorf("Bikes ids are not provided")
    }

    const queryBikesByIds = `
    FOR bike IN %s
        FILTER bike._key in @ids
            RETURN bike`

    query := fmt.Sprintf(queryBikesByIds, bikesCollectionName)
    bindVars := map[string]interface{}{"ids":in.Ids}

    cursor, err := s.database.Query(ctx, query, bindVars)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over bike docments with query '%s': %s", queryBikesByIds, err)
    }

    defer cursor.Close()

    bikes := []*bikesv1.Bike{}

    for {
        bike := new(bikesv1.Bike)
        meta, err := cursor.ReadDocument(ctx, bike)
        if driver.IsNoMoreDocuments(err) {
            break
        } else if err != nil {
            log.Print(err)
            return nil, fmt.Errorf("Failed to read bike document: %s", err)
        }

        bike.Id = meta.Key
        bikes = append(bikes, bike)
    }
    return &bikesv1.GetBikesResponse{Bikes: bikes}, nil
}

func (s *Server) GetBikesByTYPE(ctx context.Context, in *bikesv1.GetBikesByTYPERequest)(*bikesv1.GetBikesByTYPEResponse, error){
    if in == nil || in.Type == "" {
        return nil, fmt.Errorf("Bike type is not provided")
    }

    const queryBikeByTYPE = `
    FOR bike IN %s
        FILTER bike.type == @type
            RETURN bike`
    query := fmt.Sprintf(queryBikeByTYPE, bikesCollectionName)
    bindVars := map[string]interface{}{"type": in.Type}

    cursor, err := s.database.Query(ctx, query, bindVars)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over bike documents with query '%s': %s", queryBikeByTYPE, err)
    }
    defer cursor.Close()

   bikes := []*bikesv1.Bike{}
   for {
     bike := new(bikesv1.Bike)
     meta, err := cursor.ReadDocument(ctx, bike)
     if driver.IsNoMoreDocuments(err){
       break
     } else if err != nil {
         log.Print(err)
         return nil, fmt.Errorf("failed to read rentees document: %s", err)
     }
     bike.Id = meta.Key
     bikes = append(bikes, bike)
   }
   return &bikesv1.GetBikesByTYPEResponse{Bikes: bikes}, nil
}


func (s *Server) GetBikesByOWNER(ctx context.Context, in *bikesv1.GetBikesByOWNERRequest)(*bikesv1.GetBikesByOWNERResponse, error){
    if in == nil || in.OwnerName == "" {
        return nil, fmt.Errorf("Bike owner is not provided")
    }

    const queryBikeByOWNER = `
    FOR bike IN %s
        FILTER bike.owner_name == @ownername
            RETURN bike`
    query := fmt.Sprintf(queryBikeByOWNER, bikesCollectionName)
    bindVars := map[string]interface{}{"owner_name": in.OwnerName}

    cursor, err := s.database.Query(ctx, query, bindVars)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over bike documents with query '%s': %s", queryBikeByOWNER, err)
    }
    defer cursor.Close()

     bikes := []*bikesv1.Bike{}
   for {
     bike := new(bikesv1.Bike)
     meta, err := cursor.ReadDocument(ctx, bike)
     if driver.IsNoMoreDocuments(err){
       break
     } else if err != nil {
         log.Print(err)
         return nil, fmt.Errorf("failed to read rentees document: %s", err)
     }
     bike.Id = meta.Key
     bikes = append(bikes, bike)
   }
   return &bikesv1.GetBikesByOWNERResponse{Bikes: bikes}, nil
}


func (s *Server) GetBikesByMAKE(ctx context.Context, in *bikesv1.GetBikesByMAKERequest)(*bikesv1.GetBikesByMAKEResponse, error){
    if in == nil || in.Make == "" {
        return nil, fmt.Errorf("Bike make is not provided")
    }

    const queryBikeByMAKE = `
    FOR bike IN %s
        FILTER bike.make == @make
            RETURN bike`
    query := fmt.Sprintf(queryBikeByMAKE, bikesCollectionName)
    bindVars := map[string]interface{}{"make": in.Make}

    cursor, err := s.database.Query(ctx, query, bindVars)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over bike documents with query '%s': %s", queryBikeByMAKE, err)
    }
    defer cursor.Close()

     bikes := []*bikesv1.Bike{}
     for {
     bike := new(bikesv1.Bike)
     meta, err := cursor.ReadDocument(ctx, bike)
     if driver.IsNoMoreDocuments(err){
       break
     } else if err != nil {
         log.Print(err)
         return nil, fmt.Errorf("failed to read rentees document: %s", err)
     }
     bike.Id = meta.Key
     bikes = append(bikes, bike)
   }
   return &bikesv1.GetBikesByMAKEResponse{Bikes: bikes}, nil
}

//func (s *Server) GetBikeBySERIAL(ctx context.Context, in *bikesv1.GetBikesSERIALRequest)(*bikesv1.GetBikesBySERIALResponse, error){
//    if in == nil || in.Serial == "" {
//        return nil, fmt.Errorf("Bike serial is not provided")
//    }

//    const queryBikeBySERIAL = `
//    FOR bike IN %s
//        FILTER bike.serial == @serial
//            RETURN bike`
//    query := fmt.Sprintf(queryBikeBySERIAL, bikesCollectionName)
//    bindVars := map[string]interface{}{"serial": in.Serial}

//    cursor, err := s.database.Query(ctx, query, bindVars)
//    if err != nil {
//        return nil, fmt.Errorf("Failed to iterate over bike documents with query '%s': %s", queryBikeBySERIAL, err)
//    }
//    defer cursor.Close()

//    b := new(bikesv1.Bike)
//    meta, err := cursor.ReadDocument(ctx, b)
//    if driver.IsNoMoreDocuments(err){
//        return nil, fmt.Errorf("Bike with SERIAL '%s' not found: %s", in.Serial, err)
//    } else if err != nil {
//        return nil, fmt.Errorf("Failed to read bike document: %s", err)
//    }
//    b.Id = meta.Key

//    return &bikesv1.GetBikesBySERIALResponse{Bike: b}, nil
//}



func (s *Server) AddBike(ctx context.Context, in *bikesv1.AddBikeRequest)(*bikesv1.AddBikeResponse, error){
    if in == nil || in.Bike == nil {
        return nil, fmt.Errorf("Bike is not provided")
    }

    meta, err := s.bikesCollection.CreateDocument(ctx, in.Bike)
    if err != nil {
        return nil, fmt.Errorf("failed to create bike: %s", err)
    }

    in.Bike.Id = meta.Key
    return &bikesv1.AddBikeResponse{Bike: in.Bike}, nil
}

func (s *Server) DeleteBike(ctx context.Context, in *bikesv1.DeleteBikeRequest)(*bikesv1.DeleteBikeResponse, error) {
    if in == nil || in.Id == "" {
        return nil, fmt.Errorf("Bike id is not provided")
    }

    _, err := s.bikesCollection.RemoveDocument(ctx, in.Id)
    if err != nil {
        return nil, fmt.Errorf("Failed to remove existing bike: %s", err)
    }

    return &bikesv1.DeleteBikeResponse{}, nil
}
}
Вход в полноэкранный режим Выход из полноэкранного режима

Запустите команды «go mod init» и «go mod tidy».
Все, что мы делаем в приведенном выше коде, это реализуем логику для перечисления велосипедов, используя их идентификаторы, марки, типы и серийные номера, мы также реализуем логику для добавления нового велосипеда в систему и удаления велосипеда из системы, Теперь, чтобы вызвать этот сервер, создав новый сервер, вернитесь на один уровень назад «cd …» и откройте файл «main.go» и добавьте следующий код.

1 package main
  2 
  3 import (
  4     "context"
  5     "log"
  6     "os"
  7     "os/signal"
  8     "syscall"
  9     "time"
 10     "github.com/myk4040okothogodo/bikerenting/bikes/server"
 11     "github.com/myk4040okothogodo/bikerenting/db"
 12 )
 13 
 14                                                                                                                                                                     
 15 func main(){
 16   ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*5)
 17   defer cancelFn()
 18 
 19   database, err := db.Connect(ctx, db.GetDbConfig())
 20   if err != nil {
 21       log.Fatalf("d.OpenDatabase failed with error: %s", err)
 22   }
 23 
 24   srv, err := server.NewServer(ctx, database)
 25   if err != nil {
 26       log.Fatalf("NewServer failed with error: %s", err)
 27   }
 28 
 29   srv.Run()
 30 
 31   sigChan := make(chan os.Signal, 1)
 32   signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
 33   signal := <-sigChan
 34   log.Printf("shutting down bikes server with signal: %s", signal)
 35 
 36 }
Войти в полноэкранный режим Выйти из полноэкранного режима

В приведенном выше коде мы соединяемся с нашей базой данных в соответствии с конфигурацией, которую мы предоставили в файле «db/connect.go», затем создаем новый сервер в соответствии с нашим определением в файле «server.go» и запускаем этот сервер, помните, что этот сервер будет Go-программой, поэтому мы используем канал для прослушивания любых сигналов уничтожения сервера, т.е. «SIGTERM», чтобы убить наш сервер.

Мы повторим приведенный выше код для создания нашего API rentees, теперь перейдите в папку rentees из корневой папки проекта и добавьте следующий код, структура файла будет идентична той, которую мы только что создали выше, поэтому, чтобы избежать повторений, я не буду повторять объяснения.

mykmyk@skynet:~/code/src/github.com/myk4040okothogodo/bikerenting/rentees$ ll
total 12
drwxrwxr-x 3 mykmyk mykmyk 4096 Jun  2 10:49 ./
drwxrwxr-x 9 mykmyk mykmyk 4096 Jun  2 02:44 ../
-rw-rw-r-- 1 mykmyk mykmyk    0 Jun  2 10:48 main.go
drwxrwxr-x 2 mykmyk mykmyk 4096 Jun  2 10:49 server/
Вход в полноэкранный режим Выйти из полноэкранного режима

для нашего «bikerenting/rentees/server/server.go»

package server

import  (
    "context"
    "fmt"
    "log"
    "net"
    "os"
    "github.com/myk4040okothogodo/bikerenting/db"
    renteesv1 "github.com/myk4040okothogodo/bikerenting/gen/go/proto/rentees"
    "github.com/arangodb/go-driver"
    "google.golang.org/grpc"
    "google.golang.org/grpc/reflection"

)


const (
    renteesCollectionName = "rentees"
    defaultPort           = "60002"
)


type Server struct {
    database           driver.Database
    renteesCollection  driver.Collection
}

func NewServer(ctx context.Context, database driver.Database)(*Server, error){
    collection, err := db.AttachCollection(ctx, database, renteesCollectionName)
    if err != nil {
        return nil, err
    }

    return &Server {
        database: database,
        renteesCollection: collection,
    }, nil
}

func (s *Server) Run() {
    port := os.Getenv("APP_PORT")
    if port == "" {
        port = defaultPort
    }
    listener, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0: %s", port))
    if err != nil {
        log.Print("net.Listen failed")
        return
    }
    grpcServer := grpc.NewServer()
    renteesv1. RegisterRenteesAPIServer(grpcServer, s) // use autogenerated code to register the server
    reflection.Register(grpcServer)

    log.Printf("Starting Rentees server on port %s", port)
    go func() {
        grpcServer.Serve(listener)
    }()
}

func (s *Server) ListRentees(ctx context.Context, in *renteesv1.ListRenteesRequest)(*renteesv1.ListRenteesResponse, error){
    if in == nil {
        return nil, fmt.Errorf("Request is empty")
    }

    cursor, err := s.database.Query(ctx, db.ListRecords(s.renteesCollection.Name()), nil)
    if err != nil {
        return nil, fmt.Errorf("failed to iterate over documents: %s", err)
    }

    defer cursor.Close()
    allRentees := []*renteesv1.Rentee{}
    for {
        rentee := new(renteesv1.Rentee)
        var meta driver.DocumentMeta
        meta, err := cursor.ReadDocument(ctx, rentee)
        if driver.IsNoMoreDocuments(err) {
            break
        } else if err != nil {
            return nil, fmt.Errorf("Failed to read rentee document: %s", err)
        }
        rentee.Id = meta.Key
        allRentees = append(allRentees, rentee)
    }
    return &renteesv1.ListRenteesResponse{Rentees: allRentees}, nil
}


func (s *Server) GetRentee(ctx context.Context, in *renteesv1.GetRenteeRequest)(*renteesv1.GetRenteeResponse, error) {
    if in == nil || in.Id == "" {
        return nil, fmt.Errorf("Rentee id is not provided")
    }

    rentee := new(renteesv1.Rentee)
    meta, err := s.renteesCollection.ReadDocument(ctx, in.Id, rentee)
    if err != nil {
        if driver.IsNotFound(err) {
             err = fmt.Errorf("Rentee with id '%s' not found", in.Id)
        } else {
             err = fmt.Errorf("Failed to get rentee with id '%s':'%s'", in.Id, err)
        }
        return nil, err
    }
    rentee.Id = meta.Key

    return &renteesv1.GetRenteeResponse{Rentee: rentee}, nil
}


func (s *Server) GetRenteeByBikeId(ctx context.Context, in *renteesv1.GetRenteeByBikeIdRequest)(*renteesv1.GetRenteeByBikeIdResponse, error){
    if in == nil || in.Id == "" {
        return nil, fmt.Errorf("Bike id is not provided")
    }

    const queryRenteeByBikeId = `
    FOR rentee IN %s
        FOR bikeId IN rentee.held_bikes
           FILTER bikeId == @bikeId
               RETURN rentee`
    query := fmt.Sprintf(queryRenteeByBikeId, renteesCollectionName)
    bindVars := map[string]interface{}{"bikeId": in.Id}

    cursor, err := s.database.Query(ctx, query, bindVars)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over rentee documents with query '%s': %s", queryRenteeByBikeId, err)
    }
    defer cursor.Close()

    r := new(renteesv1.Rentee)
    meta, err := cursor.ReadDocument(ctx, r)
    if driver.IsNoMoreDocuments(err){
        return nil, fmt.Errorf("Rentee that held bike with id %s not found: %s", in.Id, err)
    } else if err != nil {
        return nil, fmt.Errorf("Failed to read rentee document: %s", err)
    }
    r.Id = meta.Key
    return &renteesv1.GetRenteeByBikeIdResponse{Rentee: r}, nil
}


func (s *Server) GetRenteesByBikeTYPE(ctx context.Context, in *renteesv1.GetRenteesByBikeTYPERequest)(*renteesv1.GetRenteesByBikeTYPEResponse, error){
    if in == nil || in.Type == " " {
        return nil, fmt.Errorf("Request is empty")
    }

    const queryRenteeByBikeTYPE = `
    FOR rentee IN %s
        FOR bikeType IN rentee.held_bikes
           FILTER bikeType == @type
               RETURN rentee`
    query := fmt.Sprintf(queryRenteeByBikeTYPE, renteesCollectionName)
    bindVars := map[string]interface{}{"bikeType": in.Type}

    cursor, err := s.database.Query(ctx, query, bindVars)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over rentee documents with query '%s': %s", queryRenteeByBikeTYPE, err)
    }
    defer cursor.Close()

    rentees := []*renteesv1.Rentee{}

    for {
        rentee := new(renteesv1.Rentee)
        meta, err := cursor.ReadDocument(ctx, rentee)
        if driver.IsNoMoreDocuments(err){
            break
        } else if err != nil {
            log.Print(err)
            return nil, fmt.Errorf("failed to read rentees document: %s", err)
        }
        rentee.Id = meta.Key
        rentees = append(rentees, rentee)
    }
    return &renteesv1.GetRenteesByBikeTYPEResponse{Rentees: rentees}, nil
  }

func (s *Server) GetRenteesByBikeMAKE(ctx context.Context, in *renteesv1.GetRenteesByBikeMAKERequest)(*renteesv1.GetRenteesByBikeMAKEResponse, error){
    if in == nil || in.Make == " " {
        return nil, fmt.Errorf("Request is empty")
    }

    const queryRenteeByBikeMAKE = `
    FOR rentee IN %s
        FOR bikeMake IN rentee.held_bikes
           FILTER bikeMake == @make
               RETURN rentee`
    query := fmt.Sprintf(queryRenteeByBikeMAKE, renteesCollectionName)
    bindVars := map[string]interface{}{"bikeMake": in.Make}

    cursor, err := s.database.Query(ctx, query, bindVars)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over rentee documents with query '%s': %s", queryRenteeByBikeMAKE, err)
    }
    defer cursor.Close()

    rentees := []*renteesv1.Rentee{}

    for {
        rentee := new(renteesv1.Rentee)
        meta, err := cursor.ReadDocument(ctx, rentee)
        if driver.IsNoMoreDocuments(err){
            break
        } else if err != nil {
            log.Print(err)
            return nil, fmt.Errorf("failed to read rentees document: %s", err)
        }
        rentee.Id = meta.Key
        rentees = append(rentees, rentee)
    }
    return &renteesv1.GetRenteesByBikeMAKEResponse{Rentees: rentees}, nil
  }

func (s *Server) GetRenteesByBikeOWNER(ctx context.Context, in *renteesv1.GetRenteesByBikeOWNERRequest)(*renteesv1.GetRenteesByBikeOWNERResponse, error){
    if in == nil || in.OwnerName == " " {
        return nil, fmt.Errorf("Request is empty")
    }

    const queryRenteeByBikeOWNER = `
    FOR rentee IN %s
        FOR bikeOwner IN rentee.held_bikes
           FILTER bikeOwner == @owner
               RETURN rentee`
    query := fmt.Sprintf(queryRenteeByBikeOWNER, renteesCollectionName)
    bindVars := map[string]interface{}{"bikeOwner": in.OwnerName}

    cursor, err := s.database.Query(ctx, query, bindVars)
    if err != nil {
        return nil, fmt.Errorf("Failed to iterate over rentee documents with query '%s': %s", queryRenteeByBikeOWNER, err)
    }
    defer cursor.Close()

    rentees := []*renteesv1.Rentee{}

    for {
        rentee := new(renteesv1.Rentee)
        meta, err := cursor.ReadDocument(ctx, rentee)
        if driver.IsNoMoreDocuments(err){
            break
        } else if err != nil {
            log.Print(err)
            return nil, fmt.Errorf("failed to read rentees document: %s", err)
        }
        rentee.Id = meta.Key
        rentees = append(rentees, rentee)
    }
    return &renteesv1.GetRenteesByBikeOWNERResponse{Rentees: rentees}, nil
  }

func (s *Server) AddRentee(ctx  context.Context, in *renteesv1.AddRenteeRequest) (*renteesv1.AddRenteeResponse, error) {
    if in == nil || in.Rentee == nil {
        return nil, fmt.Errorf("Rentee is not provided")
    }

    meta, err := s.renteesCollection.CreateDocument(ctx, in.Rentee)
    if err != nil {
        return nil, fmt.Errorf("Failed to create rentee: %s", err)
    }

    in.Rentee.Id = meta.Key
    return &renteesv1.AddRenteeResponse{Rentee: in.Rentee}, nil
}


func (s *Server) UpdateRentee(ctx context.Context, in *renteesv1.UpdateRenteeRequest)(*renteesv1.UpdateRenteeResponse, error){
    if in == nil || in.Rentee == nil || in.Rentee.Id == "" {
          return nil, fmt.Errorf("Existing rentee is provided")
    }

    _, err := s.renteesCollection.ReplaceDocument(ctx, in.Rentee.Id, in.Rentee)
    if err != nil {
        return nil, fmt.Errorf("Failed to update with id %s", in.Rentee.Id, err)
    }
    return &renteesv1.UpdateRenteeResponse{Rentee: in.Rentee}, nil
}

Вход в полноэкранный режим Выйти из полноэкранного режима

Для нашего файла «bikerenting/rentees/main.go» мы добавим в него следующий код.

  1 package main                                                                                                                                                        
  2 
  3 import (
  4     "context"
  5     "log"
  6     "os"
  7     "os/signal"
  8     "syscall"
  9     "time"
 10     "github.com/myk4040okothogodo/bikerenting/db"
 11     "github.com/myk4040okothogodo/bikerenting/rentees/server"
 12 )
 13 
 14 
 15 func main() {
 16     ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*5)
 17     defer cancelFn()
 18 
 19     database, err := db.Connect(ctx, db.GetDbConfig())
 20     if err != nil {
 21         log.Fatalf("db.OpenDatabase failed with error: %s", err)
 22     }
 23 
 24     srv, err := server.NewServer(ctx, database)
 25     if err != nil {
 26         log.Fatalf("NewServer failed with error: %s", err)
 27     }
 28 
 29     srv.Run()
 30 
 31     sigChan := make(chan os.Signal, 1)
 32     signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
 33     signal := <-sigChan
 34     log.Printf("shutting down Rentees servers with signal: %s", signal)
 35 }


Вход в полноэкранный режим Выйти из полноэкранного режима

Запустите команды «go mod init» и «go mod tidy».

Статья получилась слишком большой, поэтому мне придется создать «интерлюдию» перед второй частью. В этой статье речь пойдет о написании тестов для нашего API, а также об использовании grpcurl для тестирования конечных точек. До встречи.

Оцените статью
Procodings.ru
Добавить комментарий