Loading...

MBC CQRS サーバーレス フレームワーク TO-DO システム作成 6 – データの更新・削除

データの作成、読込、検索を実装しましたので作成されたデータの更新・削除を実装します。
データの削除は論理削除となります。

データ更新パラメータ用DTOの追加

src/todo/dto/update-todo.dto.ts を作成し次のように入力します。

import { PartialType } from '@nestjs/swagger'
import { Transform, Type } from 'class-transformer'
import {
  IsBoolean,
  IsOptional,
  IsString,
  ValidateNested,
} from 'class-validator'

import { TodoAttributes } from './todo-attributes.dto'

export class UpdateTodoAttributes extends PartialType(TodoAttributes) {}

export class UpdateTodoDto {
  @IsString()
  @IsOptional()
  name?: string

  @IsBoolean()
  @Transform(({ value }) =>
    value === 'true' ? true : value === 'false' ? false : value,
  )
  @IsOptional()
  isDeleted?: boolean

  @Type(() => UpdateTodoAttributes)
  @ValidateNested()
  @IsOptional()
  attributes?: UpdateTodoAttributes

  constructor(partial: Partial<UpdateTodoDto>) {
    Object.assign(this, partial)
  }
}

サービスに更新処理を実装

src/todo/todo.service.ts に以下のように入力します。

  async update(
    detailDto: DetailDto,
    updateDto: UpdateTodoDto,
    opts: { invokeContext: IInvoke },
  ): Promise<TodoDataEntity> {
    const userContext = getUserContext(opts.invokeContext)
    const { tenantCode } = parsePk(detailDto.pk)
    if (userContext.tenantCode !== tenantCode) {
      throw new BadRequestException('Invalid tenant code')
    }
    const data = (await this.dataService.getItem(detailDto)) as TodoDataEntity
    if (!data) {
      throw new NotFoundException('Task not found!')
    }
    const commandDto: CommandPartialInputModel = {
      pk: data.pk,
      sk: data.sk,
      version: data.version,
      name: updateDto.name ?? data.name,
      isDeleted: updateDto.isDeleted ?? data.isDeleted,
      attributes: {
        ...data.attributes,
        ...updateDto.attributes,
      },
    }
    const item = await this.commandService.publishPartialUpdate(
      commandDto,
      opts,
    )
    return new TodoDataEntity(item as TodoDataEntity)
  }

サービスに削除処理を実装

src/todo/todo.service.ts に以下のように入力します。

  async remove(key: DetailDto, opts: { invokeContext: IInvoke }) {
    const userContext = getUserContext(opts.invokeContext)
    const { tenantCode } = parsePk(key.pk)

    if (userContext.tenantCode !== tenantCode) {
      throw new BadRequestException('Invalid tenant code')
    }

    const data = (await this.dataService.getItem(key)) as TodoDataEntity
    if (!data) {
      throw new NotFoundException()
    }
    const commandDto: CommandPartialInputModel = {
      pk: data.pk,
      sk: data.sk,
      version: data.version,
      isDeleted: true,
    }
    const item = await this.commandService.publishPartialUpdate(
      commandDto,
      opts,
    )

    return new TodoDataEntity(item as any)
  }

最終的に src/todo/todo.service.ts は以下の通りとなります。

import {
  CommandPartialInputModel,
  CommandService,
  DataService,
  DetailDto,
  generateId,
  getUserContext,
  IInvoke,
  toISOStringWithTimezone,
  VERSION_FIRST,
} from '@mbc-cqrs-serverless/core'
import {
  BadRequestException,
  Injectable,
  Logger,
  NotFoundException,
} from '@nestjs/common'
import { Prisma } from '@prisma/client'

import {
  generateTodoPk,
  generateTodoSk,
  getOrderBys,
  parsePk,
  TODO_PK_PREFIX,
} from '../helpers'
import { PrismaService } from '../prisma'
import { CreateTodoDto } from './dto/create-todo.dto'
import { TodoSearchDto } from './dto/search-todo.dto'
import { UpdateTodoDto } from './dto/update-todo.dto'
import { TodoCommandEntity } from './entity/todo-command.entity'
import { TodoDataEntity } from './entity/todo-data.entity'
import { TodoDataListEntity } from './entity/todo-data-list.entity'

