Nest JS Graphql с Apollo Federation Gateway для микросервисов #часть-4


Создание сервиса Graphql (Home APIs) 🚀 🚀 🚀

Часть-1/2/3 этого блога вы можете посмотреть здесь, В этом примере мы построим еще один graphql сервис Home APIs

Github :
https://github.com/tkssharma/nestjs-with-apollo-federation-gateway

Для понимания всей архитектуры нам необходимо иметь

  • шлюз Nestjs сервис (шлюз-сервис)
  • служба пользователей graphql (Микросервис-1)
  • graphql Home manager service (Микросервис-2).

В целом у нас есть один шлюз и два микросервиса, открывающие интерфейс graphql.

В этом посте мы создадим службу Home Service и, самое главное, мы увидим, как шлюз собирает все подграфы и собирает для нас данные из разных графов.

  • Целью этой серии является то, как мы можем получить данные Home APIs с данными пользователя, как шлюз объединяет обе данные вместе, где данные находятся в различных базах данных сервиса.

Давайте построим Home APIs

Это проект lerna и у нас есть эти 3 модуля

  • модуль nestjs шлюза
  • модуль auth api пользователя
  • модуль домашнего сервиса api

Все сервисы graphql используют подход schema first, т.е. сначала мы напишем схему, а затем мы напишем

  • резольверы
  • сервисы
  • модули
  • корневые модули

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

  • Мы создаем простые домашние API, давайте создадим базовые вещи

  • Главная схема Graphql

  • Домашний модуль

  • Домашняя служба

  • Домашняя сущность

  • Домашнее DTO

  • Домашний резольвер

Главная сущность

import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, CreateDateColumn, DeleteDateColumn, UpdateDateColumn, OneToOne, JoinColumn, OneToMany } from 'typeorm';
@Entity('homes')
export class Home extends BaseEntity {
  @PrimaryGeneratedColumn('uuid')
  public id!: string;

  @Column('varchar', { length: 500, unique: true })
  public name!: string;

  @Column({ type: 'uuid' })
  public user_id!: string;


  @Column('varchar')
  public description!: string;

  @Column({ type: 'jsonb', default: [] })
  public display_images!: string[];

  @Column({ type: 'jsonb', default: [] })
  public original_images!: string[];

  @Column({ type: 'jsonb', default: null })
  public metadata!: any;

  @Column({ type: 'boolean', default: true, select: true })
  public is_active!: boolean;

  @CreateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  public created_at!: Date;

