Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion src/application/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { assets, upload } from '../config/directory'
import legacyErrors from '../middlewares/erros-middleware'
import { generatePreview, reportPreview } from '../reports/controller'
import { routes as createEstadoRoutes } from './estado'
import { routes as createEventoRoutes } from './evento'
import { routes as createFaseSucessionalRoutes } from './fase-sucessional'
import { routes as createPaisRoutes } from './pais'
import { routes as createVegetacaoRoutes } from './vegetacao'
Expand Down Expand Up @@ -59,7 +60,8 @@ export function createApp({
...createPaisRoutes(knex),
...createEstadoRoutes(knex),
...createFaseSucessionalRoutes(knex),
...createVegetacaoRoutes(knex)
...createVegetacaoRoutes(knex),
...createEventoRoutes(knex)
]
const application = new ExpressApplication({ logger })

Expand Down
153 changes: 153 additions & 0 deletions src/application/evento/AtualizarEventoController.ts
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
}
138 changes: 138 additions & 0 deletions src/application/evento/CriarEventoController.ts
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
}
51 changes: 51 additions & 0 deletions src/application/evento/RemoverEventoController.ts
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 })

Copy link
Copy Markdown

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 qualquer left() vira 500 direto, sem checar instanceof CheckViolationError/ForeignKeyViolationError. Hoje é inofensivo — delete() por id não produz esses erros — mas se um novo tipo de erro for adicionado ao pg-error.ts no futuro, é fácil esquecer de replicar aqui também.


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)
}
25 changes: 25 additions & 0 deletions src/application/evento/coleta-parsing.ts
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
}
Loading