Closed31

はじめてのNestJS + TypeORMでentityファイルからmigrationファイルを作成するまで -> 認証機能作るまでやりたい(フロントも)

yutaro.matsumotoyutaro.matsumoto

はじめに

9月から新しい会社で働きます。
その会社でNestJSを使うことになりそうなので、NestJSの勉強をしています(Express, NestJSどちらも経験がない状態です)。

その際、TypeORMのmigration:generateコマンドで少し詰まったので、その際の解決方法を整理・記録するために記事を書いております。
ちなみにですが、NestJSのサーバーとDBはdockerで用意しております(今回は特に触れません)。

やったこと

1. NestJSのTypeORM Integration

こちら(公式)を参考にしました。
まず、記事通りライブラリをインストールしました。
今回はdbにpostgreSQLを利用したかったので、mysql2ではなくpgをインストールしました。

yarn add @nestjs/typeorm typeorm pg

次に、デフォルトで用意されていたapp.module.tsにTypeOrmModuleをimportしました。
フォルダ構成は少しいじっております(entityフォルダを追加等)。

app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CustomNamingStrategy } from './customNamingStrategy'


@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: '127.0.0.1',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'db',
      entities: ['src/entity/*.ts'],
      migrations: ['src/migration/**/*.ts'],
      synchronize: true, // TODO: 本番環境では推奨されていないため、環境によって分ける
      namingStrategy: new CustomNamingStrategy(),
    }),
  ]
})
export class AppModule {
  constructor(private dataSource: DataSource) {}
}

CustomNamingStrategyについてはこちらの記事を参考に作成したものになります。必須ではないので、今回は触れません。
entityはこちらの記事(公式)を参考に作成しました。

2. Migration

とりあえずテーブルを作りたかったので、次にこちらを確認しました。

すると、 「MigrationはNestJSのソースコードとは分離されているので、TypeORMのドキュメントを読んでください」 的なことが書かれていました。
指示通りTypeORMのドキュメントを確認して進めます。

3. TypeORMのドキュメントを読んで進める(2の続き)

こちらを参考に進めました。

ts-nodeのインストールをします。

yarn add ts-node -D

次に、package.jsonのscriptsに下記を追記します。

package.json
"typeorm": "typeorm-ts-node-commonjs"

次に、以下を実行してエラーになりました。
NestJSもTypeORMも初めての私はここから苦戦しました・・・

typeorm migration:generate -- -n migrationNameHere

エラー内容

Generates a new migration file with sql needs to be executed to update schema.
オプションではない引数が 0 個では不足しています。少なくとも 1 個の引数が必要です:
error Command failed with exit code 1.
yutaro.matsumotoyutaro.matsumoto

上記のエラーはこちらの投稿を参考にして解決しました。
やったことはざっくり以下のような感じです。

1. ormconfig.tsを作成する

src/config/db配下にormconfig.tsを作りました。
(これに伴い、app.module.tsでのTypeOrmModuleのimportは不要になります。)

ormconfig.ts
import { DataSource } from 'typeorm'
import { CustomNamingStrategy } from './customNamingStrategy'

const source = new DataSource({
  type: 'postgres',
  host: '127.0.0.1',
  port: 5432,
  username: 'postgres',
  password: 'postgres',
  database: 'db',
  entities: ['src/entity/*.ts'],
  migrations: ['src/migration/**/*.ts'],
  synchronize: true, // TODO: 本番環境では推奨されていないため、環境によって分ける
  namingStrategy: new CustomNamingStrategy(),
})

export default source

2. package.jsonのscriptsを編集する

scriptsの"typeorm"を以下のようにします。

package.json
"typeorm": "typeorm-ts-node-commonjs -d ./src/config/db/ormconfig.ts"

3. migration:generate コマンドを実行する

以下のようなコマンドを実行します(細かい部分は皆さんの環境に沿ったものに変えてください)。

