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
2 changes: 2 additions & 0 deletions src/application/create-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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 createRelevoRoutes } from './relevo'
import { routes as createVegetacaoRoutes } from './vegetacao'

interface CorsParameters {
Expand Down Expand Up @@ -58,6 +59,7 @@ export function createApp({
const routes: Route[] = [
...createPaisRoutes(knex),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O PR está em conflito com development. No rebase, registre createRelevoRoutes sem remover createVegetacaoRoutes, que já existe na base.

...createEstadoRoutes(knex),
...createRelevoRoutes(knex),
...createFaseSucessionalRoutes(knex),
...createVegetacaoRoutes(knex)
]
Expand Down
41 changes: 41 additions & 0 deletions src/application/relevo/BuscaRelevoController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { BuscaRelevoPorIdUseCase } from '@/domain/relevo/BuscaRelevoPorIdUseCase'
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 {
buscaRelevoPorIdUseCase: BuscaRelevoPorIdUseCase
}

export class BuscaRelevoController implements RequestHandler {
private readonly buscaRelevoPorIdUseCase: BuscaRelevoPorIdUseCase

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

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

if (relevoId === undefined || relevoId === null || relevoId === '' || !/^\d+$/.test(relevoId)) {
return new BadRequestError({ message: 'relevoId inválido' })
}

const result = await this.buscaRelevoPorIdUseCase.execute({ id: Number(relevoId) })

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

if (!result.value) {
return new NotFoundError({ message: 'Relevo não encontrado' })
}

return { statusCode: StatusCode.Ok, body: result.value }
}
}
65 changes: 65 additions & 0 deletions src/application/relevo/ListaRelevosController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ListaRelevosUseCase } from '@/domain/relevo/ListaRelevosUseCase'
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 {
listaRelevosUseCase: ListaRelevosUseCase
}

export class ListaRelevosController implements RequestHandler {
private readonly listaRelevosUseCase: ListaRelevosUseCase

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

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.listaRelevosUseCase.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/relevo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { type Knex } from 'knex'

import { BuscaRelevoPorIdUseCase } from '@/domain/relevo/BuscaRelevoPorIdUseCase'
import { ListaRelevosUseCase } from '@/domain/relevo/ListaRelevosUseCase'
import { RelevoCollectionKnexAdapter } from '@/infrastructure/RelevoCollectionKnexAdapter'
import { Method } from '@/library/http/common'
import { Route } from '@/library/http/Router'

import { BuscaRelevoController } from './BuscaRelevoController'
import { ListaRelevosController } from './ListaRelevosController'

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

return [
{
handlers: [
new ListaRelevosController({
listaRelevosUseCase: new ListaRelevosUseCase({ relevoCollection })
})
],
method: Method.Get,
path: '/v2/relevos'
},
{
handlers: [
new BuscaRelevoController({
buscaRelevoPorIdUseCase: new BuscaRelevoPorIdUseCase({ relevoCollection })
})
],
method: Method.Get,
path: '/v2/relevos/:relevoId'
}
]
}
20 changes: 20 additions & 0 deletions src/domain/relevo/BuscaRelevoPorIdUseCase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Either } from '@/library/either/Either'

import { Attributes } from './Relevo'
import { RelevoCollection } from './RelevoCollection'

interface Dependencies {
relevoCollection: RelevoCollection
}

export class BuscaRelevoPorIdUseCase {
private readonly relevoCollection: RelevoCollection

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

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

import { Attributes } from './Relevo'
import { RelevoCollection, RelevoFilters } from './RelevoCollection'

interface Dependencies {
relevoCollection: RelevoCollection
}

export class ListaRelevosUseCase {
private readonly relevoCollection: RelevoCollection

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

execute(filters: RelevoFilters): Promise<Either<Error, Attributes[]>> {
return this.relevoCollection.findAll(filters)
}
}
24 changes: 24 additions & 0 deletions src/domain/relevo/Relevo.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 Relevo {
readonly id: number
readonly nome: string

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

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

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

import { Attributes } from './Relevo'

export interface RelevoOrder {
column: 'id' | 'nome'
direction: 'asc' | 'desc'
}

export interface RelevoFilters {
nome?: string
order?: RelevoOrder
}

export interface RelevoCollection {
findAll(filters: RelevoFilters): Promise<Either<Error, Attributes[]>>
findById(id: number): Promise<Either<Error, Attributes | null>>
}
46 changes: 46 additions & 0 deletions src/infrastructure/RelevoCollectionKnexAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Knex } from 'knex'

import { Attributes } from '@/domain/relevo/Relevo'
import { RelevoCollection, RelevoFilters } from '@/domain/relevo/RelevoCollection'
import { Either } from '@/library/either/Either'

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

interface Dependencies {
knex: Knex
}

export class RelevoCollectionKnexAdapter implements RelevoCollection {
private readonly knex: Knex

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

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

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

const order = filters.order ?? { column: 'id', direction: 'desc' }
query.orderBy(order.column, order.direction)

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

async findById(id: number): Promise<Either<Error, Attributes | null>> {
try {
const relevo = await this.knex<Attributes>('relevos').select(['id', 'nome']).where({ id }).first()
return Either.right(relevo ?? null)
} catch (error) {
return Either.left(new CollectionError({ message: 'Failed to find relevo', cause: error }))
}
}
}
42 changes: 42 additions & 0 deletions test/integration/relevo/busca-relevos.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
afterAll, describe, expect, test
} from 'vitest'

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

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

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

describe('GET /api/v2/relevos/:relevoId', () => {
const { agent, knex } = createTestApp()

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

test('retorna o registro encontrado', async () => {
const [relevo] = await knex('relevos')
.insert({ nome: 'XREL Relevo Encontrado' })
.returning<Relevo[]>(returning)

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

test('retorna 404 para id inexistente', async () => {
const response = await agent.get('/api/v2/relevos/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/relevos/abc').expect(400)
const body = response.body as { error: { message: string } }

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