6
1

Hasuraを使って環境構築してガンガン工数削減

Posted at

ある機会がありHasuraというものを知りました
Prismaチームが開発したPlaygroudは使用した経験があるので、Hasuraとどんな違いがあるのか調べてみたらPlaygroudより使いやすく開発工数をガンガン削減出来るなと感じました!
なので、もしこの記事を読んでHasuraのことを知ってもらえればと幸いです

Hasuraについて

簡単に言うとGraphQLサーバーになります
Hasuraはテーブルを追跡することでCRUDや集計用Queryなどを自動的に用意してくれる優れものです
また、他の自前で用意したGraphQLサーバーとHasuraを統合してリクエストをHasura一つにお任せすることも可能です

単純なCRUDや集計はHasuraに任せて、ビジネスロジックを含むような処理は自前で用意したGraphQLサーバーに任せることで工数の削減に繋げられるかと思います

HasuraにはCloud上で操作するかDockerで構築して操作するかの二種類があります
今回はDockerを使って環境構築をしていきます

ファイル構成

Terminal
.
├── backend
│   ├── README.md
│   ├── nest-cli.json
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   ├── test
│   ├── tsconfig.build.json
│   └── tsconfig.json
├── db_data
│   └── .gitkeep
├── docker
│   ├── backend
│   │   └── Dockerfile
│   └── frontend
│       └── Dockerfile
├── docker-compose.yml
├── frontend
│   ├── README.md
│   ├── codegen.yml
│   ├── next-env.d.ts
│   ├── next.config.mjs
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   ├── src
│   └── tsconfig.json
└── hasura
    ├── config.yaml
    ├── metadata
    ├── migrations
    └── seeds

backendディレクトリ

Nest.jsディレクトリ
Hasuraだけでは処理しきれないビジネスロジックを含む処理などを担当

db_dataディレクトリ

PostgreSQLのデータを永続化するためのディレクトリ

dockerディレクトリ

Dockerfileを管理するディレクトリ

frontendディレクトリ

Next.jsディレクトリ
Hasuraとやり取りをしてフロント処理を担当

hasuraディレクトリ

hasuraのテーブル定義アクセス権限まわりなどのファイル郡を管理するためのディレクトリ

環境構築

Terminal
mkdir my_app
cd my_app

作業用のルートディレクトリを作成して移動していく

docker-compose.ymlを記述

Hasura公式に記載されているcomposeファイルを取得してくる
取得してきたComposeファイルに肉付けしていく流れになります

Terminal
curl https://raw.githubusercontent.com/hasura/graphql-engine/stable/install-manifests/docker-compose/docker-compose.yaml -o docker-compose.yml

forntend・backendのserviceやhasuraに必要な環境変数を記載していったのcomposeファイルが下記になる

docker-compose.yml
version: "3.7"

