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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ TZ=UTC
PORT=3000
NODE_ENV=development

# Optional: CORS (used by Application). Comma-separated origins, or * for all.
CORS_ORIGINS=*
CORS_METHODS=HEAD,GET,POST,PUT,PATCH,DELETE
CORS_ALLOWED_HEADERS=Content-Type,Authorization

STORAGE_PATH=./uploads

MYSQL_DATABASE=herbario_dev
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ jobs:
node-version: 'lts/jod'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run tests with coverage
run: yarn test --silent --coverage
- name: Run unit tests with coverage
run: yarn test:coverage
- name: Archive code coverage results
uses: actions/upload-artifact@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1 +1 @@
npm run test --silent
npm run test:unit -- --silent
12 changes: 12 additions & 0 deletions compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
services:
postgres_test:
image: postgis/postgis:18-3.6
container_name: herbario_postgresql_test
environment:
POSTGRES_DB: herbario_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpassword
ports:
- "5433:5432"
tmpfs:
- /var/lib/postgresql
File renamed without changes.
16 changes: 16 additions & 0 deletions eslint.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,22 @@ export default defineConfig([
'@stylistic/object-property-newline': [
'error',
{ allowAllPropertiesOnSameLine: true }
],
'@stylistic/operator-linebreak': [
'warn',
'before',
{ overrides: { '=': 'after' } }
],
'@stylistic/quote-props': [
'error',
'as-needed'
],
'no-restricted-syntax': [
'warn',
{
message: 'Use EnumOf pattern instead of TypeScript enums.',
selector: 'TSEnumDeclaration'
}
]
}
},
Expand Down
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@
"lint": "run-p tsc:check lint:eslint",
"clean": "rimraf dist",
"build:app": "babel -d ./dist -x '.js,.ts,.tsx' --copy-files --no-copy-ignored ./src",
"start": "nodemon --ext 'js,ts,tsx' --exec babel-node --extensions '.js,.ts,.tsx' ./src/index.js",
"start": "nodemon --ext 'js,ts,tsx' --exec babel-node --extensions '.js,.ts,.tsx' ./src/application/index.ts",
"build": "run-s clean build:app",
"test": "vitest --run",
"test:coverage": "vitest --run --coverage",
"test:unit": "vitest --run --project unit",
"test:unit:watch": "vitest --project unit",
"test:integration": "vitest --run --project integration",
"test:integration:watch": "vitest --project integration",
"test:coverage": "vitest --run --project unit --coverage",
"prepare": "husky",
"audit": "npm audit --audit-level=moderate",
"audit:fix": "npm audit fix"
Expand Down Expand Up @@ -70,10 +73,14 @@
"@babel/preset-typescript": "7.28.5",
"@eslint/js": "9.39.1",
"@stylistic/eslint-plugin": "5.5.0",
"@types/cors": "2.8.19",
"@types/express": "5.0.5",
"@types/morgan": "1.9.10",
"@types/pg": "^8.16.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@types/supertest": "7.2.0",
"@types/swagger-ui-express": "^4.1.8",
"@vitest/coverage-v8": "4.0.7",
"babel-plugin-module-resolver": "5.0.2",
"chai": "6.2.0",
Expand All @@ -86,6 +93,7 @@
"nodemon": "3.1.10",
"npm-run-all": "4.1.5",
"rimraf": "6.1.2",
"supertest": "7.2.2",
"typescript": "5.9.3",
"typescript-eslint": "8.46.3",
"vitest": "4.0.7"
Expand Down
102 changes: 0 additions & 102 deletions src/app.js

This file was deleted.

94 changes: 94 additions & 0 deletions src/application/create-app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import makeCors from 'cors'
import express from 'express'
import makeHelmet from 'helmet'
import { Knex } from 'knex'
import morgan from 'morgan'

import { ExpressApplication } from '@/infrastructure/ExpressApplication'
import { Route } from '@/library/http/Router'
import { Logger } from '@/library/logger/Logger'

import { 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 createPaisRoutes } from './pais'

interface CorsParameters {
origins: string[]
methods: string[]
allowedHeaders: string[]
}

interface Parameters {
logger: Logger
cors: CorsParameters
knex: Knex
legacyRouter?: unknown
}

const securityConfig = {
crossOriginEmbedderPolicy: false,
contentSecurityPolicy: {
directives: {
defaultSrc: ['"self"'],
styleSrc: ['"self"', '"unsafe-inline"'],
scriptSrc: ['"self"'],
imgSrc: [
'"self"',
'"data:"',
'"https:"'
]
}
}
}

export function createApp({
logger, cors, knex, legacyRouter
}: Parameters) {
const routes: Route[] = [
...createPaisRoutes(knex),
...createEstadoRoutes(knex)
]
const application = new ExpressApplication({ logger })

application
.use(makeHelmet(securityConfig))
.use(makeCors({
origin: cors.origins,
methods: cors.methods,
allowedHeaders: cors.allowedHeaders
}))
.use(morgan('dev'))
// .use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
// .use('/fotos', express.static(upload))
// .use('/assets', express.static(assets))
.use(
'/uploads',
express.static(upload, {
index: false,
redirect: false,
setHeaders: res => {
res.setHeader('Cache-Control', 'public, max-age=2592000, immutable')
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin')
}
})
)

const reportsRouter = express.Router()
reportsRouter.get('/:fileName', reportPreview)
reportsRouter.post('/:fileName', generatePreview)
application.use('/reports', reportsRouter)

for (const route of routes) {
const sanitizedPath = `/api/${route.path}`.replaceAll(/\/{2,}/g, '/').replaceAll(/\/$/g, '')
application.endpoint(route.method, sanitizedPath, ...route.handlers)
}

if (legacyRouter) {
application.use(legacyRouter)
}
application.use(legacyErrors)

return application
}
36 changes: 36 additions & 0 deletions src/application/estado/ListaEstadosController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ListaEstadosUseCase } from '@/domain/estado/ListaEstadosUseCase'
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 {
listaEstadosUseCase: ListaEstadosUseCase
}

export class ListaEstadosController implements RequestHandler {
private readonly listaEstadosUseCase: ListaEstadosUseCase

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

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

if (!paisSigla) {
return new BadRequestError({ message: 'paisSigla é obrigatório' })
}

const result = await this.listaEstadosUseCase.execute({ paisSigla })

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

return { body: result.value, statusCode: StatusCode.Ok }
}
}
24 changes: 24 additions & 0 deletions src/application/estado/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { type Knex } from 'knex'

import { ListaEstadosUseCase } from '@/domain/estado/ListaEstadosUseCase'
import { EstadoCollectionKnexAdapter } from '@/infrastructure/EstadoCollectionKnexAdapter'
import { Method } from '@/library/http/common'
import { Route } from '@/library/http/Router'

import { ListaEstadosController } from './ListaEstadosController'

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

return [
{
handlers: [
new ListaEstadosController({
listaEstadosUseCase: new ListaEstadosUseCase({ estadoCollection })
})
],
method: Method.Get,
path: '/paises/:paisSigla/estados'
}
]
}
Loading
Loading