Nx Nestjs Mongodb Mongoose統合のセットアップ - 第2部

Nx Nest.js MongoDB Mongoose統合の第2部です。アプリケーションの効率性を高めるために、todosの生成、モジュールの作成、GraphQLモデル・リポジトリ・DTO・MongoDBスキーマの実装を行います。包括的な設定ガイドの続きにご期待ください。

技術公開日 読了目安 2 分

はじめに

Nx、Nest.js、MongoDB、Mongoose統合のチュートリアルシリーズ第2部へようこそ。今回は、Nxを使ってfeature-sample-todosライブラリを生成する方法、モジュール・リゾルバー・サービスの作成方法、そしてMongoDBドキュメントスキーマの構築方法を解説します。さらに、sample-todos機能に関するGraphQLモデル、リポジトリ、DTOの実装や、サービスとリゾルバーの更新についても取り上げます。手順に従って、これらの技術をスムーズに統合し、アプリケーションのスケーラビリティと効率性を高めていきましょう。今後も、設定と統合の旅は続きます。お楽しみに。

Feature-Sample-Todosの生成

まず、Nxを使ってfeature-sample-todosライブラリを生成します。

npx nx generate @nx/js:library api-feature-sample-todos --directory=libs/api/mongoose/feature-sample-mongoose --importPath=@libs/api/mongoose/feature-sample-mongoose --tags=scope:api --bundler=swc

✔ Which unit test runner would you like to use? · jest

モジュール・リゾルバー・サービスの作成

次に、sample-todos機能用のモジュール、リゾルバー、サービスを作成します。

nx generate @nx/nest:module sample-todos --project=api-feature-sample-todos
nx generate @nx/nest:resolver sample-todos --project=api-feature-sample-todos
nx generate @nx/nest:service sample-todos --project=api-feature-sample-todos

ファイルが作成されたら、libフォルダに移動します。

MongoDBドキュメント(スキーマ)の作成

MongoDB用のドキュメント(スキーマ)を作成します。

libs/api/feature-sample-todos/src/lib/sample-todos/models/sample-todos.schema.ts

import { AbstractDocument } from '@libs/api/mongoose/shared';
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';

@Schema({ versionKey: false })
export class SampleTodoDocument extends AbstractDocument {
  @Prop()
  content!: string;

  @Prop()
  editing!: boolean;

  @Prop()
  completed!: boolean;
}

export const SampleTodoSchema = SchemaFactory.createForClass(SampleTodoDocument);

GraphQLモデルの作成

GraphQL用のモデルを作成します。

libs/api/feature-sample-todos/src/lib/sample-todos/models/sample-todos.model.ts

import { AbstractModel } from '@libs/api/mongoose/shared';
import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
export class SampleTodo extends AbstractModel {
  @Field()
  readonly content!: string;

  @Field()
  readonly editing!: boolean;

  @Field()
  readonly completed!: boolean;
}

リポジトリの作成

sample-todos用のリポジトリを作成します。

libs/api/feature-sample-todos/src/lib/sample-todos/sample-todos.repository.ts

import { AbstractRepository } from '@libs/api/mongoose/shared';
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { SampleTodo } from './models/sample-todo.model';
import { SampleTodoDocument } from './models/sample-todo.schema';

@Injectable()
export class SampleTodosRepository extends AbstractRepository<SampleTodoDocument> {
  protected readonly logger = new Logger(SampleTodosRepository.name);

  constructor(
    @InjectModel(SampleTodo.name)
    SampleTodoModel: Model<SampleTodoDocument>
  ) {
    super(SampleTodoModel);
  }
}

DTOの作成

sample-todos用のDTOを作成します。

Args

libs/api/feature-sample-todos/src/lib/sample-todos/dto/args/get-sample-todos-args.dto.ts

import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';

@ArgsType()
export class GetSampleTodoArgs {
  @Field()
  @IsNotEmpty()
  @IsString()
  _id!: string;
}

Inputs

libs/api/feature-sample-todos/src/lib/sample-todos/dto/input/create-sample-todos-input.dto.ts

import { Field, InputType } from '@nestjs/graphql';
import { IsBoolean, IsString } from 'class-validator';

@InputType()
export class CreateSampleTodoInput {
  @Field()
  @IsString()
  content!: string;

  @Field()
  @IsBoolean()
  editing!: boolean;

  @Field()
  @IsBoolean()
  completed!: boolean;
}

libs/api/feature-sample-todos/src/lib/sample-todos/dto/input/delete-sample-todos-input.dto.ts

import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';

@InputType()
export class DeleteSampleTodoInput {
  @Field()
  @IsNotEmpty()
  @IsString()
  _id!: string;
}

libs/api/feature-sample-todos/src/lib/sample-todos/dto/input/update-sample-todos-input.dto.ts

import { Field, InputType } from '@nestjs/graphql';
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator';

@InputType()
export class UpdateSampleTodoInput {
  @Field()
  @IsNotEmpty()
  @IsString()
  _id!: string;

  @Field()
  @IsString()
  content!: string;

  @Field()
  @IsBoolean()
  editing!: boolean;

  @Field()
  @IsBoolean()
  completed!: boolean;
}

サービスの更新

sample-todos用のサービスを更新します。

libs/api/feature-sample-todos/src/lib/sample-todos/sample-todos.service.ts