services:
  backend:
    build:
      context: .
      dockerfile: ./docker/backend/Dockerfile
    tty: true
    volumes:
      - type: bind
        source: ./backend
        target: /backend
    ports:
      - "3000:3000"
    depends_on:
      - postgres

  frontend:
    build:
      context: .
      dockerfile: ./docker/frontend/Dockerfile
    tty: true
    volumes:
      - type: bind
        source: ./frontend
        target: /frontend
    ports:
      - "80:80"

  postgres:
    image: postgres:15
    restart: always
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_DB: ${POSTGRES_DB}
      TZ: "Asia/Tokyo"

  graphql-engine:
    image: hasura/graphql-engine:v2.37.0
    ports:
      - "8080:8080"
    restart: always
    environment:
      ## postgres database to store Hasura metadata
      HASURA_GRAPHQL_METADATA_DATABASE_URL: ${HASURA_GRAPHQL_DATABASE_URL}
      ## this env var can be used to add the above postgres database to Hasura as a data source. this can be removed/updated based on your needs
      PG_DATABASE_URL: ${HASURA_GRAPHQL_DATABASE_URL}
      ## enable the console served by server
      HASURA_GRAPHQL_ENABLE_CONSOLE: "true" # set to "false" to disable console
      ## enable debugging mode. It is recommended to disable this in production
      HASURA_GRAPHQL_DEV_MODE: "true"
      HASURA_GRAPHQL_ENABLED_LOG_TYPES: startup, http-log, webhook-log, websocket-log, query-log
      ## uncomment next line to run console offline (i.e load console assets from server instead of CDN)
      # HASURA_GRAPHQL_CONSOLE_ASSETS_DIR: /srv/console-assets
      ## uncomment next line to set an admin secret
      HASURA_GRAPHQL_ADMIN_SECRET: ${HASURA_GRAPHQL_ADMIN_SECRET}
      HASURA_GRAPHQL_METADATA_DEFAULTS: ${HASURA_GRAPHQL_METADATA_DEFAULTS}
      NESTJS_REMOTE_SCHEMA: ${NESTJS_REMOTE_SCHEMA}
      HASURA_GRAPHQL_ENABLE_REMOTE_SCHEMA_PERMISSIONS: "true"
      HASURA_GRAPHQL_ENABLE_APOLLO_FEDERATION: "true"
    volumes:
      - ./hasura/migrations:/hasura-migrations
      - ./hasura/metadata:/hasura-metadata
      - ./hasura/seeds:/hasura-seeds
    depends_on:
      data-connector-agent:
        condition: service_healthy

  data-connector-agent:
    image: hasura/graphql-data-connector:v2.37.0
    restart: always
    ports:
      - 8081:8081
    environment:
      QUARKUS_LOG_LEVEL: ERROR # FATAL, ERROR, WARN, INFO, DEBUG, TRACE
      ## https://quarkus.io/guides/opentelemetry#configuration-reference
      QUARKUS_OPENTELEMETRY_ENABLED: "false"
      ## QUARKUS_OPENTELEMETRY_TRACER_EXPORTER_OTLP_ENDPOINT: http://jaeger:4317
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8081/api/v1/athena/health"]
      interval: 5s
      timeout: 10s
      retries: 5
      start_period: 5s

volumes:
  db_data:

frontendやbackendなどのserviceはそこまで難しくない内容なので詳しくは解説しません
graphql-engineに記述しているenvironment、特にenvファイルから呼び出している内容は下記になります

HASURA_GRAPHQL_METADATA_DATABASE_URL: postgresql://${username}:${password}@postgres:${port}/${database}?options=--search_path%3D${schema}
PG_DATABASE_URL: postgresql://{ホスト名}:{ポート番号}/{DB名}?user={ユーザ名}&password={パスワード}
HASURA_GRAPHQL_ADMIN_SECRET: Hasuraサインイン時に必要なパスワード
HASURA_GRAPHQL_METADATA_DEFAULTS: もともとcomposeファイルに記述された内容をenvファイルに移動させただけ
NESTJS_REMOTE_SCHEMA: backend[Nest.js]のGraphQLサーバーURL(今回は「http://host.docker.internal:3000/graphql」)

HASURA_GRAPHQL_METADATA_DATABASE_URLPG_DATABASE_URL同じURLで問題ないです

Dockerfile

Dockerfileはfrontendとbackendの2つを記述していきます

Terminal
mkdir docker
mkdir docker/frontend docker/backend
touch docker/frontend/Dockerfile docker/backend/Dockerfile
docker/frontend/Dockerfile
FROM node:18.17.0-alpine3.18
EXPOSE 80
WORKDIR /frontend

RUN apk update && \
  apk upgrade

CMD ["npm", "run", "dev"]
docker/backend/Dockerfile
FROM node:18.17.0-alpine3.18
WORKDIR /backend
CMD ["npm", "run", "start:dev"]

Next.jsの環境構築

一度ルートディレクトリまで戻ってから環境構築をしていく
今回は下記の内容で設定していきましたが、個々人が作業しやすいように設定を変更して問題ないです

ただし、project nameを変更する場合はdocker-compose.ymlなどの記述内容を書き換えてください

Terminal
npx create-next-app@latest --ts

✔ What is your project named? … frontend
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … Yes
✔ What import alias would you like configured? … @/*

apolloやcodegenなどフロントでGraphQLを使用するのに必要なパッケージをインストールしていく

Terminal
cd frontend

npm i graphql @apollo/client @as-integrations/next
npm i -D @graphql-codegen/near-operation-file-preset @graphql-codegen/typescript-resolvers @graphql-eslint/eslint-plugin @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request @graphql-codegen/cli @graphql-codegen/client-preset

codegenの設定をしていく
自分なりの設定になりますので個々人が作業しやすい設定に変更して問題ありません

schema設定はbackendディレクトリを指定しているので
もし、backend側のディレクトリ名を変更する場合は記述を変更してください

Terminal
touch codegen.ts
codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
import * as dotenv from "dotenv";
dotenv.config();

const config: CodegenConfig = {
  overwrite: true,
  schema: [
    {
      [process.env.HASURA_GRAPHQL_ENDPOINT]: {
        headers: {
          "x-hasura-admin-secret": process.env.HASURA_GRAPHQL_ADMIN_SECRET,
        },
      },
    },
  ],
  documents: "src/**/*.gql",
  hooks: {
    afterAllFileWrite: ["prettier --write"],
  },
  generates: {
    "src/types/graphql.gen.ts": {
      plugins: ["typescript", "typescript-resolvers"],
      config: {
        enumsAsTypes: true,
        namingConvention: "keep",
        avoidOptionals: true,
        scalars: {
          BigInt: " string",
          ISO8601Date: "string",
          ISO8601DateTime: "string",
        },
      },
    },
    "src/": {
      preset: "near-operation-file",
      presetConfig: {
        extension: ".gen.ts",
        baseTypesPath: "types/graphql.gen.ts",
      },
      plugins: ["typescript-operations", "typescript-react-apollo"],
      config: {
        gqlImport: "@apollo/client#gql",
        constEnums: true,
        reactApolloVersion: 3,
        withComponent: false,
        withHOC: false,
        withHooks: true,
        enumsAsTypes: true,
        namingConvention: "keep",
        avoidOptionals: true,
      },
    },
  },
};

