-
Notifications
You must be signed in to change notification settings - Fork 2
Feature/eventos:Escrita - POST/PUT/DELETE #542
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vitorhugomoraes2486
wants to merge
6
commits into
532-cadastro-expedicoes
Choose a base branch
from
feature/eventos-escrita
base: 532-cadastro-expedicoes
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
b0695c3
feat: eventos - endpoints de escrita (POST/PUT/DELETE)
vitorhugomoraes2486 23900b3
test: adicionado testes de dominio faltantes
vitorhugomoraes2486 b391863
Update AtualizarEventoController.ts
vitorhugomoraes2486 432d71a
Update CriarEventoController.ts
vitorhugomoraes2486 0196a7b
Update AtualizarEventoUseCase.ts
vitorhugomoraes2486 63f0842
refactor: atende revisão da PR
vitorhugomoraes2486 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import { AtualizarEventoUseCase, Input as AtualizarEventoInput } from '@/domain/evento/AtualizarEventoUseCase' | ||
| import { EVENTO_TIPOS, EventoTipo } from '@/domain/evento/Evento' | ||
| import { CheckViolationError } from '@/infrastructure/error/CheckViolationError' | ||
| import { ForeignKeyViolationError } from '@/infrastructure/error/ForeignKeyViolationError' | ||
| import { InfrastructureError } from '@/infrastructure/error/InfrastructureError' | ||
| import { | ||
| HttpRequest, HttpResponse, StatusCode | ||
| } from '@/library/http/common' | ||
| import { BadRequestError } from '@/library/http/error/BadRequestError' | ||
| import { HttpError } from '@/library/http/error/HttpError' | ||
| import { InternalServerError } from '@/library/http/error/InternalServerError' | ||
| import { NotFoundError } from '@/library/http/error/NotFoundError' | ||
| import { UnprocessableEntityError } from '@/library/http/error/UnprocessableEntityError' | ||
| import { NextHandler, RequestHandler } from '@/library/http/Server' | ||
|
|
||
| import { parseColeta } from './coleta-parsing' | ||
|
|
||
| interface Dependencies { | ||
| atualizarEventoUseCase: AtualizarEventoUseCase | ||
| } | ||
|
|
||
| interface Body { | ||
| tipo?: unknown | ||
| capturado_em?: unknown | ||
| latitude?: unknown | ||
| longitude?: unknown | ||
| altitude?: unknown | ||
| observacoes?: unknown | ||
| coleta?: unknown | ||
| } | ||
|
|
||
| export class AtualizarEventoController implements RequestHandler { | ||
| private readonly atualizarEventoUseCase: AtualizarEventoUseCase | ||
|
|
||
| constructor(dependencies: Dependencies) { | ||
| this.atualizarEventoUseCase = dependencies.atualizarEventoUseCase | ||
| } | ||
|
|
||
| async handle(request: HttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> { | ||
| const { eventoId: rawEventoId } = request.params as { eventoId?: string } | ||
| const eventoId = parseId(rawEventoId, 'eventoId') | ||
| if (eventoId instanceof Error) return new BadRequestError({ message: eventoId.message }) | ||
|
|
||
| const body = (request.body ?? {}) as Body | ||
|
|
||
| // Substituir por request.usuario.id assim que a | ||
| // autenticação for integrada. | ||
| const input: AtualizarEventoInput = { id: eventoId, updated_by: null } | ||
|
|
||
| if (body.tipo !== undefined) { | ||
| const tipo = parseTipo(body.tipo) | ||
| if (tipo instanceof Error) return new BadRequestError({ message: tipo.message }) | ||
| input.tipo = tipo | ||
| } | ||
|
|
||
| if (body.capturado_em !== undefined) { | ||
| const capturadoEm = parseDate(body.capturado_em, 'capturado_em') | ||
| if (capturadoEm instanceof Error) return new BadRequestError({ message: capturadoEm.message }) | ||
| input.capturado_em = capturadoEm | ||
| } | ||
|
|
||
| if (body.latitude !== undefined) { | ||
| const latitude = parseOptionalNumber(body.latitude, 'latitude') | ||
| if (latitude instanceof Error) return new BadRequestError({ message: latitude.message }) | ||
| input.latitude = latitude | ||
| } | ||
|
|
||
| if (body.longitude !== undefined) { | ||
| const longitude = parseOptionalNumber(body.longitude, 'longitude') | ||
| if (longitude instanceof Error) return new BadRequestError({ message: longitude.message }) | ||
| input.longitude = longitude | ||
| } | ||
|
|
||
| if (body.altitude !== undefined) { | ||
| const altitude = parseOptionalNumber(body.altitude, 'altitude') | ||
| if (altitude instanceof Error) return new BadRequestError({ message: altitude.message }) | ||
| input.altitude = altitude | ||
| } | ||
|
|
||
| if (body.observacoes !== undefined) { | ||
| const observacoes = parseOptionalString(body.observacoes, 'observacoes') | ||
| if (observacoes instanceof Error) return new BadRequestError({ message: observacoes.message }) | ||
| input.observacoes = observacoes | ||
| } | ||
|
|
||
| if (body.coleta !== undefined) { | ||
| const coleta = body.coleta === null ? null : parseColeta(body.coleta) | ||
| if (coleta instanceof Error) return new BadRequestError({ message: coleta.message }) | ||
| input.coleta = coleta | ||
| } | ||
|
|
||
| const result = await this.atualizarEventoUseCase.execute(input) | ||
|
|
||
| if (result.left()) { | ||
| const error = result.value | ||
| if (error instanceof ForeignKeyViolationError) return new NotFoundError({ message: error.message }) | ||
| if (error instanceof CheckViolationError) return new UnprocessableEntityError({ message: error.message }) | ||
| if (!(error instanceof InfrastructureError)) return new BadRequestError({ message: error.message }) | ||
| return new InternalServerError({ message: error.message }) | ||
| } | ||
|
|
||
| if (!result.value) { | ||
| return new NotFoundError({ message: 'Evento não encontrado' }) | ||
| } | ||
|
|
||
| return { body: result.value, statusCode: StatusCode.Ok } | ||
| } | ||
| } | ||
|
|
||
| function parseId(raw: unknown, field: string): number | Error { | ||
| if (typeof raw !== 'string' || !/^\d+$/.test(raw)) { | ||
| return new Error(`${field} inválido`) | ||
| } | ||
| return Number(raw) | ||
| } | ||
|
|
||
| function parseTipo(raw: unknown): EventoTipo | Error { | ||
| if (typeof raw !== 'string' || !EVENTO_TIPOS.includes(raw as EventoTipo)) { | ||
| return new Error(`tipo inválido. Use um de: ${EVENTO_TIPOS.join(', ')}`) | ||
| } | ||
| return raw as EventoTipo | ||
| } | ||
|
|
||
| function parseDate(raw: unknown, field: string): Date | Error { | ||
| if (typeof raw !== 'string') { | ||
| return new Error(`${field} inválido. Use uma data no formato ISO 8601`) | ||
| } | ||
| const date = new Date(raw) | ||
| if (Number.isNaN(date.getTime())) { | ||
| return new Error(`${field} inválido. Use uma data no formato ISO 8601`) | ||
| } | ||
| return date | ||
| } | ||
|
|
||
| function parseOptionalNumber(raw: unknown, field: string): number | null | Error { | ||
| if (raw === undefined || raw === null) { | ||
| return null | ||
| } | ||
| if (typeof raw !== 'number' || Number.isNaN(raw)) { | ||
| return new Error(`${field} inválido`) | ||
| } | ||
| return raw | ||
| } | ||
|
|
||
| function parseOptionalString(raw: unknown, field: string): string | null | Error { | ||
| if (raw === undefined || raw === null) { | ||
| return null | ||
| } | ||
| if (typeof raw !== 'string') { | ||
| return new Error(`${field} inválido`) | ||
| } | ||
| return raw | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| import { CriarEventoUseCase } from '@/domain/evento/CriarEventoUseCase' | ||
| import { EVENTO_TIPOS, EventoTipo } from '@/domain/evento/Evento' | ||
| import { CheckViolationError } from '@/infrastructure/error/CheckViolationError' | ||
| import { ForeignKeyViolationError } from '@/infrastructure/error/ForeignKeyViolationError' | ||
| import { InfrastructureError } from '@/infrastructure/error/InfrastructureError' | ||
| import { | ||
| HttpRequest, HttpResponse, StatusCode | ||
| } from '@/library/http/common' | ||
| import { BadRequestError } from '@/library/http/error/BadRequestError' | ||
| import { HttpError } from '@/library/http/error/HttpError' | ||
| import { InternalServerError } from '@/library/http/error/InternalServerError' | ||
| import { NotFoundError } from '@/library/http/error/NotFoundError' | ||
| import { UnprocessableEntityError } from '@/library/http/error/UnprocessableEntityError' | ||
| import { NextHandler, RequestHandler } from '@/library/http/Server' | ||
|
|
||
| import { parseColeta } from './coleta-parsing' | ||
|
|
||
| interface Dependencies { | ||
| criarEventoUseCase: CriarEventoUseCase | ||
| } | ||
|
|
||
| interface Body { | ||
| tipo?: unknown | ||
| capturado_em?: unknown | ||
| latitude?: unknown | ||
| longitude?: unknown | ||
| altitude?: unknown | ||
| observacoes?: unknown | ||
| coleta?: unknown | ||
| } | ||
|
|
||
| export class CriarEventoController implements RequestHandler { | ||
| private readonly criarEventoUseCase: CriarEventoUseCase | ||
|
|
||
| constructor(dependencies: Dependencies) { | ||
| this.criarEventoUseCase = dependencies.criarEventoUseCase | ||
| } | ||
|
|
||
| async handle(request: HttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> { | ||
| const { expedicaoId: rawExpedicaoId } = request.params as { expedicaoId?: string } | ||
| const expedicaoId = parseId(rawExpedicaoId, 'expedicaoId') | ||
| if (expedicaoId instanceof Error) return new BadRequestError({ message: expedicaoId.message }) | ||
|
|
||
| const body = (request.body ?? {}) as Body | ||
|
|
||
| const tipo = parseTipo(body.tipo) | ||
| if (tipo instanceof Error) return new BadRequestError({ message: tipo.message }) | ||
|
|
||
| const capturadoEm = parseDate(body.capturado_em, 'capturado_em') | ||
| if (capturadoEm instanceof Error) return new BadRequestError({ message: capturadoEm.message }) | ||
|
|
||
| const latitude = parseOptionalNumber(body.latitude, 'latitude') | ||
| if (latitude instanceof Error) return new BadRequestError({ message: latitude.message }) | ||
|
|
||
| const longitude = parseOptionalNumber(body.longitude, 'longitude') | ||
| if (longitude instanceof Error) return new BadRequestError({ message: longitude.message }) | ||
|
|
||
| const altitude = parseOptionalNumber(body.altitude, 'altitude') | ||
| if (altitude instanceof Error) return new BadRequestError({ message: altitude.message }) | ||
|
|
||
| const observacoes = parseOptionalString(body.observacoes, 'observacoes') | ||
| if (observacoes instanceof Error) return new BadRequestError({ message: observacoes.message }) | ||
|
|
||
| const coleta = body.coleta === undefined ? null : parseColeta(body.coleta) | ||
| if (coleta instanceof Error) return new BadRequestError({ message: coleta.message }) | ||
|
|
||
| // Substituir por request.usuario.id assim que a | ||
| // autenticação for integrada. | ||
| const usuarioId = null | ||
|
|
||
| const result = await this.criarEventoUseCase.execute({ | ||
| altitude, | ||
| capturado_em: capturadoEm, | ||
| coleta, | ||
| created_by: usuarioId, | ||
| expedicao_id: expedicaoId, | ||
| latitude, | ||
| longitude, | ||
| observacoes, | ||
| tipo | ||
| }) | ||
|
|
||
| if (result.left()) { | ||
| const error = result.value | ||
| if (error instanceof ForeignKeyViolationError) return new NotFoundError({ message: error.message }) | ||
| if (error instanceof CheckViolationError) return new UnprocessableEntityError({ message: error.message }) | ||
| if (!(error instanceof InfrastructureError)) return new BadRequestError({ message: error.message }) | ||
| return new InternalServerError({ message: error.message }) | ||
| } | ||
|
|
||
| return { body: result.value, statusCode: StatusCode.Created } | ||
| } | ||
| } | ||
|
|
||
| function parseId(raw: unknown, field: string): number | Error { | ||
| if (typeof raw !== 'string' || !/^\d+$/.test(raw)) { | ||
| return new Error(`${field} inválido`) | ||
| } | ||
| return Number(raw) | ||
| } | ||
|
|
||
| function parseTipo(raw: unknown): EventoTipo | Error { | ||
| if (typeof raw !== 'string' || !EVENTO_TIPOS.includes(raw as EventoTipo)) { | ||
| return new Error(`tipo inválido. Use um de: ${EVENTO_TIPOS.join(', ')}`) | ||
| } | ||
| return raw as EventoTipo | ||
| } | ||
|
|
||
| function parseDate(raw: unknown, field: string): Date | Error { | ||
| if (typeof raw !== 'string') { | ||
| return new Error(`${field} inválido. Use uma data no formato ISO 8601`) | ||
| } | ||
| const date = new Date(raw) | ||
| if (Number.isNaN(date.getTime())) { | ||
| return new Error(`${field} inválido. Use uma data no formato ISO 8601`) | ||
| } | ||
| return date | ||
| } | ||
|
|
||
| function parseOptionalNumber(raw: unknown, field: string): number | null | Error { | ||
| if (raw === undefined || raw === null) { | ||
| return null | ||
| } | ||
| if (typeof raw !== 'number' || Number.isNaN(raw)) { | ||
| return new Error(`${field} inválido`) | ||
| } | ||
| return raw | ||
| } | ||
|
|
||
| function parseOptionalString(raw: unknown, field: string): string | null | Error { | ||
| if (raw === undefined || raw === null) { | ||
| return null | ||
| } | ||
| if (typeof raw !== 'string') { | ||
| return new Error(`${field} inválido`) | ||
| } | ||
| return raw | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { RemoverEventoUseCase } from '@/domain/evento/RemoverEventoUseCase' | ||
| import { CheckViolationError } from '@/infrastructure/error/CheckViolationError' | ||
| import { ForeignKeyViolationError } from '@/infrastructure/error/ForeignKeyViolationError' | ||
| import { | ||
| HttpRequest, HttpResponse, StatusCode | ||
| } from '@/library/http/common' | ||
| import { BadRequestError } from '@/library/http/error/BadRequestError' | ||
| import { HttpError } from '@/library/http/error/HttpError' | ||
| import { InternalServerError } from '@/library/http/error/InternalServerError' | ||
| import { NotFoundError } from '@/library/http/error/NotFoundError' | ||
| import { UnprocessableEntityError } from '@/library/http/error/UnprocessableEntityError' | ||
| import { NextHandler, RequestHandler } from '@/library/http/Server' | ||
|
|
||
| interface Dependencies { | ||
| removerEventoUseCase: RemoverEventoUseCase | ||
| } | ||
|
|
||
| export class RemoverEventoController implements RequestHandler { | ||
| private readonly removerEventoUseCase: RemoverEventoUseCase | ||
|
|
||
| constructor(dependencies: Dependencies) { | ||
| this.removerEventoUseCase = dependencies.removerEventoUseCase | ||
| } | ||
|
|
||
| async handle(request: HttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> { | ||
| const { eventoId: rawEventoId } = request.params as { eventoId?: string } | ||
| const eventoId = parseId(rawEventoId, 'eventoId') | ||
| if (eventoId instanceof Error) return new BadRequestError({ message: eventoId.message }) | ||
|
|
||
| const result = await this.removerEventoUseCase.execute({ id: eventoId }) | ||
|
|
||
| if (result.left()) { | ||
| const error = result.value | ||
| if (error instanceof ForeignKeyViolationError) return new NotFoundError({ message: error.message }) | ||
| if (error instanceof CheckViolationError) return new UnprocessableEntityError({ message: error.message }) | ||
| return new InternalServerError({ message: error.message }) | ||
| } | ||
| if (!result.value) { | ||
| return new NotFoundError({ message: 'Evento não encontrado' }) | ||
| } | ||
|
|
||
| return { statusCode: StatusCode.NoContent } | ||
| } | ||
| } | ||
|
|
||
| function parseId(raw: unknown, field: string): number | Error { | ||
| if (typeof raw !== 'string' || !/^\d+$/.test(raw)) { | ||
| return new Error(`${field} inválido`) | ||
| } | ||
| return Number(raw) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { COLETA_FIELDS, ColetaAttributes } from '@/domain/evento/Evento' | ||
|
|
||
| function isPlainObject(value: unknown): value is Record<string, unknown> { | ||
| return typeof value === 'object' && value !== null && !Array.isArray(value) | ||
| } | ||
|
|
||
| export function parseColeta(raw: unknown): ColetaAttributes | Error { | ||
| if (!isPlainObject(raw)) { | ||
| return new Error('coleta inválido. Envie um objeto com os campos da ficha') | ||
| } | ||
|
|
||
| const result = {} as Record<string, string | null> | ||
| for (const campo of COLETA_FIELDS) { | ||
| const value = raw[campo] | ||
| if (value === undefined) { | ||
| result[campo] = null | ||
| continue | ||
| } | ||
| if (value !== null && typeof value !== 'string') { | ||
| return new Error(`coleta.${campo} inválido`) | ||
| } | ||
| result[campo] = value | ||
| } | ||
| return result as unknown as ColetaAttributes | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Standards] Assimetria de estilo (não-bloqueante). Diferente de
CriarEventoController/AtualizarEventoController, aqui qualquerleft()vira 500 direto, sem checarinstanceof CheckViolationError/ForeignKeyViolationError. Hoje é inofensivo —delete()por id não produz esses erros — mas se um novo tipo de erro for adicionado aopg-error.tsno futuro, é fácil esquecer de replicar aqui também.