Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c96fa9a
feat: add ToDo entity with text and completion status
gabrielteodoroo Aug 18, 2025
45fee97
feat: add ToDoRepository abstract class with create method
gabrielteodoroo Aug 18, 2025
acbc720
feat: add CreateToDo use case for creating new todos
gabrielteodoroo Aug 18, 2025
6fdd1cd
test: add ToDoRepositoryStub for testing todo use cases
gabrielteodoroo Aug 18, 2025
31f7456
test: add CreateToDo use case for creating new todos
gabrielteodoroo Aug 18, 2025
7dfbfaf
chore: add package-lock.json for dependency version locking
gabrielteodoroo Aug 18, 2025
d52db70
feat: add Prisma schema and migration for ToDo entity
gabrielteodoroo Aug 18, 2025
5cd42af
feat: add Swagger response and DTO for ToDo creation endpoint
gabrielteodoroo Aug 18, 2025
581e983
feat: implement PrismaToDoRepository with create method
gabrielteodoroo Aug 18, 2025
e4f2459
feat: add ToDoController with create endpoint and Swagger documentation
gabrielteodoroo Aug 18, 2025
423c4c8
feat: add ToDoModule with controller, repository and use case depende…
gabrielteodoroo Aug 18, 2025
ca37004
feat: add findAll method to ToDo repository
gabrielteodoroo Aug 18, 2025
471ea92
feat: add FindAllToDoUseCase for listing todos
gabrielteodoroo Aug 18, 2025
277eac8
test: add findAll method to ToDoRepositoryStub
gabrielteodoroo Aug 18, 2025
636a3d7
test: add FindAllToDoUseCase for listing todos
gabrielteodoroo Aug 18, 2025
8826118
feat: implement PrismaToDoRepository with findAll method
gabrielteodoroo Aug 18, 2025
691621c
feat: add ToDo module, controller and Swagger documentation
gabrielteodoroo Aug 18, 2025
a223da3
feat: add findById and update methods to todo repository
gabrielteodoroo Aug 18, 2025
cbea3b2
feat: add Swagger documentation for toggle todo endpoint
gabrielteodoroo Aug 18, 2025
2e18a69
feat: add ToggleToDoUseCase for managing todo completion status
gabrielteodoroo Aug 18, 2025
68a570b
test: add ToggleToDoUseCase for managing todo completion status
gabrielteodoroo Aug 18, 2025
d8884fa
feat: add toggle endpoint to ToDo controller and module
gabrielteodoroo Aug 18, 2025
ec56344
feat: add query parameter validation and documentation for todos list
gabrielteodoroo Aug 18, 2025
fe929d5
feat: add global ValidationPipe configuration
gabrielteodoroo Aug 18, 2025
ad7e46e
feat: add @Map decorator to isCompleted field
gabrielteodoroo Aug 18, 2025
b9385be
feat: add database migration for is_completed field mapping
gabrielteodoroo Aug 18, 2025
6d6a4f0
refactor(api): change todo routes to kebab-case convention
gabrielteodoroo Aug 18, 2025
001504e
refactor: rename request types for better clarity
gabrielteodoroo Aug 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9,705 changes: 9,705 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions prisma/migrations/20250818161357_todo_migration/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- CreateTable
CREATE TABLE "todos" (
"id" SERIAL NOT NULL,
"text" TEXT NOT NULL,
"isCompleted" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "todos_pkey" PRIMARY KEY ("id")
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Warnings:

- You are about to drop the column `isCompleted` on the `todos` table. All the data in the column will be lost.

*/
-- AlterTable
ALTER TABLE "todos" DROP COLUMN "isCompleted",
ADD COLUMN "is_completed" BOOLEAN NOT NULL DEFAULT false;
16 changes: 13 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@ datasource db {
}

model Example {
id Int @id @default(autoincrement())
text String
id Int @id @default(autoincrement())
text String
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")

@@map("examples")
}
}

model ToDo {
id Int @id @default(autoincrement())
text String
isCompleted Boolean @default(false) @map("is_completed")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")

@@map("todos")
}
6 changes: 6 additions & 0 deletions src/domain/entities/toDo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { BaseEntity } from "./base";

export interface ToDo extends BaseEntity {
text: string;
isCompleted: boolean;
}
12 changes: 12 additions & 0 deletions src/domain/repositories/toDo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ToDo } from "@domain/entities/toDo";

export abstract class ToDoRepository {
abstract create(text: string): Promise<void>;
abstract findAll(filter?: string): Promise<ToDo[]>;
abstract findById(id: number): Promise<ToDo | null>;
abstract update(
id: number,
text: string,
isCompleted: boolean
): Promise<void>;
}
10 changes: 10 additions & 0 deletions src/infra/config/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { NestFactory } from "@nestjs/core";
import { AppModule } from "@infra/modules/app";
import { SwaggerConfig } from "./swagger";
import { ValidationPipe } from "@nestjs/common";