export default config;
HASURA_GRAPHQL_ENDPOINT: HasuraのGraphQLのエンドポイント
HASURA_GRAPHQL_ADMIN_SECRET: Hasuraサインイン時に必要なパスワード

package.jsonscriptを修正していく

package.json
{
  ...
  "scripts": {
    "dev": "next dev -p 80",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "codegen": "graphql-codegen --require dotenv/config --config codegen.ts"
  },
  ...
}

一先ずNext.jsディレクトリの構築は完了しました
次にNest.jsディレクトリを構築していく

Nest.jsの環境構築

一度ルートディレクトリまで戻ってから環境構築をしていく
今回は下記の内容で設定していきましたが、個々人が作業しやすいように設定を変更して問題ないです

project nameを変更する場合はdocker-compose.ymlなどの記述内容を書き換えてください

Terminal
npx nest new backend

? Which package manager would you ❤️  to use? npm

graphqlとapolloなどNest.jsで必要なパッケージをインストールしていく

Terminal
cd backend
npm i nestjs/typeorm typeorm pg @nestjs/graphql @nestjs/apollo graphql apollo-server-express

app.module.tsを整えていく
Nest.js用のGrapQLサーバーが立つように修正をしていく

src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { join } from 'path';
import { GraphQLModule } from '@nestjs/graphql';
import { ConfigModule } from '@nestjs/config';
import { ApolloDriver } from '@nestjs/apollo';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from './db/database.config';
import { OrdersModule } from './orders/orders.module';
import { CustomersModule } from './customers/customers.module';

