Cincoders

Módulo Task (Referência CRUD)

Módulo Task (Referência CRUD)

O módulo Task (src/modules/task/) é a implementação canônica de um CRUD completo no boilerplate.


📂 Estrutura do Módulo

src/modules/task/
├── dto/
│   ├── create-task.dto.ts     # Schema Zod e DTO de criação
│   └── update-task.dto.ts     # Schema .partial() e DTO de atualização
├── task.constants.ts          # Códigos válidos de status/prioridade (TASK_STATUS_CODES, TASK_PRIORITY_CODES)
├── task.type.ts                # Interface Task (shape serializado da resposta)
├── task.controller.ts         # Controller REST com OpenAPI / Swagger
├── task.module.ts             # Módulo NestJS
├── task.service.ts            # Regras de negócio usando PrismaService
└── task.service.spec.ts       # Testes unitários mockando PrismaService

Não há mais uma classe de entidade decorada (@Entity) — o modelo de dados vive em prisma/schema.prisma, e o módulo trabalha com o tipo gerado pelo Prisma Client mais a interface Task (task.type.ts) usada como shape de resposta.


🗄️ Modelo Prisma: Task

Status e prioridade são tabelas de lookup (TaskStatus, TaskPriority), não enums nativos — veja Persistência & Migrations para o racional:

// prisma/schema.prisma
model Task {
  id          String       @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  title       String
  description String?
  statusId    Int
  status      TaskStatus   @relation(fields: [statusId], references: [id])
  priorityId  Int
  priority    TaskPriority @relation(fields: [priorityId], references: [id])
  dueDate     DateTime?
  ownerId     String?      @db.Uuid
  createdAt   DateTime     @default(now())
  updatedAt   DateTime     @updatedAt

  @@index([ownerId])
  @@index([statusId])
  @@index([priorityId])
  @@map("tasks")
}

ownerId guarda o sub do token Keycloak do usuário autenticado que criou a tarefa (sem FK formal ainda — o módulo de perfil de usuário local é um passo futuro do boilerplate).

Os códigos válidos de status e prioridade ficam centralizados em task.constants.ts:

// src/modules/task/task.constants.ts
export const TASK_STATUS_CODES = ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'] as const;
export type TaskStatusCode = (typeof TASK_STATUS_CODES)[number];

export const TASK_PRIORITY_CODES = ['LOW', 'MEDIUM', 'HIGH', 'URGENT'] as const;
export type TaskPriorityCode = (typeof TASK_PRIORITY_CODES)[number];

export const DEFAULT_TASK_STATUS: TaskStatusCode = 'PENDING';
export const DEFAULT_TASK_PRIORITY: TaskPriorityCode = 'MEDIUM';

📝 Schemas Zod & DTOs

// src/modules/task/dto/create-task.dto.ts
import { createZodDto } from 'nestjs-zod';
import { z } from 'zod';
import { TASK_PRIORITY_CODES, TASK_STATUS_CODES } from '../task.constants';

export const CreateTaskSchema = z.object({
  title: z.string().min(3).max(200).describe('Título da tarefa'),
  description: z.string().optional().describe('Descrição detalhada'),
  priority: z.enum(TASK_PRIORITY_CODES).optional().describe('Prioridade da tarefa'),
  status: z.enum(TASK_STATUS_CODES).optional().describe('Status da tarefa'),
  dueDate: z.string().date().optional().describe('Data de vencimento (YYYY-MM-DD)'),
});

export class CreateTaskDto extends createZodDto(CreateTaskSchema) {}
// src/modules/task/dto/update-task.dto.ts
import { createZodDto } from 'nestjs-zod';
import { CreateTaskSchema } from './create-task.dto';

export const UpdateTaskSchema = CreateTaskSchema.partial();

export class UpdateTaskDto extends createZodDto(UpdateTaskSchema) {}

⚙️ Service com PrismaService

O service injeta PrismaService (disponível globalmente via PrismaModule) e usa include para trazer o code das tabelas de lookup relacionadas:

// src/modules/task/task.service.ts
@Injectable()
export class TaskService {
  constructor(private readonly prisma: PrismaService) {}