import { Injectable } from '@nestjs/common';
import { GetSampleTodoArgs } from './dto/args/get-sample-todo-args.dto';
import { CreateSampleTodoInput } from './dto/input/create-sample-todo-input.dto';
import { DeleteSampleTodoInput } from './dto/input/delete-sample-todo-input.dto';
import { UpdateSampleTodoInput } from './dto/input/update-sample-todo-input.dto';
import { SampleTodoDocument } from './models/sample-todo.schema';
import { SampleTodosRepository } from './sample-todos.repository';

@Injectable()
export class SampleTodosService {
  constructor(private readonly SampleTodoRepository: SampleTodosRepository) {}

  async createSampleTodo(createSampleTodoData: CreateSampleTodoInput) {
    const SampleTodoDocument = await this.SampleTodoRepository.create({
      ...createSampleTodoData
    });

    return this.toModel(SampleTodoDocument);
  }

  async updateSampleTodo(updateSampleTodoData: UpdateSampleTodoInput) {
    const SampleTodoDocument = await this.SampleTodoRepository.findOneAndUpdate(
      { _id: updateSampleTodoData._id },
      updateSampleTodoData
    );
    return this.toModel(SampleTodoDocument);
  }

  async deleteSampleTodo(deleteSampleTodoData: DeleteSampleTodoInput) {
    return await this.SampleTodoRepository.deleteOne({
      _id: deleteSampleTodoData._id
    });
  }

  async getSampleTodos() {
    const SampleTodoDocument = await this.SampleTodoRepository.find({});
    return SampleTodoDocument.map((todo) => this.toModel(todo));
  }

  async getSampleTodo(getSampleTodoArgs: GetSampleTodoArgs) {
    const SampleTodoDocument = await this.SampleTodoRepository.findOne({
      ...getSampleTodoArgs
    });
    return this.toModel(SampleTodoDocument);
  }

  private toModel(SampleTodoDocument: SampleTodoDocument) {
    return {
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      //@ts-ignore
      _id: SampleTodoDocument._id.toHexString(),
      ...SampleTodoDocument
    };
  }
}

リゾルバーの更新

sample-todos用のリゾルバーを更新します。

libs/api/feature-sample-todos/src/lib/sample-todos/sample-todos.resolver.ts

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { GetSampleTodoArgs } from './dto/args/get-sample-todo-args.dto';
import { CreateSampleTodoInput } from './dto/input/create-sample-todo-input.dto';
import { DeleteSampleTodoInput } from './dto/input/delete-sample-todo-input.dto';
import { UpdateSampleTodoInput } from './dto/input/update-sample-todo-input.dto';
import { SampleTodo } from './models/sample-todo.model';
import { SampleTodosService } from './sample-todos.service';

@Resolver()
export class SampleTodosResolver {
  constructor(private readonly SampleTodoService: SampleTodosService) {}

  @Mutation(() => SampleTodo, { name: 'createSampleTodo' })
  async createSampleTodo(
    @Args('createSampleTodoData')
    createSampleTodoData: CreateSampleTodoInput
  ) {
    return await this.SampleTodoService.createSampleTodo(createSampleTodoData);
  }

  @Mutation(() => SampleTodo, { name: 'updateSampleTodo' })
  async updateSampleTodo(
    @Args('updateSampleTodoData')
    updateSampleTodoData: UpdateSampleTodoInput
  ) {
    return this.SampleTodoService.updateSampleTodo(updateSampleTodoData);
  }

  @Mutation(() => SampleTodo, { name: 'deleteSampleTodo', nullable: true })
  async deleteSampleTodo(
    @Args('deleteSampleTodoData')
    deleteSampleTodoData: DeleteSampleTodoInput
  ) {
    return this.SampleTodoService.deleteSampleTodo(deleteSampleTodoData);
  }

  @Query(() => [SampleTodo], { name: 'sampleTodos' })
  async getSampleTodos() {
    return this.SampleTodoService.getSampleTodos();
  }

  @Query(() => SampleTodo, { name: 'sampleTodo' })
  async getSampleTodo(@Args() getSampleTodoArgs: GetSampleTodoArgs) {
    return this.SampleTodoService.getSampleTodo(getSampleTodoArgs);
  }
}

モジュールの更新

sample-todos用のモジュールを更新します。

libs/api/feature-sample-todos/src/lib/sample-todos/sample-todos.module.ts

  • MongooseModuleを追加する。
  • SampleMongooseTableRepositoryをprovidersに追加する。
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { SampleTodo } from './models/sample-todo.model';
import { SampleTodoSchema } from './models/sample-todo.schema';
import { SampleTodosRepository } from './sample-todos.repository';
import { SampleTodosResolver } from './sample-todos.resolver';
import { SampleTodosService } from './sample-todos.service';

@Module({
  imports: [
    MongooseModule.forFeature([
      {
        name: SampleTodo.name,
        schema: SampleTodoSchema
      }
    ])
  ],
  providers: [SampleTodosResolver, SampleTodosService, SampleTodosRepository]
})
export class SampleTodosModule {}

app.module.tsの更新

apps/api/src/app/ディレクトリ内のapp.module.tsファイルを更新します。

  • DatabaseModuleを追加する。
  • SampleTodosModuleを追加する。
@Module({
  imports: [
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, '..', 'web/.next'),
      exclude: ['/api/*', '/api/graphql']
    }),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      path: '/api/graphql',
      autoSchemaFile: true
    }),
    // ---- Graphql ---- //
    // When using Mongoose, the DatabaseModule is required.
    DatabaseModule,
    SampleTodosModule
  ],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}

Nx、Nest.js、MongoDB、Mongooseの設定と統合を継続し、堅牢な開発体験を築いていきますので、次の記事もお楽しみに。

Nx Nestjs Mongodb Mongoose統合のセットアップ - 第3部