Skip to content
Merged
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
2 changes: 2 additions & 0 deletions 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 createFaseSucessionalRoutes } from './fase-sucessional'
import { routes as createPaisRoutes } from './pais'
import { routes as createVegetacaoRoutes } from './vegetacao'

Expand Down Expand Up @@ -57,6 +58,7 @@ export function createApp({
const routes: Route[] = [
...createPaisRoutes(knex),
...createEstadoRoutes(knex),
...createFaseSucessionalRoutes(knex),
...createVegetacaoRoutes(knex)
]
const application = new ExpressApplication({ logger })
Expand Down
42 changes: 42 additions & 0 deletions src/application/fase-sucessional/BuscaFaseSucessionalController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BuscaFaseSucessionalUseCase } from '@/domain/faseSucessional/BuscaFaseSucessionalUseCase'
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 { NextHandler, RequestHandler } from '@/library/http/Server'

interface Dependencies {
buscaFaseSucessionalUseCase: BuscaFaseSucessionalUseCase
}

export class BuscaFaseSucessionalController implements RequestHandler {
private readonly buscaFaseSucessionalUseCase: BuscaFaseSucessionalUseCase

constructor(dependencies: Dependencies) {
this.buscaFaseSucessionalUseCase = dependencies.buscaFaseSucessionalUseCase
}

async handle(request: HttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> {
const { faseSucessionalId } = request.params as { faseSucessionalId?: string }

if (!faseSucessionalId || Number.isNaN(Number(faseSucessionalId)) || Number(faseSucessionalId) <= 0) {
return new BadRequestError({ message: 'faseSucessionalId inválido' })
}

const id = Number(faseSucessionalId)
const result = await this.buscaFaseSucessionalUseCase.execute({ id })

if (result.left()) {
return new InternalServerError({ message: result.value.message })
}

if (!result.value) {
return new NotFoundError({ message: 'Fase sucessional não encontrada' })
}

return { statusCode: StatusCode.Ok, body: result.value }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ListaFasesSucessionaisUseCase } from '@/domain/faseSucessional/ListaFasesSucessionaisUseCase'
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 { NextHandler, RequestHandler } from '@/library/http/Server'

interface Dependencies {
listaFasesSucessionaisUseCase: ListaFasesSucessionaisUseCase
}

export class ListaFasesSucessionaisController implements RequestHandler {
private readonly listaFasesSucessionaisUseCase: ListaFasesSucessionaisUseCase

constructor(dependencies: Dependencies) {
this.listaFasesSucessionaisUseCase = dependencies.listaFasesSucessionaisUseCase
}

async handle(request: HttpRequest, _next: NextHandler): Promise<HttpResponse | HttpError> {
const { nome, order } = request.params as {
nome?: string
order?: string
}

const parsedOrder = parseOrder(order)
if (parsedOrder instanceof Error) {
return new BadRequestError({ message: parsedOrder.message })
}

const result = await this.listaFasesSucessionaisUseCase.execute({
nome,
order: parsedOrder
})

if (result.left()) {
return new InternalServerError({ message: result.value.message })
}

return { statusCode: StatusCode.Ok, body: result.value }
}
}

function parseOrder(order?: string): { column: 'id' | 'nome'; direction: 'asc' | 'desc' } | Error | undefined {
if (!order) return undefined

const pieces = order.split(':')
if (pieces.length !== 2) {
return new Error('order inválido. Use o formato "id:asc", "id:desc", "nome:asc" ou "nome:desc"')
}

const [rawColumn, rawDirection] = pieces
const column = rawColumn.trim().toLowerCase()
const direction = rawDirection.trim().toLowerCase()

if ((column !== 'id' && column !== 'nome') || (direction !== 'asc' && direction !== 'desc')) {
return new Error('order inválido. Use o formato "id:asc", "id:desc", "nome:asc" ou "nome:desc"')
}

return {
column,
direction
}
}
35 changes: 35 additions & 0 deletions src/application/fase-sucessional/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type Knex } from 'knex'

import { BuscaFaseSucessionalUseCase } from '@/domain/faseSucessional/BuscaFaseSucessionalUseCase'
import { ListaFasesSucessionaisUseCase } from '@/domain/faseSucessional/ListaFasesSucessionaisUseCase'
import { FaseSucessionalCollectionKnexAdapter } from '@/infrastructure/FaseSucessionalCollectionKnexAdapter'
import { Method } from '@/library/http/common'
import { Route } from '@/library/http/Router'

import { BuscaFaseSucessionalController } from './BuscaFaseSucessionalController'
import { ListaFasesSucessionaisController } from './ListaFasesSucessionaisController'

export function routes(knex: Knex): Route[] {
const faseSucessionalCollection = new FaseSucessionalCollectionKnexAdapter({ knex })

return [
{
handlers: [
new ListaFasesSucessionaisController({
listaFasesSucessionaisUseCase: new ListaFasesSucessionaisUseCase({ faseSucessionalCollection })
})
],
method: Method.Get,
path: '/v2/fases-sucessionais'
},
{
handlers: [
new BuscaFaseSucessionalController({
buscaFaseSucessionalUseCase: new BuscaFaseSucessionalUseCase({ faseSucessionalCollection })
})
],
method: Method.Get,
path: '/v2/fases-sucessionais/:faseSucessionalId'
}
]
}
20 changes: 20 additions & 0 deletions src/domain/faseSucessional/BuscaFaseSucessionalUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Either } from '@/library/either/Either'

import { Attributes } from './FaseSucessional'
import { FaseSucessionalCollection } from './FaseSucessionalCollection'

interface Dependencies {
faseSucessionalCollection: FaseSucessionalCollection
}

export class BuscaFaseSucessionalUseCase {
private readonly faseSucessionalCollection: FaseSucessionalCollection

constructor(dependencies: Dependencies) {
this.faseSucessionalCollection = dependencies.faseSucessionalCollection
}

async execute({ id }: { id: number }): Promise<Either<Error, Attributes | null>> {
return this.faseSucessionalCollection.findById(id)
}
}
24 changes: 24 additions & 0 deletions src/domain/faseSucessional/FaseSucessional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Either } from '@/library/either/Either'