@Module({
  imports: [
    OrdersModule,
    CustomersModule,
    GraphQLModule.forRoot({
      driver: ApolloDriver,
      debug: true,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      installSubscriptionHandlers: true,
      sortSchema: true,
    }),
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`],
      load: [],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useClass: TypeOrmConfigService,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

DB関連の設定は別ファイルで管理するようにする

src/db/database.config.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    const configService = new ConfigService();
    return {
      type: 'postgres',
      host: configService.get('DATABASE_HOST'),
      port: configService.get('DATABASE_PORT'),
      username: configService.get('DATABASE_USER'),
      password: configService.get('DATABASE_PASSWORD'),
      database: configService.get('DATABASE_DB'),
      schema: configService.get('DATABASE_SCHEMA'),
      entities: [],
      synchronize: true,
    };
  }
}

Hasuraの環境構築

まずHasuraCLIをインストールしていく

OSによってインストール方法が違うので自分に合うインストール方法を選んでください
今回はMac用のインストール方法を行っていきます

Terminal
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash

ルートディレクトリまで戻ったら下記コマンドを叩いてHasuraの雛形を生成する

Terminal
hasura init

生成された中にconfig.yamlがあるので編集をしていく
HASURA_GRAPHQL_ADMIN_SECRETはcomposeファイルで設定したHasuraにサインインする際のパスワードを設定しておいてください

config.yaml
version: 3
endpoint: http://localhost:8080/
admin_secret: HASURA_GRAPHQL_ADMIN_SECRET
enable_telemetry: false
api_paths:
  v1_query: v1/query
  v2_query: v2/query
  v1_metadata: v1/metadata
  graphql: v1/graphql
  config: v1alpha1/config
  pg_dump: v1alpha1/pg_dump
  version: v1/version
metadata_directory: metadata
migrations_directory: migrations
seeds_directory: seeds
actions:
  kind: synchronous
  handler_webhook_baseurl: http://localhost:3000
  codegen:
    framework: ""
    output_dir: ""

Hasuraの環境構築は完了です

動作チェック

環境が整ったらdockerで仮想環境を整えていきます
下記コマンドを叩いてください

コーヒーを飲みながら環境が整うまで待機です

Terminal
docker compose up

各ポートは下記になります

localhost     : フロントエンド
localhost:3000: バックエンド
localhost:8080: Hasura Console

3つ開くことを確認出来れば動作確認は完了です
次にHasura Consoleを使ってHasuraの設定を整えて行きます

CleanShot 2024-03-29 at 06.31.43@2x.png

CleanShot 2024-03-29 at 06.31.05@2x.png

CleanShot 2024-03-29 at 06.32.06@2x.png

Hasuraの設定を整える

localhost:8080でHasuraが立っていますが、CLI経由でHasuraを起動すると設定した内容を永続化することが可能です

作業用のルートディレクトリ内で下記のコマンドを叩いてください
そうすると自動でブラウザが開くはずです

Terminal
hasura --project hasura console

CleanShot 2024-03-29 at 06.32.38@2x.png

開いたらHasuraとPostgreSQLを繋げていきます

HasuraとPostgreSQLを繋げる

  1. Hasura Console画面上部にあるDataをクリック
  2. Connect DatabaseをクリックしてDBを選択する画面に遷移

CleanShot 2024-03-29 at 06.33.16@2x.png

  1. 今回はPostgresを選択
  2. Connect Existing Databaseをクリック

CleanShot 2024-03-29 at 06.35.46@2x.png

  1. DB名を入力
  2. Environment variableを選択
  3. 入力フォームにcomposeファイルで定義したPG_DATABASE_URLを入力
  4. Connect Databaseをクリック

CleanShot 2024-03-29 at 06.48.25@2x.png

下記画面に遷移すればHasuraとPostgreSQLを繋げることに成功しました

CleanShot 2024-03-29 at 06.50.06@2x.png

Hasuraディレクトリ内のファイルが生成・変更が加わっていることを確認する
特にhasura/metadata/databasesは変更が顕著に変わっている箇所なのでわかりやすいです

backend(Nest.js)を整えていく

Nest.jsの使い方を知っている方は読み飛ばして問題ないです

今回はcustomerテーブルtaskテーブルの2テーブルを作成していきます

まずはターミナルで必要なファイルをコマンドで作成していきます

Terminal
cd backend

nest g mo Customer
nest g s Customer
nest g r Customer
nest g cl Customer

nest g mo Task
nest g s Task
nest g r Task
nest g cl Task

Customerテーブル

customer/customer.ts
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Task } from 'src/tasks/task';
import {
  Column,
  CreateDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity('customers')
@ObjectType()
export class Customer {
  @PrimaryGeneratedColumn({
    name: 'id',
    unsigned: true,
    type: 'int',
    comment: 'Customer ID',
  })
  @Field(() => ID)
  readonly id: number;

  @Column('text', { comment: 'ユーザーネーム' })
  @Field()
  readonly username: string;

  @CreateDateColumn({ comment: '作成日時' })
  @Field()
  readonly created_at: Date;

  @UpdateDateColumn({ comment: '更新日時' })
  @Field()
  readonly updated_at: Date;

  @OneToMany(() => Task, (task) => task.customer)
  @Field(() => [Task])
  readonly tasks: Task[];
}
customer/customer.resolver.ts
import { Args, Query, Resolver } from '@nestjs/graphql';
import { CustomerService } from './customer.service';
import { Customer } from './customer';

@Resolver(() => Customer)
export class CustomerResolver {
  constructor(private customerService: CustomerService) {}

  @Query(() => Customer)
  async getCustomer(
    @Args({ name: 'customerId' }) customerId: number,
  ): Promise<Customer> {
    return await this.customerService.findOne(customerId);
  }
}
customer/cusotmer.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Customer } from './customer';
import { Repository } from 'typeorm';

@Injectable()
export class CustomerService {
  constructor(
    @InjectRepository(Customer)
    private customerRepository: Repository<Customer>,
  ) {}

  async findOne(id: number): Promise<Customer> {
    return await this.customerRepository.findOne({
      where: { id },
      relations: ['tasks'],
    });
  }
}
customer/customer.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Customer } from './customer';
import { CustomerResolver } from './customer.resolver';
import { CustomerService } from './customer.service';
import { Task } from 'src/task/task';

@Module({
  imports: [
    TypeOrmModule.forFeature([Customer]),
    TypeOrmModule.forFeature([Task]),
  ],
  providers: [CustomerResolver, CustomerService],
  exports: [CustomerService],
})
export class CustomerModule {}

Hasuraを知っている方からするとResolverファイルに記述されている内容に疑問を持つかもしれませんが、気にしないでください
今回は環境構築をしていくのがメインなので

Taskテーブル

Task/Task.ts
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { Customer } from 'src/customers/customer';
import {
  Column,
  CreateDateColumn,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity('tasks')
@ObjectType()
export class Task {
  @PrimaryGeneratedColumn({
    name: 'id',
    unsigned: true,
    type: 'int',
    comment: 'タスクID',
  })
  @Field(() => ID)
  readonly id: number;

  @Column('text', { comment: 'タスク名' })
  @Field()
  readonly name: string;

  @CreateDateColumn({ comment: '作成日時' })
  @Field()
  readonly created_at: Date;

  @UpdateDateColumn({ comment: '更新日時' })
  @Field()
  readonly updated_at: Date;

  @ManyToOne(() => Customer, (customer) => customer.tasks)
  @Field(() => Customer)
  @JoinColumn({ name: 'customer_id' })
  readonly customer: Customer;
}
task/task.resolver.ts
import { Task } from './task';
import { TaskService } from './task.service';
import { Args, Query, Resolver } from '@nestjs/graphql';

@Resolver()
export class TaskResolver {
  constructor(private tasksService: TaskService) {}

  @Query(() => Task)
  async getTask(@Args({ name: 'taskId' }) taskId: number): Promise<Task> {
    return await this.tasksService.findOne(taskId);
  }
}
task/task.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Task } from './task';
import { Repository } from 'typeorm';

@Injectable()
export class TaskService {
  constructor(
    @InjectRepository(Task)
    private taskRepository: Repository<Task>,
  ) {}

  async findOne(id: number): Promise<Task> {
    return await this.taskRepository.findOne({
      where: { id },
      relations: ['customer'],
    });
  }
}
task/task.module.ts
import { TaskService } from './task.service';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Task } from './task';
import { Customer } from 'src/customer/customer';
import { TaskResolver } from './task.resolver';

@Module({
  imports: [
    TypeOrmModule.forFeature([Task]),
    TypeOrmModule.forFeature([Customer]),
  ],
  providers: [TaskResolver, TaskService],
  exports: [TaskService],
})
export class TaskModule {}

2つテーブルが記述できたらapp.module.tsdb/database.config.tsを修正していきます

app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { join } from 'path';
import { GraphQLModule } from '@nestjs/graphql';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ApolloDriver } from '@nestjs/apollo';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmConfigService } from './db/database.config';
import { HasuraModule } from '@golevelup/nestjs-hasura';
+ import { CustomerModule } from './customer/customer.module';
+ import { TaskModule } from './task/task.module';

const configService = new ConfigService();

@Module({
  imports: [
+   TaskModule,
+   CustomerModule,
    GraphQLModule.forRoot({
      driver: ApolloDriver,
      debug: true,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      installSubscriptionHandlers: true,
      sortSchema: true,
    }),
    HasuraModule.forRoot(HasuraModule, {
      webhookConfig: {
        /**
         * The value of the secret Header. The Hasura module will ensure that incoming webhook payloads contain this
         * value in order to validate that it is a trusted request
         */
        secretFactory: 'secret',
        /** The name of the Header that Hasura will send along with all event payloads */
        secretHeader: 'secret-header',
      },
      managedMetaDataConfig: {
        metadataVersion: 'v3',
        dirPath: configService.get('HASURA_METADATA_PATH'),
        secretHeaderEnvName: 'NESTJS_EVENT_WEBHOOK_SHARED_SECRET',
        nestEndpointEnvName: 'NESTJS_EVENT_WEBHOOK_ENDPOINT',
        defaultEventRetryConfig: {
          intervalInSeconds: 15,
          numRetries: 3,
          timeoutInSeconds: 100,
          toleranceSeconds: 21600,
        },
      },
    }),
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: [`.env.${process.env.NODE_ENV}`, `.env`],
      load: [],
    }),
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useClass: TypeOrmConfigService,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
db/database.config.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { Customer } from 'src/customers/customer';
import { Task } from 'src/tasks/task';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  createTypeOrmOptions(): TypeOrmModuleOptions {
    const configService = new ConfigService();
    return {
      type: 'postgres',
      host: configService.get('DATABASE_HOST'),
      port: configService.get('DATABASE_PORT'),
      username: configService.get('DATABASE_USER'),
      password: configService.get('DATABASE_PASSWORD'),
      database: configService.get('DATABASE_DB'),
      schema: configService.get('DATABASE_SCHEMA'),
-     entities: [],
+     entities: [Customer, Task],
      synchronize: true,
    };
  }
}

ここまできてターミナルにて下記画像のように成功ログが流れればマイグレーションが通った証拠になります
これにてbackend(Nest.js)側は終了です

CleanShot 2024-03-29 at 21.58.01.png

Hasuraとbackend(Nest.js)を繋げる

HasuraのRemote Schemaを使うことでbackend(Nest.js)をHasuraに統合出来ます
統合することでリクエストをHasuraに一元化することが可能です

Hasuraにてテーブルを追跡する

backend(Nest.js)でマイグレーションしたテーブルをHasuraで追跡するようしてあげます

  1. Hasura Console画面上部のDATAをクリック
  2. Track Allをクリックしてアラートを表示させる
  3. アラートのOKをクリック

CleanShot 2024-03-29 at 23.40.49@2x.png

  1. テーブルを追跡するようになり2つのテーブルがリレーションするようにTrack Allをクリック
  2. アラートが表示されたらOKをクリック

CleanShot 2024-03-29 at 23.43.00@2x.png

そうするとHasuraディレクトリが自動でファイルを生成・更新がされているので確認をしてみてください

Remote Schemaの設定

  1. REMOTE SCHEMASをクリック
  2. Addをクリックして追加画面に遷移

CleanShot 2024-03-29 at 23.24.30@2x.png

  1. Remote Schema Nameを入力
  2. Remote Schemaの説明を入力
  3. backend(Nest.js)のGraphQLサーバーのURLを指定(今回は{{NESTJS_REMOTE_SCHEMA}})
  4. Add Remote Schemaをクリック

CleanShot 2024-03-29 at 23.27.21@2x.png

追加が成功するとbackend(Nest.js)のresolverで記述したQueryが表示されたらHasuraと繋げることに成功です

CleanShot 2024-03-29 at 23.30.01@2x.png

データをインサートする

PostgresSQLでインサートするのも良いですが、折角なのでHasuraからデータをインサートしていきます

Customerデータをインサート

usernameを入力してSaveをクリックしてあげればCustomerデータのインサートが完了です

CleanShot 2024-03-30 at 00.01.58.png

Taskデータをインサート

nameを入力してSaveをクリックしてあげればTaskデータのインサートが完了です

CleanShot 2024-03-30 at 00.02.13.png

GraphQLを叩いてみる

  1. Hasura Console画面上部のAPIをクリック
  2. 好きにGraphQLを入力して実行してみてください

CleanShot 2024-03-30 at 00.13.31@2x.png

例1
query MyQuery {
  customers {
    tasks {
      name
    }
    username
  }
}

backend(Nest.js)でresolverに記述したQueryも実行出来ることを確かめてみるのも良いかと思います

例2
query MyQuery {
  getCustomer(customerId: 1) {
    tasks {
      name
    }
    username
  }
}

frontend(Next.js)からデータ取得

最後にfrontend(Next.js)からHasuraにリクエストを送ってデータを取得していきます

作業用ルートディレクトリに戻ったらfrontendディレクトへ移動する

Terminal
cd frontend

GraphQLからCodegennでReact Hookを生成していく

Hasuraに投げるGrahpQLを記述していく

Terminal
mkdir src/gql

touch src/gql/customer.gql

一度Hasuraで実行して問題なく動作をすることを確認してからGrahpQLを書くとエラーに悩まされずcodegenを実行出来ます

src/gql/customer.gql
# Try out GraphQL queries here
query GetCustomerById($id: Float!) {
  getCustomer(customerId: $id) {
    username
    tasks {
      name
    }
  }
}

query GetCustomers {
  customers {
    username
    tasks {
      name
    }
  }
}

GraphQLが書けたらcodegenReact Hooksを自動で生成していく
packege.jsonに記述してもらったscriptを実行する

Terminal
npm run codegen

> frontend@0.1.0 codegen
> graphql-codegen --require dotenv/config --config codegen.ts

✔ Parse Configuration
✔ Generate outputs

ApolloClientを整える

Hasuraにfrontend(Next.js)アクセスするためにApolloClientを記述していきます

Terminal
mkdir src/utils
touch src/utils/apolloClient.ts
src/utils/apolloClient.ts
import {
  ApolloClient,
  InMemoryCache,
  ApolloLink,
  createHttpLink,
} from "@apollo/client";
import { onError } from "@apollo/client/link/error";

const errorLink = onError((errors) => {
  const { graphQLErrors, networkError } = errors;
  if (graphQLErrors)
    graphQLErrors.forEach(({ message, locations, path }) =>
      console.log(
        `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
      )
    );
  if (networkError) console.log(`[Network error]: ${networkError}`);
});

