NestJS+GraphQL+TypeORMセットアップ(雑)
リアルタイムに書いていないのでところどころ記憶だよりになる
とりあえずここ見てセットアップ。
fastifyを選んだ。
$ npm i @nestjs/graphql @nestjs/apollo graphql apollo-server-fastify
app.module.ts
にGraphQLModuleの設定を書く。
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
起動してみる。
$ npm run start:dev
起動した。(たぶん)
とりあえずresourceを作ってみる
$ npx nest g res modules/users
こんな感じになる。
src/modules/users
├── dto
│ ├── create-user.input.ts
│ └── update-user.input.ts
├── entities
│ └── user.entity.ts
├── users.module.ts
├── users.resolver.spec.ts
├── users.resolver.ts
├── users.service.spec.ts
└── users.service.ts
このコマンド叩くだけでapp.module.ts
にUsersModule
がimportされてそのまま使えるようになる。
べんり。
めちゃくちゃ雑にuser.entity.tsをいじる
import { ObjectType, Field, Int } from '@nestjs/graphql';
@ObjectType()
export class User {
@Field(() => Int)
readonly id: number;
@Field(() => String)
name: string;
@Field(() => String)
email: string;
}
そんでもってapp.module.ts
をこんな感じにする。
(GraphQLのスキーマファイルの出力先を指定する)
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './modules/users/users.module';
@Module({
imports: [
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
// 自動的にGraphQLのスキーマファイルを指定したパスに出力する
autoSchemaFile: join(process.cwd(), 'src/graphql/schema.gql'),
}),
UsersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
そんでもって起動してみる。
$ npm run start:dev
そしたらスキーマファイルがsrc/graphql/schema.gql
に出力される。
TypeORMを入れる。
とりあえず公式のInstallation見てセットアップ。
$ npm install typeorm --save
$ npm install reflect-metadata --save
$ npm install @types/node --save-dev
$ npm install pg --save
@types/node
はたぶんNestJSインストールしたときに入ってるから、package.jsonにあったら改めて入れる必要はなさそう。
あと、コマンドコピペしてこれを書いてるので1つずつinstallしちゃってますが、普通にまとめてinstallしちゃえば良いと思います。
あと、pg
のところは使うDBに応じて変わります。
ほなTypeORMでDBにつなげるようにしまっせ。
設定どうやんの?ってなわけでググって見つけたこれを参考に実装。
とりま.env
から読み込めるように以下のパッケージインストール。
$ npm i @nestjs/config
んで、src/shared/typeorm/typeorm.service.ts
を実装。
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmOptionsFactory, TypeOrmModuleOptions } from '@nestjs/typeorm';
import { DatabaseType } from 'typeorm';
@Injectable()
export class TypeOrmService implements TypeOrmOptionsFactory {
constructor(private config: ConfigService) {}
public createTypeOrmOptions(): TypeOrmModuleOptions {
return {
type: this.config.get<DatabaseType>('DATABASE_TYPE'),
host: this.config.get<string>('DATABASE_HOST'),
port: this.config.get<number>('DATABASE_PORT'),
database: this.config.get<string>('DATABASE_NAME'),
username: this.config.get<string>('DATABASE_USER'),
password: this.config.get<string>('DATABASE_PASSWORD'),
synchronize: false,
} as TypeOrmModuleOptions;
}
}
ほいで、これだけでは使えんので、app.module.ts
のimportsにConfigModule
(.env
から変数を読み込むモジュール)とTypeOrmModule
(TypeORMのモジュール)を追加する。
あと、AppModuleのconstructorにDataSource
を追加する。
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { DataSource } from 'typeorm';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TypeOrmService } from './shared/typeorm/typeorm.service';
import { UsersModule } from './modules/users/users.module';
@Module({
imports: [
ConfigModule.forRoot({
envFilePath: [join(process.cwd(), '.env')],
}),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useClass: TypeOrmService,
}),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: join(process.cwd(), 'src/graphql/schema.gql'),
}),
UsersModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {
constructor(private dataSource: DataSource) {}
}
で、この辺でようやく気づくんだよね。
「そもそもDBねえな」ってね。
TypeORMの設定終わったやで~
そんなふうに考えていた時期が俺にもありました
何はともあれDB作る。
Dockerで作るで。
とりあえずdocker-compose.yml作る。
本筋じゃないのでさらっと。
version: '3.1'
services:
db:
image: postgres:14
container_name: blog-db
restart: always
volumes:
- ./src/db/initdb.d:/docker-entrypoint-initdb.d
- db-data:/var/lib/postgresql/blog-db/data
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: blog-db
POSTGRES_INITDB_ARGS: --encoding=UTF-8 --locale=C
volumes:
db-data:
あ、blog-dbっていう名前から察するにブログ作ろうとしてるねこいつ。(どうでもいい)
ほんで、src/db/initdb.d/init.sql
作るで。
初回にコンテナ作るときに勝手に実行してくれるやで。便利やで。
CREATE USER blogdbuser WITH PASSWORD 'blogdbuser' CREATEDB;
アプリケーションから接続するためのユーザーを作るだけやで。
ほな起動してみよねせやね。
$ docker-compose up
まあ起動するよね。
ほなTypeORMに戻るで。
とりあえずさっき作ったsrc/modules/users/entities/user.entity.ts
に肉付けしていくで。
(本来は概念的に別物な気がするので、GraphQLのobjectと悪魔合体するのは良くないかもしれない)
(でもやってみたかった)
import { ObjectType, Field, Int } from '@nestjs/graphql';
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
Timestamp,
UpdateDateColumn,
DeleteDateColumn,
} from 'typeorm';
@ObjectType()
@Entity('users')
export class User {
@Field(() => Int)
@PrimaryGeneratedColumn()
readonly id: number;
@Field(() => String)
@Column()
name: string;
@Field(() => String)
@Column()
email: string;
@Field(() => Date)
@CreateDateColumn({ name: 'created_at', precision: 0 })
readonly createdAt: Timestamp;
@Field(() => Date)
@UpdateDateColumn({ name: 'updated_at', precision: 0 })
readonly updatedAt: Timestamp;
@Field(() => Date, { nullable: true })
@DeleteDateColumn({
name: 'deleted_at',
precision: 0,
default: null,
nullable: true,
})
readonly deletedAt?: Timestamp;
}
なあにこれえ…
「id
, created_at
, updated_at
, deleted_at
をいちいち定義するのダルいよね」っていう当然の感想。
ベースクラス作ります。
src/shared/entities/base.entity.ts
import { Field, Int, ObjectType } from '@nestjs/graphql';
import {
CreateDateColumn,
PrimaryGeneratedColumn,
Timestamp,
UpdateDateColumn,
} from 'typeorm';
@ObjectType()
export abstract class EntityBase {
@Field(() => Int)
@PrimaryGeneratedColumn()
readonly id: number;
@Field(() => Date)
@CreateDateColumn({ name: 'created_at', precision: 0 })
readonly createdAt: Timestamp;
@Field(() => Date)
@UpdateDateColumn({ name: 'updated_at', precision: 0 })
readonly updatedAt: Timestamp;
}
src/shared/entities/deletable.entity.ts
import { Field, ObjectType } from '@nestjs/graphql';
import { DeleteDateColumn, Timestamp } from 'typeorm';
import { EntityBase } from './base.entity';
@ObjectType()
export abstract class DeletableEntity extends EntityBase {
@Field(() => Date, { nullable: true })
@DeleteDateColumn({
name: 'deleted_at',
precision: 0,
default: null,
nullable: true,
})
readonly deletedAt?: Timestamp;
}
作ってから思った。
「なんかこういうの実現する機能あるんちゃう」って。
でも深く考えるのはやめた。
ちなみにこいつら、最初は@Entity
デコレータつけてた。
でもそれやっちゃうと後述のmigration走らせたときに、base
テーブルとdeletable
テーブルが生成されちゃう。
なので、@Entity
デコレータはつけてない。
逆に、@ObjectType
デコレータはつけておかないとid
, createdAt
, updatedAt
, deletedAt
がGraphQLのスキーマに含まれないのでつけてる。
src/modules/users/entities/user.entity.ts
はこうなった。
import { ObjectType, Field } from '@nestjs/graphql';
import { DeletableEntity } from 'src/shared/entities/deletable.entity';
import { Column, Entity } from 'typeorm';
@ObjectType()
@Entity('users')
export class User extends DeletableEntity {
@Field(() => String)
@Column()
name: string;
@Field(() => String)
@Column()
email: string;
}
だいぶスッキリやね。
さて、この辺でmigrationしとかんとね。
アプリケーション実行時に自動的にmigrationする機能もあるけど、ちょいといじるたびにALTER TABLEするのちょっと怖い。
使いたければどっかにsynchronize
とかいう設定があるのでtrue
にしたら勝手にmigrationが走るようになる。
てなわけでmigrationできるようにする。
とりあえず、Generating migrationsを見た。
こう書いてある。
$ typeorm migration:generate -n PostRefactoring
いやそれじゃダメでしょ?こうでしょ?
$ npm run typeorm migration:generate -n PostRefactoring
ダメー!
もう全然ダメ。
具体的に何がダメだったのか覚えてないレベルでダメ。
そもそも-n
なんてオプションがないじゃん。
とりあえず、解決までにやったことをずらずら書いていく。
詳細な経緯は端折る。
とりあえず設定ファイルを作る。
さっき(いつ?)作ったsrc/shared/typeorm/typeorm.service.ts
に似て非なるormconfig.ts
っていうファイルを作る。
(ファイル名は何でも良いです)
import { DataSource, DataSourceOptions } from 'typeorm';
import { config } from 'dotenv';
config();
export default new DataSource({
type: process.env.DATABASE_TYPE,
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT,
database: process.env.DATABASE_NAME,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
synchronize: false,
logging: false,
entities: ['./src/**/entities/**/*.ts'],
migrations: ['./src/db/migrations/**/*.ts'],
} as DataSourceOptions);
あ、.env
から変数読み込むためにしれっとdotenv入れてます。
.env
自体はsrc/shared/typeorm/typeorm.service.ts
作ったときに作ってる(中身はお察し)ので別途作る必要なし。
$ npm install dotenv --save
これでこいつは終わり。
で、こいつをmigration:generate
コマンド実行時に読み込まないといけない。
どうするんですか?
-d
オプションで読み込めますよ。
つまりこうなる
$ npm run typeorm migration:generate -d ./ormconfig.ts -n init
(PostRefactoringの意味がわからんのでinitに変更)
/(^o^)\
これだけではダメ。
何がダメかっていうと、さっき作ったsrc/modules/users/entities/user.entity.ts
からsrc/shared/entities/deletable.entity.ts
の依存関係を解決できない。
なんやて工藤…
小一時間ああでもないこうでもない言い続けて分かった。
「migration:genarate
コマンドはtsconfig.json
のbaseUrl
の設定をガン無視する」
NestJSはセットアップ時にtsconfig.json
を作ってくれて、そのときにbaseUrl
を./
にしてくれるんですね。
なので、import文にsrc/hoge/fuga
みたいに書けるわけです。
これがmigration:generate
コマンドだと解決できんってわけですね。
f**k
てなわけでこれを解決すべきなんですがどうしたら良い…?
天才おった。よく見たらNestJSセットアップ時に設定されるjestのdebugコマンドがこれだった。
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
なんかちょっと腑に落ちた。
で、同じようにこうした。
"migration:generate": "node -r tsconfig-paths/register -r ts-node/register ./node_modules/typeorm/cli.js migration:generate -d ./ormconfig.ts",
"migration:run": "node -r tsconfig-paths/register -r ts-node/register ./node_modules/typeorm/cli.js migration:run -d ./ormconfig.ts",
そんでもって実行。
$ npm run migration:generate ./src/db/migrations/init
(なんとなくsrc/db/migrations
配下にいてほしい気がしたのでこうしている)
勝ち卍
src/db/migrations
配下にmigrationファイルが出力される。
この時点ではまだmigrationは実行されていない。
一旦ここまで…