【備忘録】NestJS + TypeORM + Neon(Postgres DBaaS) でCRUDする

# はじめに
Nest CLIでスケルトンを作成した状態でのスタートです。
Nest v10.4.2
TypeORM 0.3系


## ライブラリのインストール
```console
npm install @nestjs/typeorm typeorm pg
```
```console
npm install ts-node
```

```console
npm install class-validator class-transformer
```

## 1.TypeORMにNeonの接続情報をロードする
**1.** `Neon`のダッシュボードからPostgresの接続アドレスを取得する
**2.** **.env**に記載する
**3.** **app.module.ts**からロードする設定を作成する
**4.** **作成した設定**を**app.module.ts**からロードする

```ts:src/config/typeormConfig.ts
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
import { config } from 'dotenv';

config({ path: ".env" })

export const typeOrmConfig: TypeOrmModuleOptions = {
    type: "postgres",
    url: process.env.DB_URL,
    synchronize: true, // 開発環境下のみtrueにしておく
}
```

```ts:src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmConfig } from 'src/config/typeormConfig';

@Module({
  imports: [
    TypeOrmModule.forRoot(typeOrmConfig)
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

## 2.Entityを作ってmigrationする
### **1.** `Entity`を作成する

https://typeorm.io/entities

```ts:src/entity/product.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Product {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ nullable: true })
  imageSrcPath: string | null;

  @Column({ nullable: true })
  deployUrl: string | null;

  @Column()
  productName: string;

  @Column()
  overview: string;

  @Column()
  mainTechnology: string;

  @Column({ nullable: true })
  subTechnology: string | null;

  @Column('text', { array: true })
  productLinks: string[] | null;
}
```
### **2.** `migration`するための接続ファイルを書く
```ts:src/ormconfig.ts
import { config } from 'dotenv';
import { DataSource } from 'typeorm';

config({ path: '.env' });

export const dataSource = new DataSource({
  type: 'postgres',
  url: process.env.DB_URL,
  entities: ['src/entity/*.entity.ts'],
  migrations: ['src/migration/*.ts'],
});
```
### **3.**`entity` -> `migrationファイル`の作成を行う

・package.jsonに追記
```json:package.json
"scripts": {
    "typeorm": "typeorm-ts-node-commonjs -d ./src/config/ormconfig.ts"
}
```
・migrationファイルを生成する
```console
npm run typeorm migration:generate .\src\migration\addNullable
```

### **4.**`migarationファイル` -> `DB`に反映する
```console
npm run typeorm migration:run
```

## 3. DTO作ってリクエストのバリデーションを行う
### **1.** main.tsに`validationPipe`を適用する
```ts:src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe());
  await app.listen(3000);
}
bootstrap();
```

### **2.** `DTO`を作って**validation**を作成する
```ts:src/dto/CreateProductDto.ts
import {
  IsArray,
  IsNotEmpty,
  IsOptional,
  IsString,
  IsUrl,
} from 'class-validator';

export class CreateProductDto {
  @IsOptional()
  @IsString()
  imageSrcPath: string | null;

  @IsOptional()
  @IsUrl()
  deployUrl: string | null;

  @IsString()
  @IsNotEmpty()
  productName: string;

  @IsString()
  @IsNotEmpty()
  overview: string;

  @IsString()
  @IsNotEmpty()
  mainTechnology: string;

  @IsOptional()
  @IsString()
  subTechnology: string | null;

  @IsOptional()
  @IsArray()
  @IsUrl({}, { each: true })
  productLinks: string[] | null;
}
```

## 4.エンドポイントを作成してCRUDを通す
### **1.** `module`・`controller`・`service`を作成する
```console
nest generate module products
nest generate controller products
nest generate service products
```

### **2.** `module`に**TypeORM**を読み込む
```ts:src/products/products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Product } from 'src/entity/product.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Product])],
  controllers: [ProductsController],
  providers: [ProductsService],
})
export class ProductsModule {}
```

### **3.** `controller`にルーティングメソッドを追加する
```ts:src/products/products.controller.ts
import { Body, Controller, Get, Post, Put } from '@nestjs/common';
import { ProductsService } from './products.service';
import { CreateProductDto } from 'src/dto/CreateProductDto';
import { UpdateProductDto } from 'src/dto/UpdateProductDto';

@Controller('products')
export class ProductsController {
  constructor(private readonly ProductsService: ProductsService) {}

  @Get()
  async readAll() {
    return await this.ProductsService.read();
  }

  @Post()
  async create(@Body() CreateProductDto: CreateProductDto) {
    return await this.ProductsService.create(CreateProductDto);
  }

  @Put()
  async update(@Body() updateProductDto: UpdateProductDto) {
    return await this.ProductsService.update(updateProductDto);
  }
}
```

### **4.** `service`に`controller`から呼び出されるロジックをメソッドとして追加する
```ts:src/products/products.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { CreateProductDto } from 'src/dto/CreateProductDto';
import { UpdateProductDto } from 'src/dto/UpdateProductDto';
import { Product } from 'src/entity/product.entity';
import { Repository } from 'typeorm';

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product)
    private productsRepository: Repository<Product>,
  ) {}

  async read() {
    const productsData = await this.productsRepository.find({
      order: {
        id: "ASC"
      }
    });

    const formattedProductData = productsData.map((productData) => ({
      ...productData,
      overview: productData.overview.split(","),
      mainTechnology: productData.mainTechnology.split(","),
      subTechnology: productData.subTechnology.split(",")
    }));

    return formattedProductData;
  }

  async create(CreateProductDto: CreateProductDto) {
    return await this.productsRepository.save(CreateProductDto);
  }

  async update(updateProductDto: UpdateProductDto) {
    return await this.productsRepository.update(
      updateProductDto.id,
      updateProductDto,
    );
  }
}
```

# さいごに
学習させていただいた技術記事のリンクです。

https://zenn.dev/yutaro_matsu/scraps/b7e7bfa82bd091

https://qiita.com/Aurum64/items/f5962bd2a643447dbef9