async function bootstrap(): Promise<void> {
const app = await NestFactory.create(AppModule);

app.useGlobalPipes(
new ValidationPipe({
transform: true,
forbidNonWhitelisted: true,
whitelist: true
})
);

SwaggerConfig.config(app);
await app.listen(3001);
}
Expand Down
8 changes: 8 additions & 0 deletions src/infra/config/swagger/responses/toDo/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { applyDecorators } from "@nestjs/common";
import { ApiCreatedResponse } from "@nestjs/swagger";

export const CreateToDoResponse = applyDecorators(
ApiCreatedResponse({
description: "ToDo criado com sucesso!"
})
);
25 changes: 25 additions & 0 deletions src/infra/config/swagger/responses/toDo/find-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ToDo } from "@domain/entities/toDo";
import { applyDecorators } from "@nestjs/common";
import { ApiOkResponse, ApiProperty } from "@nestjs/swagger";

const TODO: ToDo = {
id: 1,
text: "ToDo 1",
isCompleted: false,
createdAt: new Date(),
updatedAt: new Date()
};

export class ToDoReturnDto {
@ApiProperty({
example: [TODO, TODO]
})
toDos: ToDo[];
}

export const FindAllToDoResponses = applyDecorators(
ApiOkResponse({
description: "Todas os toDos foram encontrados",
type: ToDoReturnDto
})
);
18 changes: 18 additions & 0 deletions src/infra/config/swagger/responses/toDo/toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { applyDecorators } from "@nestjs/common";
import { ApiOkResponse, ApiParam, ApiNotFoundResponse } from "@nestjs/swagger";

export const ToggleToDoResponses = applyDecorators(
ApiOkResponse({
description: "ToDo atualizado com sucesso!"
}),

ApiParam({
name: "id",
type: Number,
description: "ID do ToDo a ser atualizado"
}),

ApiNotFoundResponse({
description: "ToDo não encontrado"
})
);
11 changes: 11 additions & 0 deletions src/infra/controllers/toDo/dtos/create-toDo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";

export class CreateToDoDto {
@ApiProperty({
description: "Texto do ToDo"
})
@IsString()
@IsNotEmpty({ message: "Texto não pode ser vazio" })
text: string;
}
12 changes: 12 additions & 0 deletions src/infra/controllers/toDo/dtos/find-all-toDo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsOptional, IsString } from "class-validator";

export class FindAllToDoDto {
@ApiProperty({
description: "Filtro opcional para buscar toDos",
required: false
})
@IsOptional()
@IsString()
filter?: string;
}
48 changes: 48 additions & 0 deletions src/infra/controllers/toDo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {
Body,
Controller,
Get,
Param,
Patch,
ParseIntPipe,
Post,
Query
} from "@nestjs/common";
import { CreateToDoDto } from "./dtos/create-toDo";
import { CreateToDoUseCase } from "@use-cases/toDo/create";
import { CreateToDoResponse } from "@infra/config/swagger/responses/toDo/create";
import { ApiTags } from "@nestjs/swagger";
import { FindAllToDoResponses } from "@infra/config/swagger/responses/toDo/find-all";
import { ToDo } from "@domain/entities/toDo";
import { FindAllToDoUseCase } from "@use-cases/toDo/find-all";
import { ToggleToDoUseCase } from "@use-cases/toDo/toggle";
import { ToggleToDoResponses } from "@infra/config/swagger/responses/toDo/toggle";
import { FindAllToDoDto } from "./dtos/find-all-toDo";

@ApiTags("ToDo")
@Controller("to-do")
export class ToDoController {
constructor(
private readonly createToDoUseCase: CreateToDoUseCase,
private readonly findAllToDoUseCase: FindAllToDoUseCase,
private readonly toggleToDoUseCase: ToggleToDoUseCase
) {}

@Post()
@CreateToDoResponse
create(@Body() createToDoDto: CreateToDoDto): Promise<void> {
return this.createToDoUseCase.create(createToDoDto.text);
}

@Get()
@FindAllToDoResponses
findAll(@Query() { filter }: FindAllToDoDto): Promise<{ toDos: ToDo[] }> {
return this.findAllToDoUseCase.findAll({ filter });
}

@Patch(":id/toggle")
@ToggleToDoResponses
toggle(@Param("id", ParseIntPipe) id: number): Promise<void> {
return this.toggleToDoUseCase.toggle({ id });
}
}
3 changes: 2 additions & 1 deletion src/infra/modules/app/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Module } from "@nestjs/common";
import { ExampleModule } from "@infra/modules/example";
import { ToDoModule } from "@infra/modules/toDo";