export interface Attributes {
id: number
nome: string
}

export class FaseSucessional {
readonly id: number
readonly nome: string

private constructor(attributes: Attributes) {
this.id = attributes.id
this.nome = attributes.nome
}

static create(attributes: Attributes): Either<Error, FaseSucessional> {
if (!attributes.nome.trim()) {
return Either.left(new Error('Nome da fase sucessional não pode ser vazio'))
}

return Either.right(new FaseSucessional(attributes))
}
}
16 changes: 16 additions & 0 deletions src/domain/faseSucessional/FaseSucessionalCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Either } from '@/library/either/Either'

import { Attributes } from './FaseSucessional'

export interface FaseSucessionalFilters {
nome?: string
order?: {
column: 'id' | 'nome'
direction: 'asc' | 'desc'
}
}

export interface FaseSucessionalCollection {
findAll(filters: FaseSucessionalFilters): Promise<Either<Error, Attributes[]>>
findById(id: number): Promise<Either<Error, Attributes | null>>
}
20 changes: 20 additions & 0 deletions src/domain/faseSucessional/ListaFasesSucessionaisUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Either } from '@/library/either/Either'

import { Attributes } from './FaseSucessional'
import { FaseSucessionalCollection, FaseSucessionalFilters } from './FaseSucessionalCollection'

interface Dependencies {
faseSucessionalCollection: FaseSucessionalCollection
}

export class ListaFasesSucessionaisUseCase {
private readonly faseSucessionalCollection: FaseSucessionalCollection

constructor(dependencies: Dependencies) {
this.faseSucessionalCollection = dependencies.faseSucessionalCollection
}

async execute(filters: FaseSucessionalFilters): Promise<Either<Error, Attributes[]>> {
return this.faseSucessionalCollection.findAll(filters)
}
}
53 changes: 53 additions & 0 deletions src/infrastructure/FaseSucessionalCollectionKnexAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Knex } from 'knex'

import { Attributes } from '@/domain/faseSucessional/FaseSucessional'
import { FaseSucessionalCollection, FaseSucessionalFilters } from '@/domain/faseSucessional/FaseSucessionalCollection'
import { Either } from '@/library/either/Either'

import { CollectionError } from './error/CollectionError'

interface Dependencies {
knex: Knex
}

export class FaseSucessionalCollectionKnexAdapter implements FaseSucessionalCollection {
private readonly knex: Knex

constructor(dependencies: Dependencies) {
this.knex = dependencies.knex
}

async findAll(filters: FaseSucessionalFilters): Promise<Either<Error, Attributes[]>> {
try {
const query = this.knex<Attributes>('fase_sucessional')
.select(['id', 'nome'])

if (filters.nome) {
query.whereILike('nome', `%${filters.nome}%`)
}

if (filters.order) {
query.orderBy(filters.order.column, filters.order.direction)
} else {
query.orderBy('id', 'desc')
}

return Either.right(await query)
} catch (error) {
return Either.left(new CollectionError({ message: 'Failed to list fases sucessionais', cause: error }))
}
}

async findById(id: number): Promise<Either<Error, Attributes | null>> {
try {
const faseSucessional = await this.knex<Attributes>('fase_sucessional')
.select(['id', 'nome'])
.where({ id })
.first()

return Either.right(faseSucessional ?? null)
} catch (error) {
return Either.left(new CollectionError({ message: 'Failed to find fase sucessional by id', cause: error }))
}
}
}
50 changes: 50 additions & 0 deletions test/integration/fase-sucessional/busca-fases-sucessionais.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
afterAll, describe, expect, test
} from 'vitest'

import { createTestApp } from '../setup/app-factory'

type FaseSucessional = { id: number; nome: string }

const returning = ['id', 'nome'] as const

describe('GET /api/v2/fases-sucessionais/:faseSucessionalId', () => {
const { agent, knex } = createTestApp()

afterAll(() => knex.destroy())

test('retorna o registro encontrado', async () => {
const nome = `XFAS_BUSCA_${Date.now()}`
const [faseSucessional] = await knex('fase_sucessional')
.insert({ nome })
.returning<FaseSucessional[]>(returning)

try {
const response = await agent.get(`/api/v2/fases-sucessionais/${faseSucessional.id}`).expect(200)
expect(response.body).toEqual({ id: faseSucessional.id, nome: faseSucessional.nome })
} finally {
await knex('fase_sucessional').where({ id: faseSucessional.id }).delete()
}
})

test('retorna 404 para id inexistente', async () => {
const response = await agent.get('/api/v2/fases-sucessionais/999999').expect(404)
const body = response.body as { error: { message: string } }

expect(body.error.message).toMatch(/não encontrad[ao]|not found/i)
})

test('retorna 400 para id inválido', async () => {
const response = await agent.get('/api/v2/fases-sucessionais/abc').expect(400)
const body = response.body as { error: { message: string } }

expect(body.error.message).toMatch(/inválido|invalid/i)
})

test('retorna 400 para id negativo', async () => {
const response = await agent.get('/api/v2/fases-sucessionais/-12').expect(400)
const body = response.body as { error: { message: string } }

expect(body.error.message).toMatch(/inválido|invalid/i)
})
})
Loading
Loading