  async create(createTaskDto: CreateTaskDto, ownerId?: string): Promise<Task> {
    const task = await this.prisma.task.create({
      data: {
        title: createTaskDto.title,
        description: createTaskDto.description ?? null,
        dueDate: createTaskDto.dueDate ? new Date(createTaskDto.dueDate) : null,
        ownerId: ownerId ?? null,
        status: { connect: { code: createTaskDto.status ?? DEFAULT_TASK_STATUS } },
        priority: { connect: { code: createTaskDto.priority ?? DEFAULT_TASK_PRIORITY } },
      },
      include: { status: true, priority: true },
    });
    return this.serialize(task);
  }

  async findOne(id: string): Promise<Task> {
    const task = await this.prisma.task.findUnique({
      where: { id },
      include: { status: true, priority: true },
    });
    if (!task) {
      throw new ResourceNotFoundException('Tarefa', id);
    }
    return this.serialize(task);
  }

  // findAll, update e remove seguem o mesmo padrão: this.prisma.task.*
}

status/priority são atualizados via connect pelo code (não pelo id numérico), mantendo o DTO e a API pública desacoplados da chave interna da tabela de lookup.


🎮 Controller REST

// src/modules/task/task.controller.ts
@ApiTags('Tasks')
@ApiBearerAuth()
@Controller('tasks')
export class TaskController {
  constructor(private readonly taskService: TaskService) {}

  @Post()
  @ApiOperation({ summary: 'Criar uma nova tarefa' })
  @ApiResponse({ status: 201, description: 'Tarefa criada com sucesso' })
  create(
    @Body() createTaskDto: CreateTaskDto,
    @GetCurrentUser() user?: CurrentUser,
  ): Promise<Task> {
    return this.taskService.create(createTaskDto, user?.userId);
  }

  @Get()
  @ApiOperation({ summary: 'Listar tarefas paginadas' })
  @ApiResponse({ status: 200, description: 'Lista paginada de tarefas retornada' })
  findAll(@Query() query: PaginationQueryDto): Promise<PaginatedResponseDto<Task>> {
    return this.taskService.findAll(query);
  }

  @Get(':id')
  @ApiOperation({ summary: 'Buscar uma tarefa por ID' })
  @ApiResponse({ status: 200, description: 'Tarefa encontrada' })
  @ApiResponse({ status: 404, description: 'Tarefa não encontrada' })
  findOne(@Param('id', ParseUUIDPipe) id: string): Promise<Task> {
    return this.taskService.findOne(id);
  }

  @Patch(':id')
  @ApiOperation({ summary: 'Atualizar uma tarefa' })
  @ApiResponse({ status: 200, description: 'Tarefa atualizada' })
  @ApiResponse({ status: 404, description: 'Tarefa não encontrada' })
  update(
    @Param('id', ParseUUIDPipe) id: string,
    @Body() updateTaskDto: UpdateTaskDto,
  ): Promise<Task> {
    return this.taskService.update(id, updateTaskDto);
  }

  @Delete(':id')
  @ApiOperation({ summary: 'Remover uma tarefa' })
  @ApiResponse({ status: 204, description: 'Tarefa removida' })
  @ApiResponse({ status: 404, description: 'Tarefa não encontrada' })
  remove(@Param('id', ParseUUIDPipe) id: string): Promise<void> {
    return this.taskService.remove(id);
  }
}

ownerId é preenchido a partir do CurrentUser extraído do token Keycloak (@GetCurrentUser()) — não há checagem de propriedade (usuário só vê/edita as próprias tarefas) implementada ainda; isso faz parte de uma etapa futura de autenticação por sessão.


🧪 Testes

task.service.spec.ts mocka PrismaService com um objeto contendo apenas os métodos do model usados pelo service:

const prisma = {
  task: {
    create: vi.fn(),
    findMany: vi.fn(),
    count: vi.fn(),
    findUnique: vi.fn(),
    update: vi.fn(),
    delete: vi.fn(),
  },
};

const module: TestingModule = await Test.createTestingModule({
  providers: [TaskService, { provide: PrismaService, useValue: prisma }],
}).compile();

Veja o Guia de Testes para o padrão completo de mock do Prisma.

On this page