Closed13

NestJS+GraphQL+TypeORMセットアップ(雑)

s0ars0ar

リアルタイムに書いていないのでところどころ記憶だよりになる

s0ars0ar

https://docs.nestjs.com/graphql/quick-start

とりあえずここ見てセットアップ。
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

起動した。(たぶん)

s0ars0ar

とりあえず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.tsUsersModuleがimportされてそのまま使えるようになる。
べんり。

s0ars0ar

めちゃくちゃ雑に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に出力される。

s0ars0ar

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に応じて変わります。

s0ars0ar

ほなTypeORMでDBにつなげるようにしまっせ。
設定どうやんの?ってなわけでググって見つけたこれを参考に実装。

https://betterprogramming.pub/nest-js-project-with-typeorm-and-postgres-ce6b5afac3be

とりま.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ねえな」ってね。

s0ars0ar

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

まあ起動するよね。

s0ars0ar

ほな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;
}

なあにこれえ…

s0ars0ar

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

だいぶスッキリやね。

s0ars0ar

さて、この辺でmigrationしとかんとね。

アプリケーション実行時に自動的にmigrationする機能もあるけど、ちょいといじるたびにALTER TABLEするのちょっと怖い。

使いたければどっかにsynchronizeとかいう設定があるのでtrueにしたら勝手にmigrationが走るようになる。

てなわけでmigrationできるようにする。

とりあえず、Generating migrationsを見た。

こう書いてある。

$ typeorm migration:generate -n PostRefactoring

いやそれじゃダメでしょ?こうでしょ?

$ npm run typeorm migration:generate -n PostRefactoring


ダメー!

もう全然ダメ。
具体的に何がダメだったのか覚えてないレベルでダメ。
そもそも-nなんてオプションがないじゃん。

とりあえず、解決までにやったことをずらずら書いていく。
詳細な経緯は端折る。

s0ars0ar

とりあえず設定ファイルを作る。
さっき(いつ?)作った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^)\

s0ars0ar

これだけではダメ。
何がダメかっていうと、さっき作ったsrc/modules/users/entities/user.entity.tsからsrc/shared/entities/deletable.entity.tsの依存関係を解決できない。
なんやて工藤…

小一時間ああでもないこうでもない言い続けて分かった。
migration:genarateコマンドはtsconfig.jsonbaseUrlの設定をガン無視する」

NestJSはセットアップ時にtsconfig.jsonを作ってくれて、そのときにbaseUrl./にしてくれるんですね。
なので、import文にsrc/hoge/fugaみたいに書けるわけです。
これがmigration:generateコマンドだと解決できんってわけですね。
f**k

てなわけでこれを解決すべきなんですがどうしたら良い…?
https://stackoverflow.com/questions/68059776/typeorm-migrationgenerate-not-recognise-import-baseurl
天才おった。

よく見たら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は実行されていない。

このスクラップは2022/12/09にクローズされました