  @UpdateDateColumn({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' })
  public updated_at!: Date;

  @DeleteDateColumn()
  public deleted_at?: Date | null;

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

Главная Схема Graphql

scalar Date
scalar Upload

type Home @key (fields: "id"){
    id: ID!
    name: String!
    user: User
    description: String!
    display_images: [String!]
    original_images: [String!]
    is_active: Boolean!
    created_at: Date
    updated_at: Date
}

extend type User @key(fields: "id") {
  id: ID! @external
}

type Query {
    homes: [Home!]
    home(id: ID): Home!
    findHomes(name: String!): [Home!]
    activeHomes: [Home!]
}
input HomeInput {
  name: String!
  description: String!
  is_active: Boolean!
}

type Mutation {
    createHome(payload: HomeInput!): Home
    updateHome(id: ID!, payload: HomeInput!): Home
}
Войти в полноэкранный режим Выход из полноэкранного режима

Давайте посмотрим сюда внимательно У нас есть домашняя схема и мы расширяем здесь тип пользователя из другого сервиса

type Home @key (fields: "id"){
    id: ID!
    name: String!
    user: User
    description: String!
    display_images: [String!]
    original_images: [String!]
    is_active: Boolean!
    created_at: Date
    updated_at: Date
}

extend type User @key(fields: "id") {
  id: ID! @external
}
Вход в полноэкранный режим Выход из полноэкранного режима

Добавим резольвер, который должен просто получать данные из сервисов

import { AdminGuard } from '@app/auth/guards/admin.guard';
import { JwtAuthGuard } from '@app/auth/guards/jwt-auth.guard';
import { Logger } from '@logger/logger';
import { ConsoleLogger, UseGuards } from '@nestjs/common';
import { Resolver, Query, Args, Mutation, Parent, ResolveField, Context, ResolveReference } from '@nestjs/graphql';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { HomeLocality } from '../entity/home-locality.entity';
import { Home } from '../entity/home.entity';
import { HomeService } from './home.service';

@Resolver((of: any) => Home)
export class HomeResolver {
  constructor(private homeService: HomeService,
    private readonly logger: Logger) {
  }

  @Query()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async homes() {
    return await this.homeService.listAll();
  }

  @Query()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async findHomes(@Args('name') name: string) {
    return await this.homeService.findHome(name);
  }

  @Query()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async activeHomes() {
    return await this.homeService.listAllActiveHomes();
  }

  @Query()
  async home(@Args('id') id: string) {
    return await this.homeService.getById(id);
  }

  @Mutation()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async createHome(@Args() args: any, @Context() context: any) {
    const { userid } = context.req.headers;
    return await this.homeService.createHome(args, userid);
  }

  @Mutation()
  @UseGuards(JwtAuthGuard, AdminGuard)
  async updateHome(@Args('id') id: string, @Args() args: any) {
    return await this.homeService.updateHome(id, args);
  }
  @ResolveField('user')
  user(@Parent() home: Home) {
    this.logger.http("ResolveField::user::HomeResolver" + home.user_id)
    return { __typename: 'User', id: home.user_id };
  }

  @ResolveReference()
  async resolveReference(reference: { __typename: string; id: string }) {
    this.logger.http('Logging :: ResolveReference :: home')
    return await this.homeService.getByHomeId(reference.id);
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь все просто, кроме двух методов, которые вы видите в последнем из резолверов
и именно они отвечают за выборку данных из различных сервисов

ResolveReference — означает, что когда любому другому сервису нужны данные Home, основанные на ID, этот метод будет вызван шлюзом graphql.

Примечания — Очень важно [сохраняйте имя класса сущности TypeORM таким же, как типы Graphql, такие как User и Home].

  @ResolveField('user')
  user(@Parent() home: Home) {
    this.logger.http("ResolveField::user::HomeResolver" + home.user_id)
    return { __typename: 'User', id: home.user_id };
  }

  @ResolveReference()
  async resolveReference(reference: { __typename: string; id: string }) {
    this.logger.http('Logging :: ResolveReference :: home')
    return await this.homeService.getByHomeId(reference.id);
  }
Вход в полноэкранный режим Выйти из полноэкранного режима

Вы также можете получить доступ к контексту, если он уже установлен.

  async createHome(@Args() args: any, @Context() context: any) {
    const { userid } = context.req.headers;
    return await this.homeService.createHome(args, userid);
  }
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь сервис просто использует TypeORM Repo для получения данных для нас

import { Logger } from '@logger/logger';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ILike, Like, Repository } from 'typeorm';
import { HomeFacility } from '../entity/home-facility.entity';
import { HomeLocality } from '../entity/home-locality.entity';
import { Home } from '../entity/home.entity';
import { CreateHomeDto } from './home.dto';


@Injectable()
export class HomeService {
  constructor(
    @InjectRepository(Home) private readonly homeRepository: Repository<Home>,
    @InjectRepository(HomeLocality) private readonly homeLocalityRepository: Repository<HomeLocality>,
    private readonly logger: Logger
  ) {
  }

  async createHome(data: any, userid: string): Promise<Home> {
    const body = data.payload;
    try {
      const existingHome = await this.homeRepository.findOne({ where: { name: body.name } })
      if (existingHome) {
        return existingHome;
      }
      const res = await this.homeRepository
        .save({ ...body, user_id: userid });
      return res;
    } catch (err: any) {
      this.logger.error(err);
      throw err;
    }
  }


  async updateHome(id: string, data: any): Promise<Home> {
    const body = data.payload;
    const homeHome = await this.homeRepository.findOne({ where: { id } });
    const updatedHome = { ...homeHome, ...body }
    return await this.homeRepository.save(updatedHome)
  }

  async listAll() {
    return await this.homeRepository.find({ relations: ['locality', 'facilities'] });
  }

  async findHome(name: string) {
    return await this.homeRepository.find({ where: { name: ILike(`%${name}%`) }, relations: ['locality', 'facilities'] });
  }

  async listAllActiveHomes() {
    return await this.homeRepository.find({ where: { is_active: true }, relations: ['locality', 'facilities'] });
  }

  async getById(id: string) {
    return await this.homeRepository.findOne({ where: { id, is_active: true }, relations: ['locality', 'facilities'] });
  }

  async getByHomeName(name: string) {
    return await this.homeRepository.findOne({ where: { name } });
  }

  async getByHomeId(id: string) {
    return await this.homeRepository.findOne({ where: { id } });
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

Наконец, наш модуль домена

import { MiddlewareConsumer, Module } from '@nestjs/common';
import { TypeOrmModule, TypeOrmModuleAsyncOptions } from '@nestjs/typeorm';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { ConfigModule } from '@app/config/config.module';
import { DbModule } from '../../db/db.module';
import { Home } from './entity/home.entity';
import { HomeModule } from './home/home.module';
import { LoggerModule } from '@logger/logger.module';
import { Upload } from '../Scalars/upload.scalar';

import { ApolloFederationDriver, ApolloFederationDriverConfig } from '@nestjs/apollo';
import { GraphQLError, GraphQLFormattedError } from 'graphql';

@Module({
  imports: [
    DbModule.forRoot({
      entities: [Home],
    }),
    LoggerModule,
    HomeModule,
    ConfigModule,
    GraphQLModule.forRoot({
      typePaths: ['./**/*.graphql'],
      uploads: false,
      driver: ApolloFederationDriver,
      context: ({ req }: any) => ({ req }),
      formatError: (error: GraphQLError) => {
        console.log(error);
        const graphQLFormattedError: GraphQLFormattedError = {
          message: error?.extensions?.exception?.response?.message || error?.message,
        };
        return graphQLFormattedError;
      },
    }),
  ],
})
export class DomainModule {}
Войти в полноэкранный режим Выход из полноэкранного режима

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

extend type User @key(fields: "id") {
  id: ID! @external
}
Вход в полноэкранный режим Выход из полноэкранного режима

Все эти @key, @provided , @external предоставляются в спецификациях apollo

@key

directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
Вход в полноэкранный режим Выйти из полноэкранного режима

Директива @key используется для указания комбинации полей, которые могут быть использованы для уникальной идентификации и получения объекта или интерфейса.

type Product @key(fields: "upc") {
  upc: UPC!
  name: String
}
Ввести полноэкранный режим Выход из полноэкранного режима

Для одного типа объекта можно определить несколько ключей:

@provides

директива @provides(fields: _FieldSet!) на FIELD_DEFINITION
Директива @provides используется для аннотации ожидаемого возвращаемого набора полей из поля на базовом типе, который гарантированно может быть выбран шлюзом. Приведем следующий пример:

type Review @key(fields: "id") {
  product: Product @provides(fields: "name")
}

extend type Product @key(fields: "upc") {
  upc: String @external
  name: String @external
}
Войти в полноэкранный режим Выход из полноэкранного режима

При получении Review.product из сервиса Reviews можно запросить название с расчетом на то, что сервис Reviews сможет его предоставить при переходе от обзора к продукту. Product.name является внешним полем внешнего типа, поэтому требуется локальное расширение типа Product и аннотация name.

@requires

директива @requires(fields: _FieldSet!) on FIELD_DEFINITION
Директива @requires используется для аннотирования требуемого набора полей ввода из базового типа для резольвера. Она используется для разработки плана запроса, в котором требуемые поля могут быть не нужны клиенту, но службе может потребоваться дополнительная информация от других служб. Например:

# extended from the Users service
extend type User @key(fields: "id") {
  id: ID! @external
  email: String @external
  reviews: [Review] @requires(fields: "email")
}
Войти в полноэкранный режим Выйти из полноэкранного режима

В данном случае служба Reviews добавляет новые возможности к типу User, предоставляя список отзывов, связанных с пользователем. Чтобы получить эти отзывы, сервису Отзывы необходимо знать email пользователя из сервиса Пользователи, чтобы найти отзывы. Это означает, что поле отзывов / резольвер требует поле email из базового типа User.

@external

директива @external на FIELD_DEFINITION
Директива @external используется для пометки поля как принадлежащего другому сервису. Это позволяет службе A использовать поля службы B и при этом знать во время выполнения тип этого поля. Например:

# extended from the Users service
extend type User @key(fields: "email") {
  email: String @external
  reviews: [Review]
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Давайте посмотрим на это в действии

  • нам нужно установить docker-compose, чтобы все базы данных были доступны для TypeORM
  • установить зависимости пакетов во всех проектах
  • запустить приложения и окончательно запустить шлюз.

Настройка всей платформы локально

Эта платформа содержит все эти компоненты

  • Сервис управления пользователями
  • Служба Home Manager
  • Служба шлюза
  • Служба домашнего менеджера

Запуск всех этих служб

  • Мы используем docker-compose для загрузки всех контейнеров (только контейнеров базы данных) в корень проекта.
docker-compose up
Войти в полноэкранный режим Выйти из полноэкранного режима
  • проверьте длинные и убедитесь, что базы данных были созданы
git clone <repo>
cd nestjs-with-apollo-federation-gateway
cd packages
Войти в полноэкранный режим Выйти из полноэкранного режима

Запуск службы Auth

cd auth-service
vi .env
Войти в полноэкранный режим Выйти из полноэкранного режима

обновление env с этим содержимым

DATABASE_URL= postgres://api:development_pass@localhost:5431/auth-api
SENDGRID_API_KEY=SSSS
SENDGRID_VERIFIED_SENDER_EMAIL=ssss@gmail.com
DEBUG="ssss:*"
LOG_LEVEL=http
PORT=5006
NODE_ENV=local
JWT_SECRET=HELLO
JWT_EXPIRE_IN=3600*24
Вход в полноэкранный режим Выйдите из полноэкранного режима

Теперь запустите приложение в режиме просмотра, оно будет работать на localhost:5006

npm run start:dev
Войти в полноэкранный режим Выйти из полноэкранного режима

Запуск Home Manager

cd home-manager
vi .env
Войти в полноэкранный режим Выход из полноэкранного режима

обновить env с помощью этого содержимого

NODE_ENV=local
LOG_LEVEL=http
PORT=5003
SECRET_KEY=HELLO
NEW_RELIC_KEY=
DATABASE_URL=postgres://api:development_pass@localhost:5433/home-manager-api
Вход в полноэкранный режим Выйдите из полноэкранного режима

Теперь запустите приложение в режиме просмотра, оно будет работать на localhost:5003

npm run start:dev
Войти в полноэкранный режим Выйти из полноэкранного режима
  • docker-compose up
  • Auth Apis npm run start:dev
  • Home Apis npm run start:dev

Для шлюза также выполните ту же команду npm run start:dev.

Теперь давайте протестируем наше приложение
Наш шлюз подключен к обоим sub-graphq apis, запущенным на разных портах.
Мы должны убедиться, что обе службы запущены, прежде чем запускать службу шлюза.

{ name: 'User', url: 'http://localhost:5006/graphql' },
{ name: 'Home', url: 'http://localhost:5003/graphql' },
Войдите в полноэкранный режим Выйти из полноэкранного режима

Если вы используете тот же PORT и env, то шлюз будет запущен на http://localhost:5002/graphql.

Заключение

Я надеюсь, что эта серия статей будет полезна для разработчиков, которые хотят настроить Nest JS graphql шлюз с поддержкой федерации.
Важные моменты и сложные задачи
Примечания ::

  • получение всех зависимостей, работающих друг с другом (миграция — это настоящая боль)
  • Миграция TypeORM создаст сущности, когда мы запустим приложение
  • https://github.com/tkssharma/nestjs-with-apollo-federation-gateway Клонируйте это репо и поиграйте с этим

  • https://tkssharma.com/nestjs-with-apollo-federation-for-microservices-part-2

  • https://tkssharma.com/nestjs-with-apollo-federation-for-microservices-part-3

  • https://tkssharma.com/nestjs-with-apollo-federation-for-microservices-part-4

Ссылки

  • https://www.apollographql.com/docs/federation/federation-spec/#key
  • https://www.apollographql.com/docs/federation
  • https://www.apollographql.com/docs/federation/
  • https://www.apollographql.com/docs/federation/federation-2/new-in-federation-2
  • https://github.com/apollographql/supergraph-demo-fed2
  • https://www.apollographql.com/docs/federation/federation-spec/
  • https://docs.nestjs.com/graphql/migration-guide
  • https://docs.nestjs.com/graphql/federation

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