ある機会がありHasuraというものを知りました
Prismaチームが開発したPlaygroudは使用した経験があるので、Hasuraとどんな違いがあるのか調べてみたらPlaygroudより使いやすく開発工数をガンガン削減出来るなと感じました!
なので、もしこの記事を読んでHasuraのことを知ってもらえればと幸いです
Hasuraについて
簡単に言うとGraphQLサーバー
になります
Hasuraはテーブルを追跡することでCRUDや集計用Queryなどを自動的に用意してくれる優れものです
また、他の自前で用意したGraphQLサーバーとHasuraを統合してリクエストをHasura一つにお任せすることも可能です
単純なCRUDや集計はHasuraに任せて、ビジネスロジックを含むような処理は自前で用意したGraphQLサーバーに任せることで工数の削減に繋げられるかと思います
HasuraにはCloud上で操作するかDockerで構築して操作するかの二種類があります
今回はDockerを使って環境構築をしていきます
ファイル構成
.
├── 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のテーブル定義
やアクセス権限まわり
などのファイル郡を管理するためのディレクトリ
環境構築
mkdir my_app
cd my_app
作業用のルートディレクトリを作成して移動していく
docker-compose.ymlを記述
Hasura公式に記載されているcomposeファイルを取得してくる
取得してきたComposeファイルに肉付けしていく流れになります
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ファイルが下記になる
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_URL
とPG_DATABASE_URL
同じURLで問題ないです
Dockerfile
Dockerfileはfrontendとbackendの2つを記述していきます
mkdir docker
mkdir docker/frontend docker/backend
touch docker/frontend/Dockerfile docker/backend/Dockerfile
FROM node:18.17.0-alpine3.18
EXPOSE 80
WORKDIR /frontend
RUN apk update && \
apk upgrade
CMD ["npm", "run", "dev"]
FROM node:18.17.0-alpine3.18
WORKDIR /backend
CMD ["npm", "run", "start:dev"]
Next.jsの環境構築
一度ルートディレクトリまで戻ってから環境構築をしていく
今回は下記の内容で設定していきましたが、個々人が作業しやすいように設定を変更して問題ないです
ただし、project name
を変更する場合はdocker-compose.ymlなどの記述内容を書き換えてください
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を使用するのに必要なパッケージをインストールしていく
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側のディレクトリ名を変更する場合は記述を変更してください
touch 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.json
のscript
を修正していく
{
...
"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などの記述内容を書き換えてください
npx nest new backend
? Which package manager would you ❤️ to use? npm
graphqlとapolloなどNest.jsで必要なパッケージをインストールしていく
cd backend
npm i nestjs/typeorm typeorm pg @nestjs/graphql @nestjs/apollo graphql apollo-server-express
app.module.tsを整えていく
Nest.js用のGrapQLサーバーが立つように修正をしていく
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関連の設定は別ファイルで管理するようにする
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用のインストール方法を行っていきます
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
ルートディレクトリまで戻ったら下記コマンドを叩いてHasuraの雛形を生成する
hasura init
生成された中にconfig.yaml
があるので編集をしていく
HASURA_GRAPHQL_ADMIN_SECRET
はcomposeファイルで設定したHasuraにサインインする際のパスワードを設定しておいてください
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で仮想環境を整えていきます
下記コマンドを叩いてください
コーヒーを飲みながら環境が整うまで待機です
docker compose up
各ポートは下記になります
localhost : フロントエンド
localhost:3000: バックエンド
localhost:8080: Hasura Console
3つ開くことを確認出来れば動作確認は完了です
次にHasura Consoleを使ってHasuraの設定を整えて行きます
Hasuraの設定を整える
localhost:8080でHasuraが立っていますが、CLI経由でHasuraを起動すると設定した内容を永続化することが可能です
作業用のルートディレクトリ内で下記のコマンドを叩いてください
そうすると自動でブラウザが開くはずです
hasura --project hasura console
開いたらHasuraとPostgreSQLを繋げていきます
HasuraとPostgreSQLを繋げる
- Hasura Console画面上部にある
Data
をクリック -
Connect Database
をクリックしてDBを選択する画面に遷移
- 今回は
Postgres
を選択 -
Connect Existing Database
をクリック
- DB名を入力
-
Environment variable
を選択 - 入力フォームにcomposeファイルで定義した
PG_DATABASE_URL
を入力 -
Connect Database
をクリック
下記画面に遷移すればHasuraとPostgreSQLを繋げることに成功しました
Hasuraディレクトリ内のファイルが生成・変更が加わっていることを確認する
特にhasura/metadata/databases
は変更が顕著に変わっている箇所なのでわかりやすいです
backend(Nest.js)を整えていく
Nest.jsの使い方を知っている方は読み飛ばして問題ないです
今回はcustomerテーブル
、taskテーブル
の2テーブルを作成していきます
まずはターミナルで必要なファイルをコマンドで作成していきます
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テーブル
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[];
}
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);
}
}
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'],
});
}
}
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テーブル
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;
}
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);
}
}
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'],
});
}
}
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.ts
とdb/database.config.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 {}
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)側は終了です
Hasuraとbackend(Nest.js)を繋げる
HasuraのRemote Schema
を使うことでbackend(Nest.js)をHasuraに統合出来ます
統合することでリクエストをHasuraに一元化することが可能です
Hasuraにてテーブルを追跡する
backend(Nest.js)でマイグレーションしたテーブルをHasuraで追跡するようしてあげます
- Hasura Console画面上部の
DATA
をクリック -
Track All
をクリックしてアラートを表示させる - アラートの
OK
をクリック
- テーブルを追跡するようになり2つのテーブルがリレーションするように
Track All
をクリック - アラートが表示されたら
OK
をクリック
そうするとHasuraディレクトリが自動でファイルを生成・更新がされているので確認をしてみてください
Remote Schemaの設定
-
REMOTE SCHEMAS
をクリック -
Add
をクリックして追加画面に遷移
-
Remote Schema Name
を入力 - Remote Schemaの説明を入力
- backend(Nest.js)のGraphQLサーバーのURLを指定(今回は
{{NESTJS_REMOTE_SCHEMA}}
) -
Add Remote Schema
をクリック
追加が成功するとbackend(Nest.js)のresolverで記述したQueryが表示されたらHasuraと繋げることに成功です
データをインサートする
PostgresSQLでインサートするのも良いですが、折角なのでHasuraからデータをインサートしていきます
Customerデータをインサート
username
を入力してSave
をクリックしてあげればCustomerデータのインサートが完了です
Taskデータをインサート
name
を入力してSave
をクリックしてあげればTaskデータのインサートが完了です
GraphQLを叩いてみる
- Hasura Console画面上部の
API
をクリック - 好きにGraphQLを入力して実行してみてください
query MyQuery {
customers {
tasks {
name
}
username
}
}
backend(Nest.js)でresolverに記述したQueryも実行出来ることを確かめてみるのも良いかと思います
query MyQuery {
getCustomer(customerId: 1) {
tasks {
name
}
username
}
}
frontend(Next.js)からデータ取得
最後にfrontend(Next.js)からHasuraにリクエストを送ってデータを取得していきます
作業用ルートディレクトリに戻ったらfrontendディレクトへ移動する
cd frontend
GraphQLからCodegennでReact Hookを生成していく
Hasuraに投げるGrahpQLを記述していく
mkdir src/gql
touch src/gql/customer.gql
一度Hasuraで実行して問題なく動作をすることを確認してからGrahpQLを書くとエラーに悩まされずcodegenを実行出来ます
# 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を実行する
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を記述していきます
mkdir src/utils
touch 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を整えていきます
mkdir src/providers
touch 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>;
};
"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
<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>-></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>-></span>
</h2>
<p>
Learn about Next.js in an interactive course with 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>-></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>-></span>
</h2>
<p>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</AppProvider>
</main>
);
}
codegenで生成したReact Hooksからデータを取得する
今回は簡単なコンポーネントを作成してそこからデータを取得してくるようにする
mkdir src/componets
touch src/componets/userComponent.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してくる
"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
<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>-></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>-></span>
</h2>
<p>
Learn about Next.js in an interactive course with 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>-></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>-></span>
</h2>
<p>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</AppProvider>
</main>
);
}
ディベロッパーモードに入ってConsole
を見るとHasuraにアクセスしてデータを取得していることが分かります
これにてHasuraを使った環境構築は終了になります
ここから色々試してより使い慣れていくのもありです
今回の記事を書くために作成したコードは下記になります
色々試しているため記事に記述されていない内容も含まれていますが、見ていただければ幸いです
まとめ
Hasuraについて調べたら手が止まらないほどおもしろいものでした
まだまだ試していないものが多くなるので今以上に学んで記事にしていけたらと思います