@Injectable()
export class TodoService {
  private readonly logger = new Logger(TodoService.name)

  constructor(
    private readonly commandService: CommandService,
    private readonly dataService: DataService,
    private readonly prismaService: PrismaService,
  ) {}

  async create(
    createDto: CreateTodoDto,
    opts: { invokeContext: IInvoke },
  ): Promise<TodoDataEntity> {
    const { tenantCode } = getUserContext(opts.invokeContext)
    const pk = generateTodoPk(tenantCode)
    const sk = generateTodoSk()
    const task = new TodoCommandEntity({
      pk,
      sk,
      id: generateId(pk, sk),
      tenantCode,
      code: sk,
      type: TODO_PK_PREFIX,
      version: VERSION_FIRST,
      name: createDto.name,
      attributes: createDto.attributes,
    })
    const item = await this.commandService.publish(task, opts)
    return new TodoDataEntity(item as TodoDataEntity)
  }

  async findOne(detailDto: DetailDto): Promise<TodoDataEntity> {
    const item = await this.dataService.getItem(detailDto)
    if (!item) {
      throw new NotFoundException('Todo not found!')
    }
    this.logger.debug('item:', item)
    return new TodoDataEntity(item as TodoDataEntity)
  }

  async findAll(
    tenantCode: string,
    searchDto: TodoSearchDto,
  ): Promise<TodoDataListEntity> {
    const where: Prisma.TodoWhereInput = {
      isDeleted: searchDto.isDeleted ?? false,
      tenantCode,
    }
    if (searchDto.keyword?.trim()) {
      where.OR = [
        { name: { contains: searchDto.keyword.trim() } },
        { description: { contains: searchDto.keyword.trim() } },
      ]
    }

    if (searchDto.status) {
      where.status = searchDto.status
    }

    if (searchDto.dueDate_gte && searchDto.dueDate_lte) {
      where.dueDate = {
        gte: searchDto.dueDate_gte,
        lte: searchDto.dueDate_lte,
      }
    } else if (searchDto.dueDate_lte) {
      where.dueDate = {
        lte: searchDto.dueDate_lte,
      }
    } else if (searchDto.dueDate_gte) {
      where.dueDate = {
        gte: searchDto.dueDate_gte,
      }
    }

    const { pageSize = 10, page = 1, orderBys = ['-createdAt'] } = searchDto

    const [total, items] = await Promise.all([
      this.prismaService.todo.count({ where }),
      this.prismaService.todo.findMany({
        where,
        take: pageSize,
        skip: pageSize * (page - 1),
        orderBy: getOrderBys<Prisma.TodoOrderByWithRelationInput>(orderBys),
      }),
    ])

    return new TodoDataListEntity({
      total,
      items: items.map(
        (item) =>
          new TodoDataEntity({
            ...item,
            attributes: {
              description: item.description,
              dueDate: toISOStringWithTimezone(item.dueDate),
              status: item.status,
            },
          }),
      ),
    })
  }

  async update(
    detailDto: DetailDto,
    updateDto: UpdateTodoDto,
    opts: { invokeContext: IInvoke },
  ): Promise<TodoDataEntity> {
    const userContext = getUserContext(opts.invokeContext)
    const { tenantCode } = parsePk(detailDto.pk)
    if (userContext.tenantCode !== tenantCode) {
      throw new BadRequestException('Invalid tenant code')
    }
    const data = (await this.dataService.getItem(detailDto)) as TodoDataEntity
    if (!data) {
      throw new NotFoundException('Task not found!')
    }
    const commandDto: CommandPartialInputModel = {
      pk: data.pk,
      sk: data.sk,
      version: data.version,
      name: updateDto.name ?? data.name,
      isDeleted: updateDto.isDeleted ?? data.isDeleted,
      attributes: {
        ...data.attributes,
        ...updateDto.attributes,
      },
    }
    const item = await this.commandService.publishPartialUpdate(
      commandDto,
      opts,
    )
    return new TodoDataEntity(item as TodoDataEntity)
  }