const httpLink = createHttpLink({
  uri: process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ENDPOINT,
  headers: {
    "x-hasura-admin-secret": `${process.env.NEXT_PUBLIC_HASURA_GRAPHQL_ADMIN_SECRET}`,
  },
});

const link = ApolloLink.from([errorLink, httpLink]);

export const client = new ApolloClient({
  ssrMode: typeof window === "undefined",
  cache: new InMemoryCache(),
  link,
});

Providerを記述していく

グローバルにApolloClientへアクセスするためにProviderを整えていきます

Terminal
mkdir src/providers
touch src/providers/AppProvider.tsx
src/providers/AppProvider.tsx
import { client } from "../utils/apolloClient";
import { ApolloProvider } from "@apollo/client";
import { FC, ReactNode } from "react";

type Props = { children: ReactNode };

export const AppProvider: FC<Props> = ({ children }) => {
  return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
src/app/page.tsx
"use client";

import Image from "next/image";
import styles from "./page.module.css";
import { AppProvider } from "@/providers/AppProvider";

export default function Home() {
  return (
    <main className={styles.main}>
      <AppProvider>
        <div className={styles.description}>
          <p>
            Get started by editing&nbsp;
            <code className={styles.code}>src/app/page.tsx</code>
          </p>
          <div>
            <a
              href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
              target="_blank"
              rel="noopener noreferrer"
            >
              By{" "}
              <Image
                src="/vercel.svg"
                alt="Vercel Logo"
                className={styles.vercelLogo}
                width={100}
                height={24}
                priority
              />
            </a>
          </div>
        </div>

        <div className={styles.center}>
          <Image
            className={styles.logo}
            src="/next.svg"
            alt="Next.js Logo"
            width={180}
            height={37}
            priority
          />
        </div>

        <div className={styles.grid}>
          <a
            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            className={styles.card}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2>
              Docs <span>-&gt;</span>
            </h2>
            <p>Find in-depth information about Next.js features and API.</p>
          </a>

          <a
            href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            className={styles.card}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2>
              Learn <span>-&gt;</span>
            </h2>
            <p>
              Learn about Next.js in an interactive course with&nbsp;quizzes!
            </p>
          </a>

          <a
            href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            className={styles.card}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2>
              Templates <span>-&gt;</span>
            </h2>
            <p>Explore starter templates for Next.js.</p>
          </a>

          <a
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            className={styles.card}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2>
              Deploy <span>-&gt;</span>
            </h2>
            <p>
              Instantly deploy your Next.js site to a shareable URL with Vercel.
            </p>
          </a>
        </div>
      </AppProvider>
    </main>
  );
}

codegenで生成したReact Hooksからデータを取得する

今回は簡単なコンポーネントを作成してそこからデータを取得してくるようにする

Terminal
mkdir src/componets
touch src/componets/userComponent.tsx
src/componets/user.tsx
import { useGetCustomersQuery } from "@/gql/customers/customers.gen";
import { useEffect } from "react";

export const Test = () => {
  const { data, loading, error } = useGetCustomersQuery();

  useEffect(() => {
    if (!loading) console.log(data);
  }, [data, loading]);

  return <>GetUser</>;
};

作成したコンポーネントをpage.tsxでimportしてくる

src/app/page.tsx
"use client";

import Image from "next/image";
import styles from "./page.module.css";
import { AppProvider } from "@/providers/AppProvider";
+ import { UserComponent } from "@/components/userComponent";

export default function Home() {
  return (
    <main className={styles.main}>
      <AppProvider>
        <div className={styles.description}>
          <p>
            Get started by editing&nbsp;
            <code className={styles.code}>src/app/page.tsx</code>
          </p>
          <div>
            <a
              href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
              target="_blank"
              rel="noopener noreferrer"
            >
              By{" "}
              <Image
                src="/vercel.svg"
                alt="Vercel Logo"
                className={styles.vercelLogo}
                width={100}
                height={24}
                priority
              />
            </a>
          </div>
        </div>
+       <UserComponent />

        <div className={styles.center}>
          <Image
            className={styles.logo}
            src="/next.svg"
            alt="Next.js Logo"
            width={180}
            height={37}
            priority
          />
        </div>

        <div className={styles.grid}>
          <a
            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            className={styles.card}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2>
              Docs <span>-&gt;</span>
            </h2>
            <p>Find in-depth information about Next.js features and API.</p>
          </a>

          <a
            href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            className={styles.card}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2>
              Learn <span>-&gt;</span>
            </h2>
            <p>
              Learn about Next.js in an interactive course with&nbsp;quizzes!
            </p>
          </a>

          <a
            href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            className={styles.card}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2>
              Templates <span>-&gt;</span>
            </h2>
            <p>Explore starter templates for Next.js.</p>
          </a>

          <a
            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
            className={styles.card}
            target="_blank"
            rel="noopener noreferrer"
          >
            <h2>
              Deploy <span>-&gt;</span>
            </h2>
            <p>
              Instantly deploy your Next.js site to a shareable URL with Vercel.
            </p>
          </a>
        </div>
      </AppProvider>
    </main>
  );
}

ディベロッパーモードに入ってConsoleを見るとHasuraにアクセスしてデータを取得していることが分かります

CleanShot 2024-03-30 at 09.43.03@2x.png

これにてHasuraを使った環境構築は終了になります
ここから色々試してより使い慣れていくのもありです

今回の記事を書くために作成したコードは下記になります
色々試しているため記事に記述されていない内容も含まれていますが、見ていただければ幸いです

まとめ

Hasuraについて調べたら手が止まらないほどおもしろいものでした
まだまだ試していないものが多くなるので今以上に学んで記事にしていけたらと思います

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1