@Module({
imports: [ExampleModule]
imports: [ExampleModule, ToDoModule]
})
export class AppModule {}
10 changes: 10 additions & 0 deletions src/infra/modules/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@ import { PrismaExampleRepository } from "@infra/repositories/prisma/example";
import { Module } from "@nestjs/common";
import { ExampleRepository } from "@domain/repositories/example";
import { Prisma } from "@infra/config/prisma";
import { PrismaToDoRepository } from "@infra/repositories/prisma/toDo";
import { ToDoRepository } from "@domain/repositories/toDo";

@Module({
providers: [
Prisma,
{
useClass: PrismaExampleRepository,
provide: ExampleRepository
},
{
useClass: PrismaToDoRepository,
provide: ToDoRepository
}
],
exports: [
{
useClass: PrismaExampleRepository,
provide: ExampleRepository
},
{
useClass: PrismaToDoRepository,
provide: ToDoRepository
}
]
})
Expand Down
13 changes: 13 additions & 0 deletions src/infra/modules/toDo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from "@nestjs/common";
import { CreateToDoUseCase } from "@use-cases/toDo/create";
import { DatabaseModule } from "@infra/modules/database";
import { ToDoController } from "@infra/controllers/toDo";
import { FindAllToDoUseCase } from "@use-cases/toDo/find-all";
import { ToggleToDoUseCase } from "@use-cases/toDo/toggle";

@Module({
imports: [DatabaseModule],
controllers: [ToDoController],
providers: [CreateToDoUseCase, FindAllToDoUseCase, ToggleToDoUseCase]
})
export class ToDoModule {}
47 changes: 47 additions & 0 deletions src/infra/repositories/prisma/toDo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ToDoRepository } from "@domain/repositories/toDo";
import { Prisma } from "@infra/config/prisma";
import { Injectable } from "@nestjs/common";
import { ToDo } from "@domain/entities/toDo";

@Injectable()
export class PrismaToDoRepository implements ToDoRepository {
constructor(private readonly prisma: Prisma) {}

async create(text: string): Promise<void> {
await this.prisma.toDo.create({ data: { text } });

return;
}

async findAll(filter?: string): Promise<ToDo[]> {
if (filter) {
return this.prisma.toDo.findMany({
where: {
text: {
contains: filter,
mode: "insensitive"
}
}
});
}

return this.prisma.toDo.findMany();
}

async findById(id: number): Promise<ToDo | null> {
const toDo = await this.prisma.toDo.findUnique({ where: { id } });

if (!toDo) {
return null;
}

return toDo;
}

async update(id: number, text: string, isCompleted: boolean): Promise<void> {
await this.prisma.toDo.update({
where: { id },
data: { text, isCompleted }
});
}
}
23 changes: 23 additions & 0 deletions src/use-cases/toDo/create/create.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ToDoRepository } from "@domain/repositories/toDo";
import { CreateToDoUseCase } from ".";
import { ToDoRepositoryStub } from "@test/stubs/repositories/toDo";

describe("Create ToDo Use Case", () => {
let sut: CreateToDoUseCase;
let toDoRepository: ToDoRepository;

beforeEach(() => {
toDoRepository = new ToDoRepositoryStub();
sut = new CreateToDoUseCase(toDoRepository);
});

it("should call a method that create a toDo", async () => {
jest.spyOn(toDoRepository, "create");

const TEXT = "text";

await sut.create(TEXT);

expect(toDoRepository.create).toHaveBeenCalledWith(TEXT);
});
});
11 changes: 11 additions & 0 deletions src/use-cases/toDo/create/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ToDoRepository } from "@domain/repositories/toDo";
import { Injectable } from "@nestjs/common";

@Injectable()
export class CreateToDoUseCase {
constructor(private readonly toDoRepository: ToDoRepository) {}

async create(text: string): Promise<void> {
return this.toDoRepository.create(text);
}
}
31 changes: 31 additions & 0 deletions src/use-cases/toDo/find-all/find-all.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { FindAllToDoUseCase } from ".";
import { ToDoRepositoryStub } from "@test/stubs/repositories/toDo";
import { ToDoRepository } from "@domain/repositories/toDo";

describe("Find All ToDo Use Case", () => {
let sut: FindAllToDoUseCase;
let toDoRepository: ToDoRepository;

beforeEach(() => {
toDoRepository = new ToDoRepositoryStub();
sut = new FindAllToDoUseCase(toDoRepository);
});

const TODO = {
id: 1,
text: "ToDo 1",
isCompleted: false,
createdAt: new Date(),
updatedAt: new Date()
};

const TODOS = [TODO, TODO, TODO];

it("should call a method that return a list of toDos", async () => {
jest.spyOn(toDoRepository, "findAll").mockResolvedValue(TODOS);

const result = await sut.findAll({ filter: "ToDo 1" });

expect(result).toStrictEqual({ toDos: TODOS });
});
});
Loading