  async remove(key: DetailDto, opts: { invokeContext: IInvoke }) {
    const userContext = getUserContext(opts.invokeContext)
    const { tenantCode } = parsePk(key.pk)

    if (userContext.tenantCode !== tenantCode) {
      throw new BadRequestException('Invalid tenant code')
    }

    const data = (await this.dataService.getItem(key)) as TodoDataEntity
    if (!data) {
      throw new NotFoundException()
    }
    const commandDto: CommandPartialInputModel = {
      pk: data.pk,
      sk: data.sk,
      version: data.version,
      isDeleted: true,
    }
    const item = await this.commandService.publishPartialUpdate(
      commandDto,
      opts,
    )

    return new TodoDataEntity(item as any)
  }
}

コントローラの実装

src/todo/todo.controller.ts に以下のように入力しAPIを実装します。

  @Patch('/:pk/:sk')
  async update(
    @INVOKE_CONTEXT() invokeContext: IInvoke,
    @Param() detailDto: DetailDto,
    @Body() updateDto: UpdateTodoDto,
  ) {
    this.logger.debug('updateDto:', updateDto)
    return this.todoService.update(detailDto, updateDto, { invokeContext })
  }

  @Delete('/:pk/:sk')
  async remove(
    @INVOKE_CONTEXT() invokeContext: IInvoke,
    @Param() detailDto: DetailDto,
  ) {
    return this.todoService.remove(detailDto, { invokeContext })
  }

最終的に src/todo/todo.controller.ts は以下のようになります。

import {
  DetailDto,
  getUserContext,
  IInvoke,
  INVOKE_CONTEXT,
  SearchDto,
} from '@mbc-cqrs-serverless/core'
import {
  Body,
  Controller,
  Delete,
  Get,
  Logger,
  Param,
  Patch,
  Post,
  Query,
} from '@nestjs/common'

import { CreateTodoDto } from './dto/create-todo.dto'
import { UpdateTodoDto } from './dto/update-todo.dto'
import { TodoDataEntity } from './entity/todo-data.entity'
import { TodoService } from './todo.service'

@Controller('api/todo')
export class TodoController {
  private readonly logger = new Logger(TodoController.name)

  constructor(private readonly todoService: TodoService) {}

  @Post('/')
  async create(
    @INVOKE_CONTEXT() invokeContext: IInvoke,
    @Body() createDto: CreateTodoDto,
  ): Promise<TodoDataEntity> {
    this.logger.debug('createDto:', createDto)
    return this.todoService.create(createDto, { invokeContext })
  }

  @Get('/')
  async findAll(
    @INVOKE_CONTEXT() invokeContext: IInvoke,
    @Query() searchDto: SearchDto,
  ) {
    this.logger.debug('searchDto:', searchDto)
    const { tenantCode } = getUserContext(invokeContext)
    return await this.todoService.findAll(tenantCode, searchDto)
  }

  @Get('/:pk/:sk')
  async findOne(@Param() detailDto: DetailDto): Promise<TodoDataEntity> {
    return this.todoService.findOne(detailDto)
  }

  @Patch('/:pk/:sk')
  async update(
    @INVOKE_CONTEXT() invokeContext: IInvoke,
    @Param() detailDto: DetailDto,
    @Body() updateDto: UpdateTodoDto,
  ) {
    this.logger.debug('updateDto:', updateDto)
    return this.todoService.update(detailDto, updateDto, { invokeContext })
  }

  @Delete('/:pk/:sk')
  async remove(
    @INVOKE_CONTEXT() invokeContext: IInvoke,
    @Param() detailDto: DetailDto,
  ) {
    return this.todoService.remove(detailDto, { invokeContext })
  }
}

データ更新のテスト

データの読込にて取得した pksk を使用してデータの更新をテストします。

次に以前作成したテストに以下を追加します。

test/api.http を開き以下を追加します。

### Get Specific To-do item
PATCH {{apiBaseUrl}}/api/todo/pk/sk
Accept: application/json
Content-Type: application/json
Authorization: {{ADMIN_TOKEN}}
X-Tenant-Code: MBC

{
  "name": "First update1",
  "attributes": {
    "description": "Descripton for first task",
    "dueDate": "2024-10-30T03:59:43.590Z"
  }
}

上記ソースの pksk の部分に ローカルのDynamoDB Adminで取得した pkskURLエンコードをしてセットします。

