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
47 changes: 44 additions & 3 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Run checks
run: yarn lint

test:
unit-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -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 All @@ -39,6 +39,47 @@ jobs:
retention-days: 7
overwrite: true

integration-test:
runs-on: ubuntu-latest
continue-on-error: true
env:
PG_HOST: localhost
PG_PORT: 5432
PG_DATABASE: herbario_test
PG_USERNAME: postgres
PG_PASSWORD: testpassword
PG_MIGRATION_USERNAME: postgres
PG_MIGRATION_PASSWORD: testpassword
services:
postgres:
image: postgis/postgis:18-3.6
env:
POSTGRES_DB: herbario_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpassword
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup environment
uses: actions/setup-node@v4
with:
node-version: 'lts/jod'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Apply base schema
run: psql -h $PG_HOST -p $PG_PORT -U $PG_MIGRATION_USERNAME -d $PG_DATABASE -f test/integration/setup/schema.sql
env:
PGPASSWORD: ${{ env.PG_MIGRATION_PASSWORD }}
- name: Run integration tests
run: yarn test:integration

build:
runs-on: ubuntu-latest
steps:
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
14 changes: 14 additions & 0 deletions compose.integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
postgres:
image: postgis/postgis:18-3.6
container_name: herbario_postgresql_e2e
environment:
POSTGRES_DB: herbario_e2e
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpassword
ports:
- "5433:5432"
volumes:
- ./test/integration/setup/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
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 @@ -158,6 +158,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 -r dotenv/config --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 @@ -72,10 +75,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 @@ -88,6 +95,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('/api', 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 }
}
}
Loading
Loading