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

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

https://github.com/graphql/graphql-playground

## Hasuraについて

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

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

https://hasura.io

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

https://hasura.io/docs/latest/getting-started/getting-started-cloud/

https://hasura.io/docs/latest/getting-started/docker-simple/

## ファイル構成

```zsh: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だけでは処理しきれないビジネスロジックを含む処理などを担当

https://nestjs.com/

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

https://www.postgresql.org/

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

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

https://nextjs.org/

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

## 環境構築

```zsh:Terminal
mkdir my_app
cd my_app
```
作業用のルートディレクトリを作成して移動していく

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

https://hasura.io/docs/latest/getting-started/docker-simple/
```zsh: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ファイルが下記になる
```yml: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ファイルから呼び出している内容は下記になります
```yml
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_URL`と`PG_DATABASE_URL`同じURLで問題ないです

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

```zsh:Terminal
mkdir docker
mkdir docker/frontend docker/backend
touch docker/frontend/Dockerfile docker/backend/Dockerfile
```

```Dockerfile:docker/frontend/Dockerfile
FROM node:18.17.0-alpine3.18
EXPOSE 80
WORKDIR /frontend

RUN apk update && \
  apk upgrade

CMD ["npm", "run", "dev"]
```

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

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

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

```zsh: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を使用するのに必要なパッケージをインストールしていく

```zsh: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の設定をしていく
自分なりの設定になりますので個々人が作業しやすい設定に変更して問題ありません

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

```zsh:Terminal
touch codegen.ts
```

```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;
```

```yml
HASURA_GRAPHQL_ENDPOINT: HasuraのGraphQLのエンドポイント
HASURA_GRAPHQL_ADMIN_SECRET: Hasuraサインイン時に必要なパスワード
```

`package.json`の`script`を修正していく
```json: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の環境構築
一度ルートディレクトリまで戻ってから環境構築をしていく
今回は下記の内容で設定していきましたが、個々人が作業しやすいように設定を変更して問題ないです

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

```zsh:Terminal
npx nest new backend

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

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

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

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

```ts: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関連の設定は別ファイルで管理するようにする

```ts: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をインストールしていく

https://hasura.io/docs/latest/hasura-cli/install-hasura-cli/

OSによってインストール方法が違うので自分に合うインストール方法を選んでください
今回はMac用のインストール方法を行っていきます
```zhs:Terminal
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
```

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

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

```yml: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の環境構築は完了です

https://hasura.io/docs/latest/hasura-cli/quickstart/

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

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

```zsh:Terminal
docker compose up
```

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

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

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

![CleanShot 2024-03-29 at 06.31.43@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/f64d5c44-4ddd-7088-6a19-e4b232e103b3.png)

![CleanShot 2024-03-29 at 06.31.05@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/2a2727d2-da3b-ea15-d9bb-c93832f81311.png)

![CleanShot 2024-03-29 at 06.32.06@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/ae3377ae-5c62-f72f-ebd4-40ac39a4eb67.png)

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

https://hasura.io/docs/latest/hasura-cli/commands/hasura_console/

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

```zsh:Terminal
hasura --project hasura console
```

![CleanShot 2024-03-29 at 06.32.38@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/80cd0858-5424-3470-66e3-5c0a23e8100c.png)

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

### HasuraとPostgreSQLを繋げる

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

![CleanShot 2024-03-29 at 06.33.16@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/38964d27-1914-c313-82b3-e7fa6f0784fd.png)

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

![CleanShot 2024-03-29 at 06.35.46@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/ddecab9d-b2ba-8889-0a9f-4c43d8251c62.png)

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

![CleanShot 2024-03-29 at 06.48.25@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/9314330b-1d9c-f812-01ea-120935f579e6.png)

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

![CleanShot 2024-03-29 at 06.50.06@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/4932207b-6e8e-2462-06ee-52669f6d6afe.png)

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

### backend(Nest.js)を整えていく
Nest.jsの使い方を知っている方は読み飛ばして問題ないです

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

まずはターミナルで必要なファイルをコマンドで作成していきます
```zsh: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テーブル

```ts: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[];
}
```

```ts: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);
  }
}
```

```ts: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'],
    });
  }
}
```

```ts: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テーブル

```ts: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;
}
```

```ts: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);
  }
}
```

```ts: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'],
    });
  }
}
```

```ts: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 {}
```

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

```diff_typescript: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 {}
```

```diff_typescript: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/45eccbd8-a746-6ec4-2d85-2698f192bcc8.png)

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

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

https://hasura.io/docs/latest/remote-schemas/overview/

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

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

![CleanShot 2024-03-29 at 23.40.49@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/3bf4efdd-17f8-1c3f-08df-9cf7bd7d8b2f.png)

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

![CleanShot 2024-03-29 at 23.43.00@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/9b3b6788-9dad-e828-9d3e-1eef50d14905.png)

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

### Remote Schemaの設定

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

![CleanShot 2024-03-29 at 23.24.30@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/ace59497-c853-5a82-fc9b-9fdb0a4a7f1c.png)

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

![CleanShot 2024-03-29 at 23.27.21@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/9f03c262-35f1-9bbe-4342-ed137382fa3f.png)

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

![CleanShot 2024-03-29 at 23.30.01@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/44b0e3bb-4be6-7d71-0915-42194fa1358d.png)

## データをインサートする

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

### Customerデータをインサート

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

![CleanShot 2024-03-30 at 00.01.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/74e9ff24-d0b1-d44c-a9d2-a36071305f6b.png)

### Taskデータをインサート

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

![CleanShot 2024-03-30 at 00.02.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/8790a448-baeb-b59c-bcd5-ba266185fbbe.png)

## GraphQLを叩いてみる

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

![CleanShot 2024-03-30 at 00.13.31@2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/830c85aa-abb9-ac36-82e8-b3603cf05be3.png)

```graphQL:例1
query MyQuery {
  customers {
    tasks {
      name
    }
    username
  }
}
```

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

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

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

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

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

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

https://the-guild.dev/graphql/codegen

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

```zsh:Terminal
mkdir src/gql

touch src/gql/customer.gql
```

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

```graphql: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が書けたら`codegen`で`React Hooks`を自動で生成していく
packege.jsonに記述してもらったscriptを実行する

```zsh: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を記述していきます

https://www.apollographql.com/docs/react/

```zsh:Terminal
mkdir src/utils
touch src/utils/apolloClient.ts
```

```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を整えていきます

```zsh:Terminal
mkdir src/providers
touch src/providers/AppProvider.tsx
```

```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>;
};
```

```tsx: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からデータを取得する

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

```zsh:Terminal
mkdir src/componets
touch src/componets/userComponent.tsx
```

```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してくる

```tsx: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311195/3f5a8c8a-0f91-ec36-0b26-ab4cad8baa5c.png)

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


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

https://github.com/roll1226/study-next-nest

## まとめ

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