URLエンコードして pksk をセットした例は以下の通りです。

###
PATCH {{apiBaseUrl}}/todo/TODO%23MBC/01JAFJHS7KM8ZM50X5CX4T6517
Accept: application/json
Content-Type: application/json
Authorization: {{token}}
X-Tenant-Code: MBC

{
  "name": "First update1",
  "attributes": {
    "description": "Descripton for first task",
    "dueDate": "2024-10-30T03:59:43.590Z"
  }
}

トークンを取得してから上記を実行すると下記の様な結果を得ることが出来ます。

HTTP/1.1 200 OK
x-powered-by: Express
access-control-allow-origin: *
content-type: application/json; charset=utf-8
content-length: 597
etag: W/"255-vDX8XZ9ejyRrrTkcFzx3sC+COBA"
cache-control: no-cache
Date: Sun, 20 Oct 2024 16:12:49 GMT
Connection: close

{
  "code": "01JAFJHS7KM8ZM50X5CX4T6517",
  "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4",
  "createdIp": "127.0.0.1",
  "tenantCode": "MBC",
  "type": "TODO",
  "version": 3,
  "createdAt": "2024-10-20T16:12:49.597Z",
  "updatedIp": "127.0.0.1",
  "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4",
  "requestId": "0e9a2f88-a460-4c5f-a532-6ae7316a576b",
  "name": "First update",
  "sk": "01JAFJHS7KM8ZM50X5CX4T6517@3",
  "attributes": {
    "description": "desc",
    "status": "PENDING",
    "dueDate": "2024-10-30T03:59:43.590Z"
  },
  "id": "TODO#MBC#01JAFJHS7KM8ZM50X5CX4T6517",
  "pk": "TODO#MBC",
  "status": "finish:FINISHED",
  "updatedAt": "2024-10-20T16:12:49.597Z"
}

データ削除のテスト(論理削除)

続いてデータを削除します。

test/api.http に以下を追加します。

### Get Specific To-do item
PATCH {{apiBaseUrl}}/api/todo/pk/sk
Accept: application/json
Content-Type: application/json
Authorization: {{ADMIN_TOKEN}}
X-Tenant-Code: MBC

{
  "name": "First update1",
  "attributes": {
    "description": "Descripton for first task",
    "dueDate": "2024-10-30T03:59:43.590Z"
  }
}

上記ソースの pksk の部分に ローカルのDynamoDB Adminで取得した pkskURLエンコードをしてセットします。

URLエンコードして pksk をセットした例は以下の通りです。

###
DELETE {{apiBaseUrl}}/todo/TASK%23MBC/01JAFJHS7KM8ZM50X5CX4T6517
Accept: application/json
Content-Type: application/json
Authorization: {{token}}
X-Tenant-Code: MBC

トークンを取得してから上記を実行すると下記の様な結果を得ることが出来ます。

HTTP/1.1 200 OK
x-powered-by: Express
access-control-allow-origin: *
content-type: application/json; charset=utf-8
content-length: 614
etag: W/"266-rT24uxPBzFne8lHx5VkMfT0uM5M"
cache-control: no-cache
Date: Sun, 20 Oct 2024 16:17:31 GMT
Connection: close

{
  "code": "01JAFJHS7KM8ZM50X5CX4T6517",
  "updatedBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4",
  "createdIp": "127.0.0.1",
  "tenantCode": "MBC",
  "type": "TODO",
  "version": 4,
  "createdAt": "2024-10-20T16:17:31.511Z",
  "updatedIp": "127.0.0.1",
  "createdBy": "92ca4f68-9ac6-4080-9ae2-2f02a86206a4",
  "requestId": "78c24071-9c32-429d-8c94-12c4702fadcd",
  "name": "First update",
  "sk": "01JAFJHS7KM8ZM50X5CX4T6517@4",
  "attributes": {
    "description": "desc",
    "status": "PENDING",
    "dueDate": "2024-10-30T03:59:43.590Z"
  },
  "id": "TODO#MBC#01JAFJHS7KM8ZM50X5CX4T6517",
  "pk": "TODO#MBC",
  "status": "finish:FINISHED",
  "updatedAt": "2024-10-20T16:17:31.511Z",
  "isDeleted": true
}
Top