yarn typeorm migration:generate src/migration/****

コマンドを実行すると、entityファイルの情報を元にmigrationファイルが作成されます。
今回の場合はsrc/migrationの直下に以下のような名前のファイルが作成されます(****の部分はusers-tableと入力しました)。
1658842282804-users-table.ts

4. migrationを実行

以下のコマンドを実行すれば、migrationファイルを元にテーブルを作成してくれます。

yarn typeorm migration:run
yutaro.matsumotoyutaro.matsumoto

毎回 typeorm と入力するのは面倒なので、scriptsを以下のようにしました。

package.json
"typeorm": "typeorm-ts-node-commonjs -d ./src/config/db/ormconfig.ts",
"migration:gen": "yarn typeorm migration:generate",
"migration:run": "yarn typeorm migration:run",
"migration:revert": "yarn typeorm migration:revert",
"migration:show": "yarn typeorm migration:show"
yutaro.matsumotoyutaro.matsumoto

user.module.tsとかでTypeOrmModuleを使おうとすると怒られてしまった。
やはり、app.module.tsでもTypeOrmModuleを読み込む必要があるらしい・・・
ということで、app.module.tsは以下のようにしたら動きました!
ormconfigで設定した値をここでも使えたら良いのですが、なかなか実現できなかったので一旦この設定で先に進みます。
(誰か助けてください・・!!)

ちなみに、entitiesの部分は"Cannot use import statement outside a module"というエラーと格闘した結果、たどり着いた値に直しました。

app.module.ts
import { Module } from '@nestjs/common'
import { TestModule } from 'src/module/test.module'
import { AuthModule } from 'src/module/auth.module'
import { UsersModule } from 'src/module/users.module'
import { TypeOrmModule } from '@nestjs/typeorm'
import { CustomNamingStrategy } from 'src/config/db/customNamingStrategy'
import { databaseProviders } from './config/db/database.provider'

// TODO: ormconfigの内容を参照できるようにしたい
@Module({
  imports: [
    // ---最低限動く設定--- //
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: '127.0.0.1',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'db',
      entities: ['dist/entity/*.entity.{js,ts}'],
      synchronize: true, // TODO: 本番環境では推奨されていないため、環境によって分ける
      namingStrategy: new CustomNamingStrategy(),
    }),
    TestModule,
    UsersModule,
    AuthModule,
  ],
  })
export class AppModule {}

分かんないことだらけ!
次はnestjs/configを使って環境ごとにDBの設定を分けれるようにする。

dallPdallP

Nest, TypeORMでプロジェクト作成しており、非常に参考になりました。
ありがとうございます!

TODOの件、既に解決されているかもですが一つの対処法として、

  1. orm-data-source-options.ts: 純粋なDataSourceOptionのみを定義・exportする
  2. orm-data-source.ts: 1のOptionをimportし、マイグレーションコマンド専用のデータソースを定義・exportする
  3. app.module.ts: 1のOptionをimportし、NestのTypeOrmModuleに食わせる

という形でアプリケーション・マイグレーションがそれぞれ動作することが確認できました!
(DataSourceの定義が2つになってしまうのがイケてないですが。。)

yutaro.matsumotoyutaro.matsumoto

コメントありがとうございます。
確かにいけそうですね!
私もより良い方法あったらコメント残します!

yutaro.matsumotoyutaro.matsumoto

@nextjs/config, cross-envを利用してpackage.jsonのscriptsを更新。

package.json
"typeorm:dev": "cross-env NODE_ENV=development typeorm-ts-node-commonjs -d ./src/config/db/ormconfig.ts",
"typeorm:prod": "cross-env NODE_ENV=production typeorm-ts-node-commonjs -d ./src/config/db/ormconfig.ts",
"migration:gen": "yarn typeorm:dev migration:generate",
"migration:run": "yarn typeorm:dev migration:run",
"migration:revert": "yarn typeorm:dev migration:revert",
"migration:show": "yarn typeorm:dev migration:show",
"migration:gen:prod": "yarn typeorm:prod migration:generate",
"migration:run:prod": "yarn typeorm:prod migration:run",
"migration:revert:prod": "yarn typeorm:prod migration:revert",
"migration:show:prod": "yarn typeorm:prod migration:show"
yutaro.matsumotoyutaro.matsumoto

認証機能を作っていく

■ 参考記事
https://docs.nestjs.com/security/authentication
https://zenn.dev/uttk/articles/9095a28be1bf5d

■ 例外処理の実装について
アカウント登録機能を実装する際、すでに存在するメールアドレスを利用して処理を実行しようとした場合は、409のstatusを返したかった。
上記の実装については別途記事を書く(予定)。

yutaro.matsumotoyutaro.matsumoto

GraphQLでjwtを実装

GraphQLが動く環境は作れた。
整理してから投稿する。

yutaro.matsumotoyutaro.matsumoto

とりあえず動く環境はできていたが、手直ししないといけない部分がありそう。
NestJS + GraphQL + TypeORMの記事が意外となくて苦戦中。
HideFieldデコレータを利用すれば課題を解決できそう?

yutaro.matsumotoyutaro.matsumoto

とりあえず、distのフォルダ構成が正しくない。
src直下のものがdist直下になっているため、絶対パスのいimportが正しく動かない。

yutaro.matsumotoyutaro.matsumoto

フォルダ構成をいじる方法はわかった(tsconfigのoutDirをいじれば良い)。
だが、そもそもdistフォルダ内では絶対パスでのimportがうまくいってなさそう。

yutaro.matsumotoyutaro.matsumoto

tsconfig.build.jsonでexcludeの対象からdistを消してみても変わらない。
nestjsのdistフォルダ内で絶対パスを有効にする方法を調べてみる。

yutaro.matsumotoyutaro.matsumoto

そもそもdist内でも絶対パスが効いていればフォルダ構成を変える必要もないのか。

yutaro.matsumotoyutaro.matsumoto

これあまり困ってる人いないのか、tsconfigをちょっといじるだけで良いとかの単純な問題なのかのどっちかなのだろうか?
調べても中々出てこないな。

ハトすけハトすけ

こんにちは。業務でNestJS使っています。
基本的に絶対パスがおすすめですが、相対パスでも動くと思います。

src直下のものがdist直下になっているため、絶対パスのいimportが正しく動かない。

これって具体的にはどのような状況ですか?

yutaro.matsumotoyutaro.matsumoto

コメントいただきありがとうございます!
すみません、まずどんな問題が起きているのか具体的に記載させていただきます。
お時間あるときに確認いただけますと幸いです。

例えば、dist下にコンパイルされた以下のファイルで、Cannot find moduleのエラーが発生している状態になります(エラーはmigration実行時に起きております)。

user.entity.d.ts
// このimportで Cannot find module のエラーが起きる
import { UserStatus } from 'src/entity/types/UserTableTypes';

// importを以下のような相対パスにすればエラーは起きません。
import { UserStatus } from '../entity/types/UserTableTypes';

export declare class User {
    id: number;
    email: string;
    lastName: string;
    firstName: string;
    password: string;
    userStatus: UserStatus;
    readonly createdAt: Date;
    readonly updatedAt: Date;
    hashPassword(): Promise<void>;
    validateInsert(): Promise<void>;
    validateUpdate(): Promise<void>;
}

Nestの環境のルートディクトリは以下のようになっていまして、src下の構成がdist下にコンパイルされている状態でした(dist下にsrcというフォルダがないことが問題の原因なのかなと考えておりました。)。
ちなみに、今は dist/src/* というフォルダ構成にできていますが状況変わらずです。

.
├── README.md
├── dist
├── docker
├── docker-compose.yml
├── nest-cli.json
├── node_modules
├── ormlogs.log
├── package.json
├── src
├── test
├── tsconfig.build.json
├── tsconfig.build.tsbuildinfo
├── tsconfig.json
└── yarn.lock

問題を解決するためには、以下の2つのどちらかの方法かなと考えているところです。
長くなりましたが、お時間あるときにご意見いただけますと幸いです!

  • dist内で絶対パスを有効にする(そもそもできるのかも分かってません)
  • app.module.tsのTypeOrmModuleでdistを見ている状態なので、この状態をやめる(distをみるようにしないと動かなかった経緯で今のようになっているので、別途調査が必要)。
ハトすけハトすけ

なるほど、絶対パスを使っている部分が、コンパイル時に解決されずに、エラーになってしまってるんですね。

tsconfig.jsonのcompilerOptionsbaseUrlpathsはこのようになっている形ですか?

    "baseUrl": "./",
    "paths": {
      "src/*": ["./src/*"]
    } 

NodeJSでTS使ってるとよくある問題で、公式が提供しているtscという変換ツールは絶対パスを解釈してくれず、tsconfig-pathというツールを追加利用する必要があります。webpackはここらへん絶対パスを解釈してコンパイルしてくれます。おそらくnestのbuildコマンドはwebpackつかってると思うので、絶対パスを自動的に相対パスに置き換えてくれそうなのですが..

また、よくマイグレーションとかするときに、パスの問題にぶつかります。一番よいのは、マイグレーションの設定やファイルがsrcとは別に管理できることです。prismaだとデフォルトでルートにprismaフォルダができるので困らなかったのですが、TypeORMは触ったことがないため、どのような動きなのかちょっとわからないですね^^;

yutaro.matsumotoyutaro.matsumoto

ご親切に色々と教えてくださりありがとうございます!
今回やりたいことに関する記事でも大体prismaが使われていて、そっちを使ってみたくなりました。
ただ、業務の環境ではtypeormが使われているのでもう少し粘りたいと思います!
初めて環境を作ってから少し時間も経ってちょっと迷子になってる感もするので、改めて今の状況整理しつつ進めてみます。
また、少し整理したら環境も共有させていただきたいです。
よろしくお願いいたします!

yutaro.matsumotoyutaro.matsumoto

以下理由からdistのimportエラーと、今回のmigration generateのエラーは関係ない可能性が出てきた。

  • distがimportエラーのままでも、entityのimportのパスを相対パスにすれば動く
  • graphqlを導入する前の環境ならdistがimportエラーのままでもmigrationが動く
このスクラップは5ヶ月前にクローズされました